指一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
。强调的是用抽象构建框架,用实现扩展细节
,可以提高软件系统的可复用性及可维护性。开闭原则是面向对象设计中最基础的设计原则。它指导我们如何建立稳定灵活的系统,例如版本更新,我们尽可能不修改源码,但是可以增加新功能。
1、课程接口ICourse
2、他有一个Java架构课程的类JavaCourse
3、现在要给Java架构课程做活动,价格优惠。
如果修改JavaCourse中的getPrice()方法,则会存在一定风险,可能影响其他地方的调用结果。
如何在不修改原有代码的前提下,实现价格优惠这个功能呢?
4、我们再写一个处理优惠逻辑的JavaDiscountCourse类,而不是直接修改JavaCourse类
ublic interface ICourse {
Double getPrice();
}
public class JavaCourse implements ICourse {
public Double price;
public JavaCourse(Double price) {
this.price = price;
}
@Override
public Double getPrice() {
return price;
}
}
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Double price) {
super(price);
}
public Double getDiscountPrice(Double discountRate) {
return price * discountRate;
}
public Double getOriginPrice() {
return price;
}
}
指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象
。抽象不应该依赖细节,细节应该依赖抽象
。通过依赖倒置,可以降低类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并降低修改程序带来的风险
public class Tom {
public void studyEnglish(){}
public void studyChinese(){}
}
public class MainBusiness {
public static void main(String[] args) {
Tom tom = new Tom();
tom.studyEnglish();
tom.studyChinese();
}
}
Tom再追加学习画画的话。
这个时候,需要业务扩展,代码要从底层到高层(调用层)一次修改代码。
在Tom类中增加studyDraw()的方法,在高层也要追加调用。
如此一来,在系统发布以后,实际上是非常不稳定的,在修改代码的同时会带来意想不到的风险。
public interface ICourse {
void study();
}
public class ChineseCourse implements ICourse {
@Override
public void study() {
}
}
public class EnglishCourse implements ICourse {
@Override
public void study() {
}
}
public class Daming {
void study(ICourse course){
course.study();
}
}
public class MainBusiness {
public static void main(String[] args) {
Daming daming = new Daming();
daming.study(new ChineseCourse());
daming.study(new EnglishCourse());
}
}
如上涉及到了代码注入,目前已知有3种
1、作依赖注入
Daming daming = new Daming();
daming.study(new ChineseCourse());
2、构造器注入
Daming daming = new Daming(new ChineseCourse());
3、Setter注入方式(如果lingLing的ICourse是全局单例,则只能选择Setter注入方式)
public class LingLing {
ICourse course;
public void setCourse(ICourse course) {
this.course = course;
}
public void study() {
course.study();
}
}
以抽象为基准比以细节为基准搭建起来的架构要稳定得多,因此大家在拿到需求后,要面向接口编程,按照先顶层再细节
的顺序设计代码结构。
指不要存在一个以上导致类变更的原因
。假设有一个Class负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能会导致另一个职责的功能发生故障。这样一来,这个Class就存在两个导致类变更的原因。
如何解决这个问题呢?
我们就要分别用两个Class来实现两个职责,进行解耦。后期需求变更维护互不影响。这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说就是一个Class、Interface、Method只负责一项职责。
在实际项目中,代码会存在依赖、组合、聚合关系,在项目开发过程中还受到项目的规模、周期、技术人员水平、对进度把控的影响,导致很多类都不能满足单一职责原则。但是,我们在编写代码的过程中,尽可能地让接口和方法保持单一职责,对项目后期的维护是有很大帮助的。
指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
。
这个原则指导我们在设计接口时,应当注意以下几点。
(1)一个类对另一个类的依赖应该建立在最小接口上。
(2)建立单一接口,不要建立庞大臃肿的接口。
(3)尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度)。
接口隔离原则符合“高聚合、低耦合”的设计思想,使得类具有很好的可读性、可扩展性和可维护性。在设计接口的时候,要多花时间思考,要考虑业务模型,包括还要对以后可能发生变更的地方做一些预判。所以,在实际开发中,我们对抽象、业务模型的理解是非常重要的。
public interface IAnimal {
void fly();
void swim();
void run();
}
大多数时候鸟类的swim()方法可能只能空着,
陆地动物的fly()方法可能只能空着,代表接口就有些臃肿了
这时候,我们针对不同动物的行为来设计不同的接口
public interface IFlyAnimal {
void fly();
}
public interface IRunAnimal {
void run();
}
public interface ISwimAnimal {
void swim();
}
指一个对象应该对其他对象保持最少的了解,尽量降低类与类之间的耦合
。迪米特法则主要强调只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入和输出参数中的类都可以被称为成员朋友类,而出现在方法体内部的类不属于朋友类。
public class Task {}
public class Employee {
void checkTask(List<Task> tasks){}
}
public class Leader {
public void commandCheck(Employee employee){
List<Task> taskList = new ArrayList<>();
employee.checkTask(taskList);
}
}
public class MainBusiness {
public static void main(String[] args) {
Leader leader = new Leader();
Employee employee = getTaskList();
leader.commandCheck(employee);
}
}
代码实际意义:领导吩咐员工取检查任务
根据迪米特法则,TeamLeader和Course并不是朋友。
Employee统计需要引用Course对象,Leader只想要结果,不需要跟Course产生直接交流。
PS:领导让你统计报表你让老板去替你收集数据也说不过去
public class Employee {
void checkTask1(){
List<Task> taskList = getTaskList();
// 校验逻辑
}
}
public class Leader {
public void commandCheck1(Employee employee){
employee.checkTask1();
}
}
public class MainBusiness {
public static void main(String[] args) {
Leader leader = new Leader();
Employee employee1 = new Employee();
leader.commandCheck1(employee1);
}
}
领导发话就可以了,不需要做任何员工的任务量
指如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有对象O1都替换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的
子类型
子类对象能够替换父类对象,而程序逻辑不变。说明,子类可以扩展父类的功能,但不能改变父类原有的功能
。根据这个理解,我们对里氏替换原则的定义总结如下。
(1)子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
(2)子类中可以增加自己特有的方法。
(3)当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松。
(4)当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的方法更严格或相等。
public class JavaCourse implements ICourse {
public Double price;
public JavaCourse(Double price) {
this.price = price;
}
@Override
public Double getPrice() {
return price;
}
}
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Double price) {
super(price);
}
public Double getDiscountPrice(Double discountRate) {
return price * discountRate;
}
public Double getOriginPrice() {
return price;
}
}
getOriginPrice() 就是典型的违反了里氏替换原则。我们不应该覆盖getPrice()方法
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Double price) {
super(price);
}
public Double getDiscountPrice(Double discountRate) {
return price * discountRate;
}
public Double getPrice() {
return price;
}
}
使用里氏替换原则有以下优点。
(1)约束继承泛滥
,是开闭原则
的一种体现。
(2)加强程序的健壮性
,同时变更时可以做到非常好的兼容性
,提高程序的维护性、可扩展性,降低需求变更时引入的风险。
指尽量使用对象组合(has-a)或对象聚合(contanis-a)的方式实现代码复用,而不是用继承关系达到代码复用的目的
。合成复用原则可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较小。
继承,又被称为白箱复用,相当于把所有实现细节暴露给子类。
组合/聚合又被称为黑箱复用,对类以外的对象是无法获取实现细节的。
我们要根据具体的业务场景来做代码设计,其实也都需要遵循面向对象编程(Object Oriented Programming,OOP)模型。
-----------------------------------------------------------------------------摘自《设计模式就该这样学:基于经典框架源码和真实业务场景》(谭勇德)