软件设计有很多原则,比如软件设计上的 SOLID principle,单元测试中的 FIRST和AAA,代码实现上的 DRY principle 等。熟悉这些原则,可以把我们的经验上升到理论高度,有利于程序员的成长,也便于团队带头人和组员控制软件质量。
我们先介绍 SOLID 原则。SOLID 是下面几个英文词组的缩写
-
Single-Responsibility principle
单一职责原则
-
Open-Close principle
开放扩展,封闭修改
-
Liskov substitution principle
Liskov 替换原则
-
Interface s egregation principle
接口隔离原则
-
Dependency inversion principle
依赖反转原则
Single-Responsibility principle
单一职责原则
这个原则是指一个软件模块只应该负责一件事情。当需求调整的时候,我们只需要修改与需求相关的模块。软件模块不应该像瑞士军刀那样,什么都能干,而应该像厨房里刀具套装里的刀具一样各司其责。
比如下面的代码:
public class ProtocolTranslator
上面的代码是一个协议翻译类,在协议翻译的过程中如果出现了异常,则把异常写入文件日志中。粗略看来这个类没有问题,但是如果我们需要把日志写入数据库,那么我么就需要改变代码。按照单一职责原则,这个类的设计就没有达到要求,因为日志规范的修改,却需要修改协议翻译类。为此我们可以引入专门的日志类来解决这个问题。代码如下:
public class ProtocolTranslator
如果再遇到日志相关的需求变更,我们只需要修改日志类就好了。
Open-Close principle
开放扩展,封闭修改
开放扩展说的是我们设计的模块或者类只有在收到新的需求的时候才会增加新的功能,只有在发现了缺陷 (bug) 的时候才需要修改。现代软件往往要求单元测试达到一定覆盖率,如果不遵从OCP,那么光重新修改单元测试就会产生巨大的工作量。如果我们增加新功能就要大量修改我们的单元测试代码,那么就说明我们需要引入OCP原则。这里说的增加新功能是指通常通过继承类来实现的。
假设我们有下面的鸡和狗的类:
public class Dog
上面这个程序 AnimalCounter 负责统计动物的腿数,如果我们要增加一种新动物比如 Sheep,我们就需要给 AnimalCounter 的 countFeet 函数增加一个判断,判断数组中是不是有 Sheep 实例,这就是说当有新的需求来的时候,我们得修改 AnimalCounter 代码,而不是扩展它。
下面的代码可以解决这个问题。我们使用了一个接口,鸡类和狗类都继承了这个接口,在 AnimalCounter 中我们只要调用这个接口就可以知道动物有多少只脚了。无论是再有绵羊类或者是昆虫类,它们只要继承了这个接口,AnimalCounter 都可以计算出动物的总脚数。也就是我们通过扩展 IAnimal 接口就可以满足需求,而不用修改 AnimalCounter 类。
public interface IAnimal{
Liskov substitution principle
Liskov 替换原则
这条原则是说,程序中的对象可以被它的子类的实例替换掉而不会影响程序的正确性。这个原则跟契约式编程 (design by contract) 非常像 。
来看下面的代码。我们定义了一个类叫 Bird,这个接口有四个方法,然后我们有一个天鹅类,一个鸡类。可以看到,Bird 的四个方法用 Swan 类来代替是没有问题的,但是用 Chicken 类来代替当调用到 fly 方法的时候就会抛出异常。这个就不符合 Liskov 原则,因为作为 Bird 类的子类的 Chicken 类没有做到替换父类 Bird 类而不影响程序运行。
public class Bird {
解决方法是拆分Bird类的功能,因为家禽是不会飞的。
Interface segregation principle
接口隔离原则
接口隔离原则说的是类不应该被强迫去依赖它用不到的方法。大的原则是很多小而精的接口,要好于一个大一统的接口。比如下面的 ICar 接口,我们不应该为了大一统把加油 (fuel) 和 (Charge) 充电都放在里面,因为烧汽油的汽车才需要加油,使用电池驱动的电动车才需要充电。如果子类继承了父类中用不到的方法,子类也会打破上面的 Liskov 原则,也不利于将来的重构和优化。
public interface ICar {
正确的设计,应该设计两个接口。汽车实现汽车的接口,电动车实现电动车的接口,混合动力汽车既可以充电也可以加油,所以需要同时实现汽车和电动车接口。
public IGasCar {
Dependency inversion principle
依赖反转原则
依赖反转说了两点:
-
高层模块不应该依赖低层模块,双方应该依赖抽象。
-
抽象不应该依赖细节,而细节应该依赖抽象。
听起来很绕口,不过这个确实是面向对象编程里解决紧耦合问题最重要的原则之一。通常的解决方案就是大名鼎鼎的依赖注入!
下面的代码的任务是打印一个指定路径的文件,打印完成后发出 email。这个代码就违反了依赖反转原则,所有的 new 语句处都表示高层模块需要知道低层模块的细节,比如 Program 类就需要如何生成 PrinterService 和 EMailService,PrinterService 和 EMailService 的功能也没有被抽象出来。这样程序的功能在需要重构、扩展或者替换时高层模块和低层模块都需要知道对方的细节。
public Program {
解决上面的方法可以是依赖注入,也可以通过类工厂的方法来解决。由于篇幅有限,我们在这里使用类工厂来展示解决方案。首先我们抽象出我们用到的组件的接口,然后我们通过类工厂来实现这些接口,最后通过类工厂来解决依赖问题。
下面是我们抽象出来的接口:
public inteface IPrinterService {
下面是类工厂的代码,虽然看上去很简单,但是通过类工厂,我们就解决了抽象到实现细节的问题,这使我们的业务逻辑独立于我们的依赖项。依赖项可以来自外部文件,对 Java 来说就是不同的 JAR 文件,对 .Net 来说可以是不同的 DLL。
public class Factory {
下面是使用了类工厂以后的业务逻辑代码:
public Program {
结论
SOLID 是面向对象设计中5个重要原则的缩写。这5个原则可以帮助我们实现软件高内聚,低耦合的目标。到目前为止,还没有编译器或者软件设计工具能帮助我们自动应用这些原则,我们还是需要通过探索和实践才能掌握和应用它们。