软件设计最大的难题就是应对需求的变化,但是纷繁复杂的需求变化又是不可预料的,我们要为不可预料的变化做好准备,这本身是一件非常痛苦的事情,但好在有大师们已经给我们提出了非常好的六大设计原则和 23 种设计模式来“封装”未来的变化。
在软件设计上有一些经典的设计原则,其中包括,SOLID、KISS、YAGNI、DRY、LOD 等。这些设计原则,从字面上理解,都不难。你一看就感觉懂了,一看就感觉掌握了,但是用在项目中的时候就会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。我有时会对这些原则理解的不够透彻,导致在使用时过于教条注意,拿原则当真理,生搬硬套,适得其反。
下面我对这些原则做一些梳理和整理,确保自己理解的是正确的。
下面我对这些原则做一些梳理和整理,确保自己理解的是正确的。
SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:
- 单一职责原则
- 开闭原则
- 里氏替换原则
- 接口隔离原则
- 依赖反转原则
分别对应 SOLID 的 S、O、L、I、D 这 5 个英文字母。
原则描述
单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。单一职责原则:
1 | 一个类或者模块只负责完成一个职责(或者功能) |
这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,有两种理解方式。
一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。但不管是哪种理解方式,单一职责原则在应用到这两个描述对象的时候,道理都是相通的。
单一职责原则的定义描述非常简单,也不难理解。一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
如何判断类的职责是否足够单一?
在大部分情况下,类里的方法是归为同一类功能,还是归为不相关的两类,并不是那么容易判定的。在不同的应用场景、不同阶段的需求背景和下,对同一个类的职责是否单一的判定,可能都是不一样的。
在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但是如果换个应用场景或者在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。
评价一个类的职责是否足够单一,并没有一个非常明确的、可以量化的标准,可以说这是一件非常主观、仁者见仁智者见智的事情。实际上,在软件开发中,也没必要过于未雨绸缪,过度设计。
所以我们可以先写一个粗粒度的类,满足业务需求。随着业务发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候我们就可以将这个粗粒度的类,拆分成几个更细粒度的类,这个就是所谓的持续重构。
也有一些技巧从侧面去判定一个类的职责是否单一,而且这几个小技巧比起主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:
- 类中的代码行数、函数或者属性过多,会影响代码的可读性和可维护性,这时候就需要考虑对类进行拆分;
- 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想;
- 私有方法过多,需要考虑将私有方法独立到新的类中,供更多的类使用从而提高代码的复用性;
- 比较难给类起一个合适名字,很难用一个业务名词去概括,或者只能用一些笼统的 Manager、Context 之类的词语去命名,这就说明类的职责定义可能不够清晰;
- 类中大量的方法都是集中操作类的某几个属性,可以考虑将这些属性拆分出来成一个单独的类。
1 | 在上面的判定原则中,提到类中的代码行数、函数或者属性过多,就有可能不满足单一职责原则。 |
当一个类的代码,读起来让你头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能就要引入整个类(类中包含很多无关此功能的函数)的时候,这就说明类的行数、函数、属性过多了。
类的职责是否设计得越单一越好?
为了满足单一职责原则,是不是把类拆得越细就越好呢?答案是否定的。
内聚和耦合其实是一个意思,从相反方向的两种阐述。
1 |
|
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。