概述
文章目录
- 一、开闭原则(Open Closed Principle,OCP)
- 二、依赖倒置原则(Dependency Inversion Principle,DIP)
- 三、单一职责原则(Single Responsibility Principle,SRP)
- 四、接口隔离原则(Interface Segregation Principle,ISP)
- 五、迪米特原则(Law of Demeter,LOD)
- 六、里氏替换原则(Liskov Substitution Principle,LSP)
- 七、组合/聚合复用原则 (Composite/Aggregate Reuse Principle,CRP)
一、开闭原则(Open Closed Principle,OCP)
如何理解“对扩展开放、对修改关闭”?
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,有两点要注意:
- 第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
- 第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
如何做到“对扩展开放、修改关闭”?
要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
如何在项目中灵活应用开闭原则?
如果你开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。
如果你开发的是跟业务无关的、通用的、偏底层的系统,比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。
不过,有一句话说得好“唯一不变的只有变化本身”。最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
二、依赖倒置原则(Dependency Inversion Principle,DIP)
控制反转(Inversion Of Control,IOC)
实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制,流程的控制权从程序员“反转”给了框架。
依赖注入(Dependency Injection,DI)
依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。
依赖注入框架(DI Framework)
我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
依赖倒置原则(Dependency Inversion Principle,DIP)
原则规定,高层模块(调用者)不要依赖低层模块(被调用者),高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
三、单一职责原则(Single Responsibility Principle,SRP)
如何理解单一职责原则(SRP)?
一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
如何判断类的职责是否足够单一?
不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:
- 类中的代码行数、函数或者属性过多;
- 类依赖的其他类过多,或者依赖类的其他类过多;
- 私有方法过多;
- 比较难给类起一个合适的名字;
- 类中大量的方法都是集中操作类中的某几个属性。
类的职责是否设计得越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
四、接口隔离原则(Interface Segregation Principle,ISP)
如何理解“接口隔离原则”?
理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。
- 如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
- 如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
- 如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者依赖不需要的接口函数。
接口隔离原则与单一职责原则的区别
单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
五、迪米特原则(Law of Demeter,LOD)
如何理解“高内聚、松耦合”?
“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。
所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
如何理解“迪米特法则”?
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。
六、里氏替换原则(Liskov Substitution Principle,LSP)
如何理解“里式替换原则”?
这条原则规定,子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
它是用来指导继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
里氏替换原则和多态的区别?
虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法,它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。
哪些代码明显违背了里氏替换原则?
- 子类违背父类声明要实现的功能
父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。
- 子类违背父类对输入、输出、异常的约定
在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。那子类的设计就违背里式替换原则。
- 子类违背父类注释中所罗列的任何特殊说明
父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。
七、组合/聚合复用原则 (Composite/Aggregate Reuse Principle,CRP)
- 描述
- 定义:
尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的
- 聚合(has-A)和组合(contains-A)
- 优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
- 组合/聚合缺点:通过组合/聚合建造的系统会有较多的对象需要管理,比如 A 类里面有 B 类也有 C 类,也就是说管理的对象较多,而继承复用的优点是扩展性比较容易实现,因为继承父类,父类的所有功能通过继承都会自动进入子类,修改和扩展相对容易些
- 继承的缺点:会破坏包装,因为继承会将父类的实现细节暴露给子类
- 何时使用组合/聚合、继承:
聚合是 has-A,组合是 contains-A,而继承是 is-A
- 举例
- 场景:获取数据库连接
- 反例:
public class DBConnection {
public String getConnection() {
return "MySQL数据库连接";
}
}
public class ProductDao extends DBConnection {
public void addProduct() {
String conn = super.getConnection();
System.out.println("使用" + conn + "增加产品");
}
}
public class Test {
public static void main(String[] args) {
ProductDao productDao = new ProductDao();
productDao.addProduct();
}
}
- 说明:代码很简单,Dao层获取父类Mysql连接进行操作,现在要增加第二个数据库连接,继承方式就不好实现了,所以可以改成组合方式
- 正例:
public abstract class DBConnection {
public abstract String getConnection();
}
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL数据库连接";
}
}
/**
* PostgreSQL数据库
*/
public class PostgreSQLConnection extends DBConnection {
@Override
public String getConnection() {
return "PostgreSQL数据库连接";
}
}
/**
* 使用组合的方式
*/
public class ProductDao{
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct(){
String conn = dbConnection.getConnection();
System.out.println("使用"+conn+"增加产品");
}
}
public class Test {
public static void main(String[] args) {
ProductDao productDao = new ProductDao();
productDao.setDbConnection(new PostgreSQLConnection());
productDao.addProduct();
}
}
- UML类图:
其实在实际开发过程中,讲究的是一个平衡,不应该只为了程序的扩展性而一味的去遵循这些设计原则,还要考虑人力、时间、成本、质量、Deadline,如果一开始就把扩展性做的特别完美的话,成本又上来了,所以不应该过度,在适当的场景去遵循设计原则,体现的是一个取舍的问题,就像有些设计模式可能遵循两样三样而破坏一样两样,最重要的是合适的业务场景,所以设计原则不是强行遵守的,而是要讲究一个度,讲究一个取舍。
死记硬背小技巧(真是个小机灵鬼):
- 开(开闭原则)
- 一(依赖倒置原则)
- 单(单一职责原则)
- 间(接口隔离原则)
- 最(最少知道原则,也称迪米特原则)
- 适(里氏替换原则)
- 合(组合/聚合复用原则)
最后
以上就是搞怪皮皮虾为你收集整理的【设计模式】软件设计七大原则一、开闭原则(Open Closed Principle,OCP)二、依赖倒置原则(Dependency Inversion Principle,DIP)三、单一职责原则(Single Responsibility Principle,SRP)四、接口隔离原则(Interface Segregation Principle,ISP)五、迪米特原则(Law of Demeter,LOD)六、里氏替换原则(Liskov Substitution Principle,LSP的全部内容,希望文章能够帮你解决【设计模式】软件设计七大原则一、开闭原则(Open Closed Principle,OCP)二、依赖倒置原则(Dependency Inversion Principle,DIP)三、单一职责原则(Single Responsibility Principle,SRP)四、接口隔离原则(Interface Segregation Principle,ISP)五、迪米特原则(Law of Demeter,LOD)六、里氏替换原则(Liskov Substitution Principle,LSP所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复