在开发中,为了提高系统的可维护性和可复用性,增加软件的可扩展性和灵活性,要尽量遵循以下7条原则来进行开发。
开闭原则
定义:软件实体应当对扩展开放,对修改关闭。
开闭原则是面向对象程序设计的终极目标。
里氏替换原则
定义1:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。
定义3:继承必须确保超类所拥有的性质在子类中仍然成立。
如何理解?
通俗来讲就是,子类可以扩展父类的功能,但不能改变父类原有的功能,也就是尽量不要重写父类方法。
但是Java的多态又是支持子类重写父类的,这似乎是一种矛盾。看似矛盾,其实不然,个人理解就是建议尽量不要重写可直接实例化的父类的方法,而是基于抽象继承。
里氏替换原则是针对继承而言的,所谓的继承就是为了共享方法,那么共享的父类方法就应该保持不变。当子类通过新增方法来扩展功能而不是继承的时候,调用父类方法的地方可以替换为子类,此时逻辑一致,并不会出现问题。
依赖倒置原则
定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
核心思想:面向接口编程,不要面向实现编程。
如何理解?
在Java中抽象指接口或抽象类,细节就是实现类。实现类之间的依赖关系要通过接口或抽象类,也就是细节应该依赖抽象。
如果在抽象类中的属性是某个实现类,或者在抽象类的方法中调用实现类的方法,那么当实现类发生变动的时候,抽象类也会发生变动,抽象类发生变动的时候,低下的所有实现类也可能发生变动,这有点绕,总结起来就是:当抽象依赖细节的时候,很可能牵一发动全身,这是很危险的,因此抽象不应该依赖细节。
另外依赖是可以传递的,A依赖B,B依赖C,C依赖D,.......,此时如果做到抽象依赖,那么多层依赖也无所畏惧,但实际上应该没有人会写出很多层依赖的代码,这让人感觉很愚蠢。
怎么做?
1、通过构造函数传递依赖对象
2、通过Setter依赖注入
如下代码,Driver类中的 car 属性是抽象的,可以通过构造函数或者set方法注入具体实现类,可以根据不同情况放入法拉利、奔驰等汽车,从而降低了类之间的耦合性。
public interface IDriver {
void driver();
}
public class Driver implements IDriver {
private ICar car;
//
public Driver(ICar car) {
this.car = car;
}
@Orivide
public void driver() {
this.car.run();
}
public void setCar(ICar car) {
this.car = car;
}
}
最佳实践
每个类尽量提供接口或抽象类,或者两者都具备
变量的声明类型尽量是接口或者是抽象类
任何类都不应该从具体类派生
尽量不覆写基类方法
使用继承时尽量遵循里氏替换原则
优点:
降低类之间的耦合性
提高系统的稳定性
降低并行开发引起的风险
提高代码可读性和可维护性
单一职责原则
定义:一个类应该有且仅拥有一个引起它变化的原因,否则类应该被拆分。
核心思想:控制类的粒度大小、将对象解耦、提高其内聚性。
如何理解?
光看定义有点难以理解。假如:类A负责职责1和职责2,当职责1需求变更而变更A,可能造成职责2执行错误,为此不得不改变职责2,此时引起类A变化的原因就有两个了。这显然降低了代码的可维护性。
怎么做?
其实就是在设计类或者接口的时候,类所拥有职责应该清晰,不应该混杂其他相关性不强的职责,因此我们在设计类的时候,要对类的不同职责进行划分和界定,将其封装到不同的类或者模块中。
举个例子:
现在有一些职责需要抽象成类
获取用户基本信息
用户进行登录
用户退出登录
这个时候如何进行类的设计呢?
第一种:
既然都是与用户相关,那么我抽象出一个用户类不就可以了吗,类图如下:
很明显,上图的用户类混杂着与用户类不相关的职责或者相关性不强的职责——登录和退出登录。当改变其中的某一职责时,可能导致其他职责也需要进行改变。
第二种:
既然改变某个职责会影响其他职责,那么就将相关性相同的抽离出来,将类进行拆分。如下,当用户信息类发送改变,并不会导致用户行为类发生改变。
单一职责适用于类或接口,也适用于方法,也就是说一个方法只做一件事情,比如获取用户头像,不要把这个方法放到“获取用户基本信息”中去,否则这个方法颗粒度很粗,我们应该将其进行拆分。
优点:
降低类复杂度
提高类可读性
提高系统可维护性
降低变更引起的风险
接口隔离原则
定义1:客户端不应该被迫依赖于它不使用的方法。
定义2:一个类对另一个的依赖应该建立在最小的接口上。
和单一职责原则的区别?
单一职责原则强调的是,类和接口的职责要单一,也就是说一个类最好只负责一类事情。
而接口隔离原则强调的是对接口依赖的隔离,接口要尽量小。
如何做?
一个接口只服务于一个子模块或业务逻辑
只提供调用者需要的方法,屏蔽不需要的方法
根据业务进行最好的接口设计
迪米特法则
定义:只与直接朋友交谈,不跟陌生人说话
如何理解?
什么是直接朋友?两个对象之间存在耦合关系,如:组合、聚合、依赖等
如果两个类无须直接通信,那么就不应该发生互相调用,应当通过第三方转发调用。例如:买房时,买家无须与卖家交谈,而是通过中介进行通信。
怎么做?
在类的划分上,尽量创建弱耦合的类
在类的结构设计上,尽量降低类成员的访问权限
在类的设计上,优先考虑将一个类设置成不变类
在对其他类的引用上,将引用其他对象的次数降低到最低
不暴露类的属性成员,而应该提供相应的get/set方法
合成复用原则
定义:在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
总结
开闭原则:对扩展开放,对修改关闭
里氏替换原则:继承必须确保超类所拥有的性质在子类中仍然成立
依赖倒置原则:面向接口编程,不要面向实现编程
单一职责原则:控制类的粒度,类的职责要单一
接口隔离原则:设计接口要精简单一
迪米特法则:只与直接朋友交谈,不跟陌生人说话
合成复用原则:优先使用组合或聚合关系,其次才是继承