定义:单一职责的英文全称是Single Responsibility Principle,简称SPR。
英文解释是:There should never be more than one reason for a class to change.
翻译过来就是,一个类只能有且仅仅有一个原因导致类的变更。
我们用一个例子说明下:
需求场景:设计一个手机,手机包含功能为打电话,挂电话,播放音乐功能。
public interface Imobile { //打电话 public void call(String number); //播放音乐 public void playMusic(Object o); //挂断电话 public void hangup(); } 复制代码
上面设计了一个 Imobile
的接口,声明了打电话,挂断,播放音乐的方法,我们初步看,觉得这么设计没什么问题,但是如果我们考虑单一职责的话,这个设计就有问题了,其实单一职责最难划分的就是职责,我们针对这个场景可以给这个电话分为两个职责,打电话和挂电话是属于协议管理的,播放音乐其实属于附属功能管理,所以这里的职责就划分了两个:1.协议管理;2.附属功能管理。那么单一职责的定义就是:一个类只能有且仅仅有一个原因导致类的变更。而上面这个接口中划分了两个职责,而且,协议的变动,附属功能的变动,都会导致接口和类改变,所以,这个接口就是不符合单一职责的。那么如何让其满足单一职责原则呢?我们需要拆分接口,因为协议管理和附属功能管理两个彼此并不互相影响,所以我们可以直接拆分为两个接口,如下:
//协议管理接口 public interface IMobileManager { //打电话 public void call(String number); //挂断电话 public void hangup(); } //附属功能接口 public interface Ifunction { public void playMusic(Object o); } 复制代码
这个时候很多人可能不理解,你这么做的好处是什么呢?我感觉不到这么做的好处啊。这里做一个假设,假设这个时候新增了一部高级手机,它可以保持会话,这个时候协议管理接口需要修改了,需要新增一个保持会话的功能,这个时候实现类也要跟着改变,如果采用第一种设计,那么所有的电话都要修改。如果有一个玩具手机,它并不会通话,这个时候也要修改这个实现类,这个设计就糟糕了。如果采用了单一职责,玩具手机并不会实现协议管理的接口,只会实现附属功能接口,所以协议管理的修改并不会导致玩具手机也要修改。
其实单一职责并不只要求接口,方法也是,我们写一个方法要能清晰的定义这个方法的职责,比如修改用户信息最好就要写多个方法来实现,不要就只写一个方法。类似于这样:
public interface IUserSerivice { void updateUserInfo(User user); } 复制代码
这种设计不清晰,我们应该针对每一个修改都有一个方法,类似于这样:
public interface IUserSerivice { void updateUserName(String name,String id); void updateUserTelPhone(String phone,String id); void updateUserHomeAddr(String adrr,String id); } 复制代码
这样写虽然很啰嗦,但是职责很清晰,后续代码也好维护,直接就能知道更新了什么信息。
定义:里氏替换原则的英文全称:Liskov Substitution Principle ,简称LSP。
英文解释:Functions that user pointer or references to base classes must be able to use objects of derived classes without knowing it.
翻译:所有引用基类的地方都必须能透明的使用其子类对象。
其实理解这句话很简单,无非就是父类执行的方法,替换成子类也可以正确执行并且达到一样的效果。我们先写一个没有按照里氏替换原则的代码。
public class Father { public void doSomeThing(Map map){ System.out.println("父类执行啦!"); } } public class Son extends Father{ public void doSomeThing(HashMap map) { System.out.println("子类执行了!"); } } public class Client1 { public static void main(String[] args) { HashMap map=new HashMap(); Father father=new Father(); father.doSomeThing(map); } } public class Client2 { public static void main(String[] args) { HashMap map=new HashMap(); Son son=new Son(); son.doSomeThing(map); } } 复制代码
我们执行客户端main方法,发现结果输出为:“父类执行啦!”,我们采用子类替换父类执行 doSomeThing()
方法,发现输出结果是:“子类执行了!”,这和父类执行的结果不一致,不符合里氏替换原则,这里为什么没有执行父类的方法呢?这里因为是子类重载了父类的方法,客户端调用的参数是HashMap,所以匹配到了子类的方法。那么我们如何修改就能满足里氏替换原则呢?其实很简单,两种方式。
第一个好理解,那第二个怎么理解呢?我们还是用上面那个例子改动下,代码如下:
public class Father { public void doSomeThing(HashMap map){ System.out.println("父类执行啦!"); } } public class Son extends Father{ public void doSomeThing(Map map) { System.out.println("子类执行了!"); } } 复制代码
这里其实就只把子类的参数类型改成了Map,父类的参数类型改成了HashMap, 这样客户端声明的参数类型是HashMap,所以调用 son.doSomeThing(map)只会执行父类的方法。
这里其实可以总结一句:里氏替换原则就是要求, 不要重写父类的非抽象方法,尽量不要重载父类的方法,如果要重载,需要注意方法的前置条件(形参),如果要保持子类的个性化,可以采用新增方法的方式。
定义:依赖倒置英文全称为:Dependence Inversion Princiole,简称DIP。
英文解释:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。
官方翻译:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
依赖倒置,我们用通俗的解释就是,平常我们生活中的依赖都是依赖具体细节,比如我要用手机就是具体的某个手机,用电脑就是用具体的某台电脑,这个依赖倒置就是和我们生活是反的,故称为倒置,所以依赖倒置就是依赖抽象(接口或者抽象类)。我们同样用一个例子来说明下:
我们实现一个司机开车的例子,我们可以抽象出2个接口,一个是司机接口,一个是汽车接口。
public interface ICar { //开汽车方法 public void run(); } public interface IDriver { //开车 public void driver(ICar car); } //汽车实现类,宝马车 public class BmwCar implements ICar { @Override public void run() { System.out.println("宝马车开动啦"); } } //司机实现类,C1驾照司机 public class COneDriver implements IDriver { @Override public void driver(ICar car) { System.out.println("我是C1驾照司机"); car.run(); } } // 客户端场景类 public class Client { public static void main(String[] args) { ICar bmw=new BmwCar(); IDriver cOneDriver=new COneDriver(); cOneDriver.driver(bmw); } } 复制代码
这里实现了C1驾照司机开宝马车的场景,这就是依赖倒置原则的写法,那如果我不采用依赖倒置会发生什么情况呢?不依赖倒置也就是说要依赖细节,以上场景就会出现C1驾照车司机只能开宝马车的情况,这显然是有问题的。
根据上面的例子以及我们的分析,我们可以总结出依赖倒置的几个规则:
定义:迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP)
迪米特法则通俗的解释就是,一个类要对其所耦合的类了解的尽量少,不管耦合的类内部多么复杂,都只管其暴露的public方法。迪米特法则另外一种说法是,只和朋友类交流。朋友类的定义:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。我们先看一个违法迪米特法则的例子。
场景:我们吃饭要经过客户点菜,服务员下单,厨师做菜这三个流程,我们来用代码设计这个场景。
//厨师接口 public interface ICooker { //根据订单做菜 public void cooke(List<Order> orders); } //服务员接口 public interface IWaiter { //下单 public void doOrder(List<String> dishNames); } // 订单实体类 public class Order { private List<String> dishNames; public Order(List<String> dishNames) { this.dishNames = dishNames; } public List<String> getDishNames() { return dishNames; } public void setDishNames(List<String> dishNames) { this.dishNames = dishNames; } } // 服务员实现类 public class ChineseWaiter implements IWaiter { private ICooker cooker; public ChineseWaiter(ICooker cooker) { this.cooker = cooker; } @Override public void doOrder(List<String> dishNames) { List<Order> cookOrders=new ArrayList<>(); cookOrders.add(new Order(dishNames)); cooker.cooke(cookOrders); } } //厨师实现类 public class ChineseCooker implements ICooker { @Override public void cooke(List<Order> orders) { for (int i = 0; i < orders.size(); i++) { Order order=orders.get(i); List<String> dishNames=order.getDishNames(); for (int j = 0; j < dishNames.size(); j++) { System.out.println("我是中餐厨师,我做:"+dishNames.get(j)); } } } } //场景类 public class Client { public static void main(String[] args) { IWaiter waiter=new ChineseWaiter(new ChineseCooker()); List<String> dishNames=new ArrayList<>(); dishNames.add("红烧鱼块"); dishNames.add("宫保鸡丁"); waiter.doOrder(dishNames); } } 复制代码
我们自己思考下,其实上述代码中,违法迪米特法则地方就是服务员的实现类,我们发现,服务员实现类ChineseWaiter在实现类中,和非朋友类产生了依赖,这个依赖就是Order类,我们再回顾下朋友类的定义: 出现在成员变量、方法的输入输出参数中的类称为成员朋友类 ,Order类并不满足这个定义,所以它违反了迪米特法则。那么我们如何修改满足迪米特法则呢?我们只要修改服务员实现类和场景类即可,修改后的代码如下:
public interface IWaiter { //下单 public void doOrder(List<Order> orders); } public class ChineseWaiter implements IWaiter { private ICooker cooker; public ChineseWaiter(ICooker cooker) { this.cooker = cooker; } @Override public void doOrder(List<Order> orders) { cooker.cooke(orders); } } public class Client { public static void main(String[] args) { IWaiter waiter=new ChineseWaiter(new ChineseCooker()); List<String> dishNames=new ArrayList<>(); dishNames.add("红烧鱼块"); dishNames.add("宫保鸡丁"); List<Order> orders =new ArrayList<>(); orders.add(new Order(dishNames)); waiter.doOrder(orders); } } 复制代码
这里把订单的封装丢给了场景类中,服务员只依赖他的朋友类厨师类就可以了。那么这个迪米特法则有什么作用呢?其实迪米特法则最主要的作用就是降低耦合,从而使得类的复用率得以提高。但是采用迪米特法则后就会导致产生了过多的中间类和跳转类,导致系统的复杂性提高,所以我们在使用该法则的时候要权衡利弊,还是那句话,没有最完美的设计,只有最合适的设计。
英文解释:Clients should not be forced to depend upon interfaces that they don't use.The dependency of one class to another one should depend on the smallest possible interface.
官方翻译:客户端不应该依赖它不需要的接口。类间的依赖关系应该建立在最小的接口上。
接口隔离原则,其实可以理解为接口设计的粒度要尽量小,接口中的方法要尽量少。这里其实和单一职责很相识,但是有区别,单一职责是职责的划分要求,每个接口只要表述对应的职责就可以了。但是接口隔离一般是对应于某个模块调用,可能只用到某个接口的部分方法,可以更细分。举例说明:
还是以单一职责的例子,设计手机。之前的代码是分为了一个功能接口,一个协议管理接口。代码见单一职责部分。我们看看如果是用接口隔离还可以怎么设计。我们其实还可以对功能接口可以划分更细的粒度,比如最新的iPhone手机有faceId功能,三星手机有虹膜功能。那这个时候,我还是用一个功能接口,就会导致接口非常冗余,一个接口有faceid,虹膜,但是实际上有些手机并没有这些功能,那么我们就可以对功能接口进行拆分。拆分成这样:
public interface ISamFunction { //虹膜功能 public void iris(); } public interface IAppleFnction { //faceId 功能 public void faceId(); } 复制代码
然后如果有手机既有虹膜又有faceId功能,直接实现两个接口就可以了。这样就满足了接口隔离原则。
英文解释:Software entities like classes,modules and functions should be open for extension but closed for modifications
官方翻译:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
开闭原则,其实是一个总的原则,前面五种原则其实都是开闭原则的具体实现,它并没有一个具体的设计思路,只是要求我们对设计的类,方法等对扩展开放,对修改关闭。掌握了前面五种设计原则,其实也就掌握了开闭原则了,这里就不举例说明了。