开放封闭原则

206 阅读5分钟

开放封闭原则

为什么需要开放封闭原则

  • 代码有修改,就会有出错的风险,所以应尽量避免对原有代码的修改。
  • 当需求发生变化的时候,我们只想通过新增文件的方式来实现。这样只需要对新增的文件进行测试,确保代码正确即可。

什么是开放封闭原则

定义:软件实体如类、模块、函数应该对扩展开放,对修改关闭。

开放封闭原则的主要思想是:当系统需求发生改变时,尽量不修改系统原有代码功能(但可以直接删除或标记deprecated),应该扩展模块的功能,来实现新的需求。

如何实现开放封闭原则

首先预测、判断哪些代码是不稳定的代码。然后通过接口,组合的设计将稳定不稳定的代码区分开。

  1. 接口:依赖接口,将可能发生变化的代码用接口代替,当需求发生变化时,只需要新增,并注入接口实现即可,这样即实现了功能的扩展,也不会修改原有代码,遵循开闭原则。
  2. 组合:通过组合多个稳定的代码模块实现一个可能发生变化的功能模块。
    1. 当功能发生变化时,只需要创建新文件,重新组合功能即可。原有功能模块文件直接删除
    2. 当新增功能时,直接创建新文件。原有功能模块文件不需要修改

接口的具体实现和组合产生的新文件都属于不稳定的代码。

实例:登录接口设计

遵循开放封闭原则的设计

需求v1.0:PC端登录成功返回token,nickname

// 认证服务,认证通过返回认证信息
class AuthService {
    AuthInfo auth(String account, String password) {
        // ....
    }
}

// 用户服务,返回用户数据
class UserService {
    User getUser(String uid) {
        // ....
    }
}

// 登录成功后返回的数据
class LoginInfo {
    final String nickname;
    final String token;
}

// api
class LoginController {
    final AuthService authService;
    final UserService userService;
    
    LoginInfo login(String account, String password) {
        AuthInfo authInfo = authService.auth(account, password);
        String uid = getUid(authInfo.token);
        User user = userService.getUser(uid);
        return LoginInfo(user.nickname, authInfo.token);
    }
}

需求v2.0:增加android端登录,成功返回token, nickname, avatar

// 登录成功后返回的数据
class AndroidLoginInfo {
    final String nickname;
    final String token;
    final string avatar;
}

// api
class AndroidLoginController {
    final AuthService authService;
    final UserService userService;
    
    LoginInfo login(String account, String password) {
        AuthInfo authInfo = authService.auth(account, password);
        String uid = getUid(authInfo.token);
        User user = userService.getUser(uid);
        return LoginInfo(user.nickname, authInfo.token, user.avatar);
    }
}

需求3.0: 增加iOS端登录,返回token,nickname,postCount(用户帖子数量);

// 登录成功后返回的数据
class iOSLoginInfo {
    final String nickname;
    final String token;
    final string avatar;
    final int postCount;
}

// 帖子服务
class PostService {
    int getUserPostCount(String uid);
}

// api
class iOSLoginController {
    final AuthService authService;
    final UserService userService;
    final PostService postService;
    
    LoginInfo login(String account, String password) {
        AuthInfo authInfo = authService.auth(account, password);
        String uid = getUid(authInfo.token);
        int postCount = postService.getUserPostCount(uid);
        User user = userService.getUser(uid);
        return LoginInfo(user.nickname, authInfo.token, user.avatar, postCount);
    }
}

随着上述3个版本的变化,我们始终没有去修改原有代码,都只是新增代码文件,遵循了开闭原则。 优点:

  • 三个API相互解耦,不用担心Android和iOS需求的变化,会对pc端造成什么影响。
  • 三个接口都很好的复用基础服务(authService, UserService,PostService)的代码 反驳的观点:
  • 需要创建更多的文件,写更多的代码(他们判断代码质量的好坏就是代码行数,就喜欢一个函数适用所有功能的“银弹”,喜欢公共垃圾桶的设计,如下面例子)

违反开放封闭原则的设计

需求v1.0:PC端登录成功返回token,nickname

// 认证服务,认证通过返回认证信息
class AuthService {
    AuthInfo auth(String account, String password) {
        // ....
    }
}

// 用户服务,返回用户数据
class UserService {
    User getUser(String uid) {
        // ....
    }
}

// 登录成功后返回的数据
class LoginInfo {
    final String nickname;
    final String token;
}

// api
class LoginController {
    final AuthService authService;
    final UserService userService;
    
    LoginInfo login(String account, String password) {
        AuthInfo authInfo = authService.auth(account, password);
        String uid = getUid(authInfo.token);
        User user = userService.getUser(uid);
        return LoginInfo(user.nickname, authInfo.token);
    }
}

需求v2.0:增加android端登录,成功返回token, nickname, avatar

使用同一个接口,修改LoginInfo和LoginController来实现功能的扩展, 测试需要同时对Android,PC端的登录功能进行测试验证。

// 登录成功后返回的数据
class LoginInfo {
    final String nickname;
    final String token;
    final string avatar;
}

// api
class LoginController {
    final AuthService authService;
    final UserService userService;
    
    LoginInfo login(String account, String password) {
        AuthInfo authInfo = authService.auth(account, password);
        String uid = getUid(authInfo.token);
        User user = userService.getUser(uid);
        return LoginInfo(user.nickname, authInfo.token, user.avatar);
    }
}

需求3.0: 增加iOS端登录,返回token,nickname,postCount(用户帖子数量);

继续使用同一个接口,修改LoginInfo和LoginController来实现功能的扩展, 测试需要同时对Android,PC,iOS三端的登录功能进行测试验证。

// 登录成功后返回的数据
class LoginInfo {
    final String nickname;
    final String token;
    final string avatar;
}

// 帖子服务
class PostService {
    int getUserPostCount(String uid);
}

// api
class LoginController {
    final AuthService authService;
    final UserService userService;
    final PostService postService;
    
    LoginInfo login(String account, String password) {
        AuthInfo authInfo = authService.auth(account, password);
        String uid = getUid(authInfo.token);
        int postCount = postService.getUserPostCount(uid);
        User user = userService.getUser(uid);
        return LoginInfo(user.nickname, authInfo.token, user.avatar, postCount);
    }
}

缺点:

  1. 登录接口已经成为了一个公共的接口,维护者必须同时了解三端的业务需求才敢修改这快代码,
  2. 每一次功能修改都需要对Android,iOS,PC进行测试验证
  3. PC,Android端会返回一些无关的业务数据,用户体验上增加了流量消耗,也会增加代码维护者的心智负担。(黑客也喜欢接口暴露更多业务数据的接口)

采取这种方案的人认为的优点:

  • 三端只需写要一套代码,这就是代码“复用”。(从表面上看,代码是少写了,但是活干好了吗?,接口的调用者用起来舒不舒服,代码的维护者好不好维护?)

登录这个例子中,即使PC,Android,iOS三端的登录需求一样,我们提供对外的也应该是单独的三个接口。因为它们三端的登录功能不稳定的因素,会随着版本迭代而有各自的要求,这是明显可以预测到的。(Android,iOS估计是一样的需求,但pc和移动端很可能会不一样)