单一职责
对于一个类来说,应该只有一个原因引起它的变化,也就是他所承担的职责应该单一化,不应该承担不同的责任。一个类承担的职责越多说明耦合性越高,越不容易被复用。比如我们常用的用户管理类,里面包含了登录,登出等行为。而在登录登出行为中,一个要将用户信息写入到缓存,一个要从缓存中清楚用户信息。
那么根据单一职责,显然这里写入以及读取缓存具体的方法实现是不该属于此类的,UserManager类只关心登录登出即可,缓存要交给另外的类实现。代码类似如下:
public class UserManager {
/**
* 登录
*/
void login(){
HttpUtil.login(new Callback(){
@Override
void onSuccess(User user){
//登录完成将user写入缓存
UserCache.saveUser(user);
}
});
}
/**
* 登出
*/
void logout(){
HttpUtil.logout(new Callback(){
@Override
void onSuccess(){
//登出完成将user从缓存清除
UserCache.clearUser();
}
});
}
}
public class UserCache {
static void saveUser(User user){
Cache.getInstance().saveUser(user);
}
static void clearUser(){
Cache.getInstance().clearUser();
}
}
其中saveUser和clearUser被拿到单独的缓存类实现,而不是直接实现在UserManager里,单一职责的目的就是使其减少耦合,更容易维护。
开闭原则
一个模块或类应该对拓展开放,对修改关闭。就是说不应该去改变一个现有的类,而可以去给这个类进行拓展增加功能。举个例子,一个手机生产的时候是要有手机屏幕的,如果你想要加个手机壳自己加就行了,但是不应该把我的屏幕给拆了,而在代码上就要用到继承或接口来实现。
里氏替换原则
凡是父类出现的地方,子类都应该能替换父类,而不会出现错误和异常,也就是保证子类的行为和属性要与父类一致,可以对父类进行拓展补充。听上去其实也是对开闭原则的延伸。假设在用户缓存中,现有的UserCache是将用户信息缓存到SD卡,那么现在如果需求变更为除了缓存到SD卡外还要缓存到sp中一份。那么这个时候只要集成UserCache重写相关方法即可,而不是直接去修改UserCache,而在调用端直接替换UserCahe的初始化(其实应该将缓存抽象成接口,这个后面会说到)。
// 调用端
UserCache userCache = new UserCache();
public class UserCache {
void saveUser(User user){
Cache.getInstance().saveUser(user);
}
void clearUser(){
Cache.getInstance().clearUser();
}
}
修改为:
// 调用端
SpAndSdCache userCache = new SpAndSdCache();
public class SpAndSdCache extends UserCache{
@Override
void saveUser(User user) {
super.saveUser(user);
// 加入sp缓存
}
@Override
void clearUser() {
super.clearUser();
// 清除缓存
}
}
依赖倒置原则
高层模块不应该依赖于底层模块,而都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。所谓的“要面向接口编程,不要面向实现编程”。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
比如上面的UserManager用户管理类中,直接获取UserManager实例调用login和logout方法,这?我们的?层模块,即登录界?也是?个细节实现类,该登录功能依赖的是UserManager这个实体类,即依赖了细节没有依赖抽象。?旦UserManager更改,或者我们实?其他?户管理类,那么这?登录?法都要全部进?改动。对此,我们可以将对UserManager 的依赖改为依赖抽象:
public class UserManager implements IUserManager{
/**
* 登录
*/
@Override
public void login(){
HttpUtil.login();
}
/**
* 登出
*/
@Override
public void logout(){
HttpUtil.logout();
}
}
调用方:
IUserManager userManager = UserManager.getInstance();
userManager.login();
依赖倒置原则的实现方法:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
接口隔离原则
一个模块对另一个模块的依赖应该建立在最小的接口上,它不应该依赖不需要的接口,不同的功能要抽象成不同的接口。当一个接口过于庞大时就要考虑是否要拆分成不同模块的接口,其实这也是单一职责的补充。比如客户端可以使用sp键值对缓存和SD卡缓存,可能会这样设计接口:
public interface ICacheManager {
// SD卡缓存
void sdCache();
// sp缓存
void spCache();
}
//调用方
public class UserManager implements ICacheManager{
@Override
public void sdCache() {
}
@Override
public void spCache() {
}
}
这里就存在缺陷,如果一个调用方是需要sp缓存或者SD卡缓存的话是不需要去实现另一个接口的。为了符合接隔离原则可以这样拆分为两个接口,调用方需要哪个方法实现对应接口即可:
// SD卡缓存
public interface ISdCacheManager {
void sdCache();
}
// sp缓存
public interface ISpCacheManager {
void spCache();
}
// 调用方
public class UserManager implements ISdCacheManager,ISpCacheManager{
@Override
public void sdCache() {
}
@Override
public void spCache() {
}
接口隔离原则的实现方法
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
迪米特法则
也称最小知法则,一个模块应该减少对其他模块的依赖,减少耦合。比如我们设计了用户管理类UserManager用来管理登录,登出等操作拱调用端调用,但是具体的登录登出中的写入,清空用户缓存功能调用端是不了解的。
总结
其实所有的设计原则及设计模式都是为了使系统能够更加易于维护,拓展。减少耦合,提高开发效率,使代码易读性更高。我们在编程时,要时刻以这些原则约束自己,养成良好的编码习惯。
关于设计模式的相关视频讲解可移步https://www.bilibili.com/video/BV1WL411s7zj/
作者:单总不会亏待你 链接:https://juejin.cn/post/7013357097635020808
|