开放封闭原则
为什么需要开放封闭原则
- 代码有修改,就会有出错的风险,所以应尽量避免对原有代码的修改。
- 当需求发生变化的时候,我们只想通过新增文件的方式来实现。这样只需要对新增的文件进行测试,确保代码正确即可。
什么是开放封闭原则
定义:软件实体如类、模块、函数应该对扩展开放,对修改关闭。
开放封闭原则的主要思想是:当系统需求发生改变时,尽量不修改系统原有代码功能(但可以直接删除或标记deprecated),应该扩展模块的功能,来实现新的需求。
如何实现开放封闭原则
首先预测、判断哪些代码是不稳定的代码。然后通过接口,组合的设计将稳定和不稳定的代码区分开。
- 接口:依赖接口,将可能发生变化的代码用接口代替,当需求发生变化时,只需要新增,并注入接口实现即可,这样即实现了功能的扩展,也不会修改原有代码,遵循开闭原则。
- 组合:通过组合多个稳定的代码模块实现一个可能发生变化的功能模块。
- 当功能发生变化时,只需要创建新文件,重新组合功能即可。原有功能模块文件直接删除
- 当新增功能时,直接创建新文件。原有功能模块文件不需要修改
接口的具体实现和组合产生的新文件都属于不稳定的代码。
实例:登录接口设计
遵循开放封闭原则的设计
需求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);
}
}
缺点:
- 登录接口已经成为了一个公共的接口,维护者必须同时了解三端的业务需求才敢修改这快代码,
- 每一次功能修改都需要对Android,iOS,PC进行测试验证
- PC,Android端会返回一些无关的业务数据,用户体验上增加了流量消耗,也会增加代码维护者的心智负担。(黑客也喜欢接口暴露更多业务数据的接口)
采取这种方案的人认为的优点:
- 三端只需写要一套代码,这就是代码“复用”。(从表面上看,代码是少写了,但是活干好了吗?,接口的调用者用起来舒不舒服,代码的维护者好不好维护?)
登录这个例子中,即使PC,Android,iOS三端的登录需求一样,我们提供对外的也应该是单独的三个接口。因为它们三端的登录功能不稳定的因素,会随着版本迭代而有各自的要求,这是明显可以预测到的。(Android,iOS估计是一样的需求,但pc和移动端很可能会不一样)