共计 3804 个字符,预计需要花费 10 分钟才能阅读完成。
软件设计六大原则
网上有很多朋友都介绍过这六个原则,下面我简单说说我对这六个原则的一些理解。
如上图所示,这就是软件设计的六大原则。
其中我认为最重要的就是 开闭原则,可以理解为这是最终目的。其余的五种原则都是为了达到这个目的而存在的。
要理解开闭原则,则需要先看看另外五个原则。
单一职责(Single Responsibility Principle)
单一职责原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。
个人理解
这是单一职责的概念,我对这个原则的理解是,一个类最好只有一种职责,返回来说就是,把相同的职责放到一个类中。这样就实现了高内聚。
职责可以从各种维度去切割,比如以中间件为维度拆分类,或者以业务规则为维度拆分类等等。
想要将职责才分的恰到好处,需要对业务领域的深入理解,和对软件设计的经验。
例子
举个例子,用户模块的业务需要 MySql
和Redis
这两个中间件来支持。
如果我创建一个 UserRepository
,里面既有访问MySql
存储数据的方法,又有访问 Redis
存储数据的方法,那么可以理解为这个类有两个职责,访问 MySql
和Redis
。
那么可以创建两个类:UserRepository
封装访问 MySql
的方法,UserRedisRepository
封装访问 Redis 的方法,这种类的职责就单一了。
再举个例子,天气模块中的类 WeatherService
有一个方法getWeather()
,该方法会发起 HTTP 请求访问第三方资源接口获取天气数据(返回 JSON 格式),随后将 JSON 数据转换成WeatherEntity
。
这里最好在进行一次拆分,可以把请求第三方接口当作一种职责,提供一个 WeatherProxy
类,然后将数据转换作为一种职责,提供一个 WeatherConverter
类。
好处
降低类的复杂度、提高可读性、提高可维护性、提高扩展性、降低了变更引起的风险。
里氏替换(Liskov Substitution Principle)
如果对每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 S 是类型 T 的子类型。
所有引用基类的地方必须能透明地使用其子类的对象,通俗的说,就是派生类(子类)对象可以在程序中代替其基类(父类)对象。
个人理解
通俗的理解是,在使用父类的地方可以任意使用其子类,但是在使用子类的地方不一定能使用父类。
例子
最简单的一个例子,在 Java 中,大家都有使用过List<T>
,它的实现最初可以是ArrayList
,随后也可以修改成LinkedList
。
在做修改时,List<T>
的使用并不会被影响。
List<T> list = new ArrayList<>();
list.add(...);
list.forEach(t -> {...});
上面代码中,可随意切换 List<T>
的实现类,对后续使用的代码不会造成任何影响。
List<T> list = new LinkedList<>(); // 修改实现类不会对下面的代码造成影响
list.add(...);
list.forEach(t -> {...});
上面的例子中的 list.forEach
就实现了里氏替换原则,该方法接收任何 List
对象,此处传递 ArrayList
或LinkedList
都是可以的,做到了 子类可以在程序中代替其父类。
好处
增强程序的健壮性,即使切换了子类,不会对使用代码造成影响。
依赖倒置原则(Dependence Inversion Principle)
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
个人理解
这里应该指的是面相接口编程,说的是对象与对象之间的依赖关系。
通俗的说就是把业务的功能划分清楚后,定义在接口和抽象类中,然后编写业务类继承或实现该接口或抽象类,如果对外发布封装的业务功能,要发布接口或抽象类,而不是具体的实现类。
例子
依赖倒置在工作中是最常使用的。
项目中常用的分层架构,Controller 层,Service 层,Dao 层。Controller 层会调用 Service 层,Service 层会调用 Dao 层。
假如使用 Spring Framework
开发用户模块,编写数据访问层的时候,都会编写一个 UserDao
接口,然后编写一个 UserDaoImpl
的实现类。UserServiceImpl
会创建一个 UserDao
对象,然后使用 @Autowired
自动装配。
@Service("userService")
public class UserServiceImpl implements UserService {
....
@Autowired
public void setUserDao(UserDao userDao) {this.userDao = userDao;}
private UserDao userDao;
}
这里就用到了依赖倒置原则,依赖抽象接口而不依赖于具体的实现类。
好处
可以减少类见的耦合性,提高系统的稳定性,可高代码的可读性和可维护性,也可以降低同时开发时引起的风险。
接口隔离原则(Interface Segregation Principle)
拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。
个人理解
设计接口的时候,尽量不要设计出一个拥有非常多的方法的庞大接口,应该尽量使用多个专门提供某种功能的接口。
例子
还是用开发用户模块举例,在编写根据手机号登录,如果登录不存在则注册的时候,需要用到发送短信验证码、验证短信验证码、登录、注册等功能。
此时,如果根据接口隔离原则,就需要登录将一个 LoginService
接口拆分成多个专门提供某种功能的接口,例如:
-
LoginService
接口,里面有登录功能; -
PinCodeService
接口,里面有发送 PinCode 和验证 PinCode 的功能; -
RegeditService
接口,里面有注册的功能;
那么在登录时有可能会用到这三个接口。
此时,还有一个业务场景,修改手机号时,需要输入旧的手机号,获取 Pin 码并验证,然后输入新的手机号,获取 Pin 码并验证,最后调用修改电话号的功能。那么在修改手机号时只需要用到 PinCodeService
即可,不需要使用 LoginService
和RegeditService
。
这就是接口隔离原则的一个实现例子。
满足接口隔离原则的同时,先满足单一职责原则。
好处
使程序解耦,从而容易重构、更改和重新部署。
最少知识原则(Least Knowledge Principle)
又叫迪米特法则,系统中的类,尽量不要过多的与其他类互相作用,减少类之间的耦合度。
- 每个单元对其他单元只拥有有限的知识,只了解与当前单元紧密联系的单元;
- 每个单元只能和它的 “ 朋友 ” 交谈,不能和 “ 陌生人 ” 交谈;
- 只和自己直接的 “ 朋友 ” 交谈;
个人理解
一个对象调用另一个对象时,应该不去过分关注这个对象的实现。被调用的对象的内部是如何复杂都和调用者无关,简单的说,调用者就知道被调用者提供的的 public 方法就行,其他的不需要关心。
例子
假设一个场景,老板要统计公司的人数,可以这个命令委托给主管,由主管去统计人数。
public class Boss {public void command(Manager manager) {....}
}
public class Manager {
private List<Employee> employees;
public Manager(List<Employee> employees) {this.employees = employees;}
public int employeeCount() {return this.employees.size();
}
}
public class Employee {}
public class Client {public static void mand(String[] args) {List<Employee> employees = new ArrayList<>();
... 添加 20 个 employee
Boss boss = new Boss();
boss.command(new Manager(employees));
}
}
其中 Boss
只与 Manager
进行交谈,Manager
只与 Employee
进行交谈,Boss
不与 Employee
进行交谈,这就是最少知识原则的一个例子。
好处
类负责的职责清晰明了,使类可以高度内聚降低类与类之间的耦合。
开闭原则(Open Close Principle)
软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。
个人理解
我对开闭原则的理解是,程序应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。面向对象可以达到这种方式完成对软件的扩展。
在对软件进行设计时,应该尽可能的考虑到后续会变更的地方,以便在变更真发生时,可以尽可能在不修改或修改很少的情况下,把变更实现。
例子
开闭原则是最基础的设计原则,其它的五个设计原则都是开闭原则的具体形态。
好处
开闭原则可以提高复用性,维护性,减少测试范围。