一、依赖倒置原则 Dependency Inversion Principle (DIP)
首先,什么是依赖呢?如果模块A使用或者调用了模块B,我们称A依赖B。
低层模块:在程序设计中,一些类实现了最基本、基础的操作。我们称呼为低层模块。
高层次模块:一些类实现了复杂的逻辑封装,并且依赖低层次模块。
指导方针:1、高层模块不应该依赖于低层模块,二者应该依赖于抽象类。
2、抽象类不应该依赖于细节,而细节应该依赖于抽象类。
栗子:比如某大公司生产车,本田和福特:那这两个类就是实现了基本功能的低层类了。
/**
* create time on 2019/4/23
* function:本田汽车
*/
public class HondaCar {
private String Tag = "chenhua";
public void Run() {
Log.i(Tag, "本田开始启动了");
}
public void Turn() {
Log.i(Tag, "本田开始转弯了");
}
public void Stop() {
Log.i(Tag, "本田开始停车了");
}
}
/**
* create time on 2019/4/23
* function:福特汽车
*/
public class FordCar {
private String Tag = "chenhua";
public void Run() {
Log.i(Tag, "福特开始启动了");
}
public void Turn() {
Log.i(Tag, "福特开始转弯了");
}
public void Stop() {
Log.i(Tag, "福特开始停车了");
}
}
现在公司希望在这两个车上加一个功能,自动化系统。那么下面这个类就是高层模块,依赖低层模块:FordCar和HondaCar。
/**
* create time on 2019/4/23
* function: 功能类
*/
public class AutoSystem {
public enum CarType {
Ford, Honda
}
private HondaCar hcar = new HondaCar();
private FordCar fcar = new FordCar();
private CarType type;
public AutoSystem(CarType type) {
this.type = type;
}
private void RunCar() {
if (type == CarType.Ford) {
fcar.Run();
} else {
hcar.Run();
}
}
private void TurnCar() {
if (type == CarType.Ford) {
fcar.Turn();
} else {
hcar.Turn();
}
}
private void StopCar() {
if (type == CarType.Ford) {
fcar.Stop();
} else {
hcar.Stop();
}
}
}
可以看到这个高层模块依赖了2个低层模块,这种写法存在一个很严重的问题,当公司有多生产了好几辆车,那么在自动化系统类中,就会增加更多的if/else,以及低层的模块。这非常不方便扩展和维护。即:低层模块发生改变,增加,删除等等,高层模块的修改量巨大,是不是想到产品经理让你改需求了?每次搞完一个就说这个感觉不好,换一个的时候,惊不惊喜,意不意外?
那么如果是依赖倒置原则的写法是什么样的呢?
首先定义个接口类/抽象类,看下面,这个接口抽象出了汽车的公有行为,运行,转弯,停车。
/**
* create time on 2019/4/23
* function: 汽车接口
*/
public interface ICar {
void Run();
void Turn();
void Stop();
}
其次是低层模块类:低层模块实现了ICar接口
/**
* create time on 2019/4/23
* function:本田汽车
*/
public class HondaCar implements ICar {
private String Tag = "chenhua";
@Override
public void Run() {
Log.i(Tag, "本田开始启动了");
}
@Override
public void Turn() {
Log.i(Tag, "本田开始转弯了");
}
@Override
public void Stop() {
Log.i(Tag, "本田开始停车了");
}
}
/**
* create time on 2019/4/23
* function:福特汽车
*/
public class FordCar implements ICar {
private String Tag = "chenhua";
@Override
public void Run() {
Log.i(Tag, "福特开始启动了");
}
@Override
public void Turn() {
Log.i(Tag, "福特开始转弯了");
}
@Override
public void Stop() {
Log.i(Tag, "福特开始停车了");
}
}
和前面的写法区别就是都implement了ICar接口,并实现了接口方法。关键点来了,编写高层模块:
/**
* create time on 2019/4/23
* function: 功能类
*/
public class AutoSystem {
private ICar icar;
public AutoSystem(ICar iCar) {
this.icar = iCar;
}
private void RunCar() {
icar.Run();
}
private void TurnCar() {
icar.Turn();
}
private void StopCar() {
icar.Stop();
}
}
是不是感觉超级清爽,没有if/else,没有依赖任何低层模块!!!试想,当低层模块增加。修改的时候,完全不会影响高层模块吧。不管公司在怎么增加、删除生产的汽车,也不会到影响到autoSystem这个类。并且AutoSystem类也遵循单一职责原则以及里氏替换原则。
总结一下:
High Level Classes(高层模块) --> Abstraction Layer(抽象接口层) --> Low Level Classes(低层模块)
抽象接口是对低层模块的抽象,比如上文的ICar就是对各种基本的car的抽象。
高层模块与低层模块都依赖于接口,比如AutoSystem和各种car都依赖ICar。
二、里氏替换原则 Liskov Substitution Principle (LSP)
所有使用基类的地方,必须能透明的使用其子类。就是使用基类的地方被子类代替了,代码还能跑起来就阔以。比如上面提到的Autosystem类,使用了基类ICar,那么使用Icar的地方都可以用FordCar或者HondaCar替代,运行代码还是可以跑得,ok。那就符合了LSP原则了。无需确保逻辑是否正确!
LSP保证了2点:
1、体现了类的继承原则。如果使用基类的地方无法使用其子类替换,那么就要重新思考该子类和基类的继承关系了。
2、符合LSP设计原则的类,扩展时不会给已有的类造成问题。
小栗子:
//基类baseA,子类ClasB
// BaseA baseA = new BaseA(); 可以替换下面这行
BaseA baseA = new ClassB();
可以看到要创建的是BaseA对象,可以使用ClassB去实例化。
另一方面,在创建和输出方面有一定要求哈。用博主精心设计的两幅图感受一下??
是不是简单明了~
不明白的我们来总结下:
1.子类的输入范围 <= 基类
2.子类的输出范围 >=基类
符合上面的那就算真滴符合了LSP原则了。~
三、接口隔离原则 Interface Segregation Principle (ISP)
客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
说白了就是别什么都堆在一个接口上,我们要的是细节·~~!细节!要懂得拆分接口。拆分可以遵循单一原则哦~
栗子:假设市面上有2种空调,一种是普通空调,最高30度的那种,另一种是更好的空调,可以制暖。
先看普通空调接口,定义了基本功能,温度调节,开关空调
/**
* create time on 2019/4/23
* function: 普通空调基类
*/
public interface IAirCondition {
//调节温度
void changeTemperature();
void openAirCondition();
void closeAirCondition();
}
下面是普通空调的实现类:
/**
* create time on 2019/4/23
* function: 普通空调
*/
public class NormalAirCondition implements IAirCondition {
private String TAG = "chenhua";
@Override
public void changeTemperature() {
Log.i(TAG, "改变温度");
}
@Override
public void openAirCondition() {
Log.i(TAG, "打开空调");
}
@Override
public void closeAirCondition() {
Log.i(TAG, "关闭空调");
}
}
再定义个加温的接口
然后是实现类,注意下面这个更好的空调实现类实现了2个接口!
/**
* create time on 2019/4/23
* function: 更牛逼的空调,能制热啊
*/
public class BetterAirCondition implements IAirCondition, IHeatingAirContion {
private String TAG = "chenhua";
@Override
public void changeTemperature() {
Log.i(TAG, "改变温度");
}
@Override
public void openAirCondition() {
Log.i(TAG, "打开空调");
}
@Override
public void closeAirCondition() {
Log.i(TAG, "关闭空调");
}
@Override
public void heating() {
Log.i(TAG,"升温,暖气!走起~");
}
}
这样做的好处是啥呢?为什么不把加温的方法写在空调的基类中,而要单独哪一个出来,因为不是所有的空调都有加温功能,如果2个接口合为1个,那所有的空调都要实现加温功能。这里有兄弟可能会说了,实现就实现呗~我空方法可以吧~但这位兄台可否想过,如果基类空调有很多子类,后续中对加温的方法进行了修改,参数,返回类型修改等,是不是所有的空调都要改了?首先是工作量的问题,其次这不符合我们另外一个原则,开放封闭原则(简单来说就是对扩展开放,对修改封闭,就是你可以扩展,但是别动我前面的源码!)。所以就细分出接口喽~方便后期的维护和扩展呀。
四、单一职责原则 Single Responsibility Principle (SRP)
如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。
上面啥意思呢?就是一个人只做一件事,如果一个人负责很多事情,可能再处理某件事的时候会影响到其他事情的处理。
小栗子:别什么****事情都给一个人做,看脉脉上有人吐槽,大概是说他就是一个小程序员,老板除了让他写写程序,写写文档,还要负责帮忙接待客户,设计UI,确定需求,。。,打扫卫生,带带孩子,煮几个菜。。。。。啥都让一个人做,是不是会崩溃,那万一奔溃了是不是全部事情都GG了?
就像把bean对象,适配器,各种逻辑什么的,都写在activity里面,后期维护工作量将会很大。
那如何划分职责呢?
罗伯特·C·马丁(Robert C. Martin)是这么定义单一职责的:一个类应该只有一个发生变化的原因。
说的不是辣么清楚明白,我的理解就是,只做一种事情,如果看到接口中有好几种事情的,那就不遵循单一原则了。比如设计程序员接口类,那么程序员接口类需要实现的必定是有写代码和参与项目进度会议这两个方法了。但是写代码和参加会议这两个事情是互相不影响的。是独立的两个职责。可以拆分~看下代码~
/**
* create time on 2019/4/23
* function:程序员接口类
*/
public interface IProgrammer {
//写代码方法。
void writeCode();
}
/**
* create time on 2019/4/23
* function:会议接口
*/
public interface IMeeting {
//参加会议
void atterndMeeting();
}
五、开放封闭原则 Open-Closed Principle (OCP)
开放封闭原则(OCP,Open Closed Principle)是所有面向对象原则的核心。软件设计本身所追求的目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其他的设计原则,很多时候是为实现这一目标服务的,
OCP的思想核心就是2点:
1、当有新的需求的时候,可以在原来的代码上进行扩展。
2、一旦项目完成,原来的代码就不要再去改变了(ps:谁都不想再去改变吧。旧代码一般不是自己写的,要是遇到注释少的,写代码不规范的,看别人代码修改估计会掉不少头发吧。)
那么对于新的需求,就需要在原来的代码不变的情况下,完成对新功能的扩展啦。可以参考前面汽车类代码哦~原来汽车不变的情况下,增加新的汽车或者增加新的功能。(这个汽车类代码好通用哦。(⊙o⊙))就是说,我们可以继承抽象类/实现接口的方式啦。
对于空调栗子也是适用滴,比如现在出了一个新的功能,空调出了能制冷,制热,还能吸收太阳能!那对于能吸收太阳能的功能,我们可以新增加一个接口。
/**
* create time on 2019/4/23
* function: 吸收太阳能哦~
*/
public interface IAbsorbEnergyyAirContion {
//吸收太阳能
void absorbSunEnergy();
}
这样之后,我们可以创建一个类
/**
* create time on 2019/4/23
* function: 太阳能空调
*/
public class SunEnergyAirCondition implements IAirCondition, IHeatingAirContion,IAbsorbEnergyyAirContion {
private String TAG = "chenhua";
@Override
public void changeTemperature() {
Log.i(TAG, "改变温度");
}
@Override
public void openAirCondition() {
Log.i(TAG, "打开空调");
}
@Override
public void closeAirCondition() {
Log.i(TAG, "关闭空调");
}
@Override
public void heating() {
Log.i(TAG,"升温,暖气!走起~");
}
@Override
public void absorbSunEnergy() {
Log.i(TAG,"我吸收了太阳能!");
}
}
可以看到除了实现了基本的空调基类,还实现了制热和太能能基类。其实这里空调基类中调节温度可以再单独出来,这样所有接口都符合单一原则(看看就好,懒得改了,<(* ̄▽ ̄*)/)。但是前面定义的原则是所有空调都可以调节温度,所以算是公有的方法,不会改变。这里也就合在基类接扣中了。(强行解释就是减少接口类!毕竟分解接口也要有个度嘛~嘿嘿嘿)
对于开放封闭的理解总结:
1、封闭误区,常有人说就是不改变原来旧的代码,其实更准确的说应该是:尽量不要去动,尽可能的少动!
2、用抽象构建架构,用实现扩展细节:因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。这就是开放性。
六、迪米特原则 Law of Demeter(LOD)
迪米特原则又叫最少知道原则,就是说一个类对于其他类尽可能的少依赖。如果必须依赖,是通过一个中间类来建立关联。迪米特原则的初衷就是为了降低耦合。
缺点:会创建大量的中间类以及方法(这些方法仅仅是为了间接调用才出现的方法,与业务逻辑无关系。)
迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
栗子~:假设我们有总工厂和子工厂,现在需要打印输全部工厂的员工id信息。
首先来个违反迪米特原则写法:
首先编写工厂总部员工
//工厂总部员工id
public class Worker {
private int workerId;
public int getWorkerId() {
return workerId;
}
public void setWorkerId(int workerId) {
this.workerId = workerId;
}
}
子工厂员工
//子工厂员工id
public class SubWorker {
private int subWorkerId;
public int getSubWorkerId() {
return subWorkerId;
}
public void setSubWorkerId(int subWorkerId) {
this.subWorkerId = subWorkerId;
}
}
工厂总部类:
//工厂总部
public class Factory {
private List<Worker> workers = new ArrayList<>();
public Factory() {
//创建工厂总部员工id
for (int i = 0; i < 20; i++) {
Worker worker = new Worker();
worker.setWorkerId(i);
workers.add(worker);
}
}
public void getAllWorkersId(SubFactory subFactory) {
//此处获取了子工厂的员工列表,这里需要自己手动获取员工列表,
//即总工厂需要知道子工厂的细节。
List<SubWorker> subWorkers = subFactory.getSubWorkers();
//输出子工厂员工id
for (int i = 0; i < subWorkers.size(); i++) {
Log.i("chenhua", subWorkers.get(i).getSubWorkerId() + "");
}
//输出工厂总部的员工id
for (int i = 0; i < workers.size(); i++) {
Log.i("chenhua", workers.get(i).getWorkerId() + "");
}
}
}
子工厂
public class SubFactory {
private List<SubWorker> subWorkers = new ArrayList<>();
public SubFactory(){
//创建子工厂员工id
for (int i = 0; i < 20; i++) {
SubWorker subWorker = new SubWorker();
subWorker.setSubWorkerId(i);
}
}
//返回员工列表
public List<SubWorker> getSubWorkers(){
return subWorkers;
}
}
最后是调用
Factory factory = new Factory();
SubFactory subFactory = new SubFactory();
factory.getAllWorkersId(subFactory);
看到代码的注释应该是比较清晰了,想要获取全部员工信息,调用getAllWorkersId方法传入了子工厂,这里没问题,出现的问题是,在该方法中,即我们在总工厂中还要去获取子工厂的员工列表,然后通过列表获取员工id,根据迪米特原则,减少耦合,这里我们通过总工厂-->子工厂员工列表-->子工厂员工id。总工厂还要自己去了解子工厂内部的细节,才能进一步获取信息,很明显的,这里子工厂员工列表不是我们想要的,多余的耦合。在实际中,要是出现这种状况,总工厂老板想要统计全部员工的id号,然后子工厂汇报了自己的所有员工信息,然后对老板说,你自己找下!哈哈,这子工厂负责人怕是要被炒鱿鱼了吧。那么如何改进呢?
只需要在子工厂类中增加返回子工厂员工的id即可。这样总工厂列表可以直接调用获取id,减少了耦合。
public class SubFactory {
private List<SubWorker> subWorkers = new ArrayList<>();
public SubFactory(){
//创建子工厂员工id
for (int i = 0; i < 20; i++) {
SubWorker subWorker = new SubWorker();
subWorker.setSubWorkerId(i);
}
}
//返回员工列表
public List<SubWorker> getSubWorkers(){
return subWorkers;
}
//返回子工厂员工id
public void getSubWorkersId(){
for (int i = 0; i < subWorkers.size(); i++) {
Log.i("chenhua",subWorkers.get(i).getSubWorkerId()+"");
}
}
}
在总工厂中getAllWorkersId方法中直接获取子工厂员工id
public void getAllWorkersId(SubFactory subFactory) {
//输出子工厂员工id信息.
subFactory.getSubWorkersId();
//输出工厂总部的员工id
for (int i = 0; i < workers.size(); i++) {
Log.i("chenhua", workers.get(i).getWorkerId() + "");
}
}
应用到Android开发,个人常见的举个小栗子:
我们在activity中中代参数传递跳转页面是怎么实现的呢?嘿嘿,你大概知道了吧~
团队开发中,你要跳转的页面可能不是你写的,这是咋办嘞,带参数呢,不至于屁颠屁颠跑过去问同事吧,比如我们某个页面需要跳转到MainActivity页面中。一般在MainActivity中会写一个类似这样的方法:
public static void createMainActivity(Activity activity,String userid){
Intent intent = new Intent(activity,MainActivity.class);
intent.putExtra("userid",userid);
activity.startActivity(intent);
}
这样就极大的方面了我们的页面跳转,无需关系跳转细节,比如intent传递参数的key值,我们不需要知道。简单明了直接一行调用即可。也不用屁颠屁颠跑去问同事需要什么参数,直接看createXXXActivity()方法的参数即可。
MainActivity.createMainActivity(FirstAcivity.this,userid);
简单来说就是两个类A,B之间要实现通信,比如A引用B,那么A应该是不知道B的内部实现逻辑的,并且A在内部最好不要直接调用B。在举个生活小栗子。
去普通餐厅吃饭,作为顾客,你真的知道你吃的饭是哪一位厨师做的吗? 你也没有直接找厨师下单说要吃什么,厨师也不会告诉你这道菜做的细节是什么吧?这里顾客是A,厨师是B,那么A怎么调用B?通过服务员C,A要什么菜告诉服务员C,C会让B做出来给A,这样A,B就没有耦合关系了。当然,迪米特原则的后果就是会出现大量的C。
最后总结:
- 单一职责原则告诉我们实现类要职责单一;
- 里氏替换原则告诉我们不要破坏继承关系;
- 依赖倒置原则告诉我们要面向接口编程;
- 接口隔离原则告诉我们在设计接口的时候要精简单一;
- 迪米特法则告诉我们要降低耦合
- 开闭原则告诉我们要对扩展开发,对修改关闭;
6大原则并不是一定要完全遵循,当然能这样最好了,根据实际情况取舍。如果对你有帮助希望可以给个赞~Thanks♪(・ω・)ノ。