软件设计有很多原则,比如软件设计上的 SOLID principle,单元测试中的 FIRST和AAA,代码实现上的 DRY principle 等。熟悉这些原则,可以把我们的经验上升到理论高度,有利于程序员的成长,也便于团队带头人和组员控制软件质量。

我们先介绍 SOLID 原则。SOLID 是下面几个英文词组的缩写

  1. Single-Responsibility principle (单一职责原则)
  2. Open-Close principle (开放扩展,封闭修改)
  3. Liskov substitution principle (Liskov 替换原则)
  4. Interface segregation principle (接口隔离原则)
  5. Dependency inversion principle (依赖反转原则)

Single-Responsibility principle (单一职责原则)

这个原则是指一个软件模块只应该负责一件事情。当规范修改的时候,我们只需要修改与规范相关的模块。软件模块不应该像瑞士军刀那样,什么都能干,而应该像厨房里刀具套装里的刀具一样各司其责。

比如下面的代码:

public class ProtocolTranslator
{public String Translate(String content){try {// Translate the protocol}catch(Exception ex){WriteLogToFile(ex.getMessage());}}public void WriteLogToFile(String log){// write log to file}
}

上面的代码是一个协议翻译类,在协议翻译的过程中如果出现了异常,则把异常写入文件日志中。粗略看来这个类没有问题,但是如果我们需要把日志写入数据库,那么我么就需要改变代码。按照单一职责原则,这个类的设计就没有达到要求,因为日志规范的修改,确需要修改协议翻译类。为此我们可以引入专门的日志类来解决这个问题。代码如下:

public class ProtocolTranslator
{private final Logger logger;ProtocolTranslator(Logger logger){this.logger = logger;}public String Translate(String content){try {// Translate the protocol}catch(Exception ex){this.logger.log(ex);}}
}

如果再遇到日志相关的需求变更,我们只需要修改日志类就好了。

Open-Close principle (开放扩展,封闭修改)

开放扩展说的是我们设计的模块或者类只有在收到新的需求的时候才会增加新的功能,只有在发现了缺陷 (bug) 的时候才需要修改。现代软件往往要求单元测试达到一定覆盖率,如果不遵从OCP,那么光重新修改单元测试测试就会产生巨大的工作量。如果我们增加新功能都要大量修改我们的单元测试代码,那么就说明我们需要引入OCP原则。这里说的增加新功能是指通常通过继承类来实现的。

假设我们有下面的鸡和狗的类:

public class Dog
{}public class Chicken
{}public class AnimalCounter
{public int countFeet(ArrayList<Object> animals){int count = 0;for(Object animal: animals){if(animal instanceof Dog)count += 4;else if(animal instanceof Chicken)count +=2;}return count;}
}

上面这个程序 AnimalCounter 负责统计动物的腿数,如果我们要增加一种新动物比如 Sheep,我们就需要给 AnimalCounter 的 countFeet 函数增加一个判断,判断数组中是不是有 Sheep 实例,这就是说当有新的需求来的时候,我们得修改 AnimalCounter 代码,而不是扩展它。

下面的代码可以解决这个问题。我们使用了一个接口,鸡类和狗类都继承了这个接口,在 AnimalCounter 中我们只要调用这个接口就可以知道动物有多少只脚了。无论是再有绵羊类或者是昆虫类,它们只要继承了这个接口,AnimalCounter 都可以计算出动物的总脚数。也就是我们通过扩展 IAnimal 接口就可以满足需求,而不用修改 AnimalCounter 类。

public interface IAnimal{int getFeet();
}public class Dog implements IAnimal
{public int getFeet() { return 4; }
}public class Chicken
{public int getFeet() { return 2; }
}Public class AnimalCounter
{public int countFeet(ArrayList<IAnimal> animals){int count = 0;for(IAnimal animal: animals){count += animal.getFeet();}return count;}
}

Liskov substitution principle (Liskov 替换原则)

这条原则是说,程序中的对象可以被它的子类的实例替换掉而不会影响程序的正确性。这个原则跟契约式编程 (design by contract) 非常像 。

来看下面的代码。我们定义了一个类叫 Bird,这个接口有四个方法,然后我们有一个天鹅类,一个鸡类。可以看到,Bird 的四个方法用 Swan 类来代替是没有问题的,但是用 Chicken 类来代替当调用到 fly 方法的时候就会抛出异常。这个就不符合 Liskov 原则,因为作为 Bird 类的子类的 Chicken 类没有做到替换父类 Bird 类而不影响程序运行。

public class Bird {String setName();String getName();void playSound(){...}void fly(){...}
}public class Swan extends Bird{String setName() { ... }String getName() { ... }void playSound() {playSwanSound();}}
public class Chicken extends Bird {String setName() { ... }String getName() { ... }void playSound() { playChickenSound();}void fly() {throw new Exception();}
}

解决方法是拆分Bird类的功能,因为家禽是不会飞的。

Interface segregation principle (接口隔离原则)

接口隔离原则说的是类不应该被强迫去依赖它用不到的方法。大的原则是很多小而精的接口,要好于一个大一统的接口。比如下面的 ICar 接口,我们不应该为了大一统把加油 (fuel) 和 (Charge) 充电都放在里面,因为烧汽油的汽车才需要加油,使用电池驱动的电动车才需要充电。如果子类继承了父类中用不到的方法,子类也会打破上面的 Liskov 原则,也不利于将来的重构和优化。

public interface ICar {void fuel(int litreAmount);void charge(int kilowatt);
}public GasCar implements ICar {void fuel(int litreAmount){...}void charge(int kilowatt) {// 汽油车不需要充电throw new Exception();}
}public EV implements ICar {void fuel(int litreAmount){// 电动车不需要加油throw new Exception();}void charge(int kilowatt) {...}
}

正确的设计,应该设计两个接口。汽车实现汽车的接口,电动车实现电动车的接口,混合动力汽车既可以充电也可以加油,所以需要同时实现汽车和电动车接口。

public IGasCar {void fuel(int litreAmount);
}public IEV {void charge(int kilowatt);
}public GasCar implements IGasCar {void fuel(int litreAmount){...}
}public EV implements IEV {void charge(int kilowatt) {...}
}// 混合动力汽车
public Hybrid implements IGasCar, IEV {void fuel(int litreAmount){...}void charge(int kilowatt) {...}
}

Dependency inversion principle (依赖反转原则)

依赖反转说了两点:

  1. 高层模块不应该依赖低层模块,双方应该依赖抽象。
  2. 抽象不应该依赖细节,而细节应该依赖抽象。

听起来很绕口,不过这个确实是面向对象编程里解决紧耦合问题最重要的原则之一。通常的解决方案就是大名鼎鼎的依赖注入!

下面的代码的任务是打印一个指定路径的文件,打印完成后发出 email。这个代码就违反了依赖反转原则,所有的 new 语句处都表示高层模块需要知道低层模块的细节,比如 Program 类就需要如何生成 PrinterService 和 EMailService,PrinterService 和 EMailService 的功能也没有被抽象出来。这样程序的功能在需要重构、扩展或者替换时高层模块和低层模块都需要知道对方的细节。

public Program {public static void main(String[] args) {var filePath = args[0];var printerService = new PrinterService();printerService.print(filePath);var emailService = new EMailService();emailService.send("surfirst@example.com", "File printed", filePath);}
}public class PrinterService {final Logger logger;PrinterService(){this.logger = new Logger();}void print(String filePath) {// process print task...this.logger.log(filePath + " printed");}
}public class Logger {public log(String content){System.out.println(content);}
}public class EmailService {public void Send(String email, String subject, String content){System.out.println("Email %s has been sent to %s. ", subject, email);}
}

解决上面的方法可以是依赖注入,也可以通过类工厂的方法来解决。由于篇幅有限,我们在这里使用类工厂来展示解决方案。首先我们抽象出我们用到的组件的接口,然后我们通过类工厂来实现这些接口,最后通过类工厂来解决依赖问题。

下面是我们抽象出来的接口:

public inteface IPrinterService {void print(String filePath);
}public interface ILogger {void log(String content);
}public interface IEmailService {void send(String email, String subject, String content);
}

下面是类工厂的代码,虽然看上去很简单,但是通过类工厂,我们就解决了抽象到实现细节的问题,这使我们的业务逻辑独立于我们的依赖项。依赖项可以来自外部文件,对 Java 来说就是不同的 JAR 文件,对 .Net 来说可以是不同的 DLL。

public class Factory {public static IPrinterService CreatePrinterService(){return new PrinterService();}public static ILogger CreateLogger() {return new Logger();}public static IEmailService CreateEmailService() {return new EmailService();}
}

下面是使用了类工厂以后的业务逻辑代码:

public Program {public static void main(String[] args) {var filePath = args[0];IPrinterService printerService = Factory.CreatePrinterService();printerService.print(filePath);IEmailService emailService = Factory.CreateEmailService();emailService.send("surfirst@example.com", "File printed", filePath);}
}public class PrinterService implements IPrinterService {final Logger logger;PrinterService(){this.logger = Factory.CreateLogger();}void print(String filePath) {// process print task...this.logger.log(filePath + " printed");}
}public class Logger implements ILogger {public log(String content){System.out.println(content);}
}public class EmailService implements EmailService {public void Send(String email, String subject, String content){System.out.println("Email %s has been sent to %s. ", subject, email);}
}

结论

SOLID 是面向对象设计中5个重要原则的缩写。这5个原则可以帮助我们实现软件高内聚,低耦合的目标。到目前为止,还没有编译器或者软件设计工具能帮助我们自动应用这些原则,我们还是需要通过探索和实践才能掌握和应用它们。
在接下来的文章中,我们还会讨论 FIRST,AAA,DRY 等经常提到的软件设计原则。希望能对大家有所帮助。

参考链接

单元测试中的 FIRST 原则
单元测试中的 AAA 规则
编写代码中的 DRY 原则

软件设计原则之 SOLID Principle相关推荐

  1. SOLID 软件设计原则

    * 软件腐化的原因: 问题所在   设计目标 ---------------------------------------------------------------------------- ...

  2. 软件设计原则(一)开闭原则(Open-Closed Principle, OCP)

    狭义理解:对扩展开发,对修改封闭 在学习设计模式之前,应该先对软件设计原则有一定的了解,设计模式在一定程度上是迎合软件设计原则而产生的,脱离了软件设计原则,设计模式是没有意义的. 开-闭原则(Open ...

  3. 五大软件设计原则学习笔记5——依赖倒置原则

    五大软件设计原则SOLID: 单一职责原则(Single responsibility principle,SRP)开放封闭原则(Open–closed principle,OCP)Liskov 替换 ...

  4. 五大软件设计原则学习笔记4——接口隔离原则

    五大软件设计原则SOLID: 单一职责原则(Single responsibility principle,SRP) 开放封闭原则(Open–closed principle,OCP) Liskov ...

  5. 五大软件设计原则学习笔记3——Liskov 替换原则

    五大软件设计原则SOLID: 单一职责原则(Single responsibility principle,SRP) 开放封闭原则(Open–closed principle,OCP) Liskov ...

  6. 五大软件设计原则学习笔记2——开放封闭原则

    五大软件设计原则SOLID: 单一职责原则(Single responsibility principle,SRP) 开放封闭原则(Open–closed principle,OCP) Liskov ...

  7. 五大软件设计原则学习笔记1——单一职责原则

    五大软件设计原则SOLID: 单一职责原则(Single responsibility principle,SRP) 开放封闭原则(Open–closed principle,OCP) Liskov ...

  8. 设计原则之 SOLID 原则

    以下是在极客时间<设计模式之美> 中的写学习笔记与心得的总结 在最初开始学设计模式的时候,总觉的要学的是那23种经典的设计模式.通过一段的学习,才突然领悟,设计原则才是王道,才是真正的内功 ...

  9. 设计模式之软件设计原则篇

    3.软件设计原则 本文的内容绝大部分借鉴了https://www.jianshu.com/u/cc272db15285的内容,感兴趣的小伙伴可以进入其简书浏览更细的内容,讲的非常好. 在软件开发中,为 ...

最新文章

  1. 微调BERT:序列级和令牌级应用程序
  2. ASP.NET MVC 2 模型验证
  3. centOS上docker 的简单使用
  4. 解放你内心的自然领袖,从你的内心而非你的自我来领导你自己
  5. 2行代码实现小程序分享到朋友圈功能
  6. python连接sql server
  7. winform窗体MaximizeBox
  8. golang之strings
  9. 《现代操作系统(中文第四版)》课后习题答案 第四章 文件系统
  10. PHP九九乘法表代码
  11. 分享一个强大的在线写API接口文档的工具showdoc
  12. windows虚拟桌面_在Windows中使用虚拟桌面的最佳免费程序
  13. 【Encoding】UTF-8编码规则
  14. 深度学习入门,计算机视觉,推荐系统,自然语言处理理论框架以及学习资料【附知识图谱与链接】
  15. 用Python爬取2020链家杭州二手房数据
  16. 在windows内使用virtualbox搭建安卓x86,以及所遇到的问题解决--2.virtualbox上安卓x86的配置
  17. 堂食快餐连锁店系统(一)业务流程分析
  18. Rosetta Stone 4.1.15 下载破解和语言包
  19. day43_crud
  20. P3131 [USACO16JAN]Subsequences Summing to Sevens S

热门文章

  1. python批量打印mathcad_转载:简单比较几个计算数学软件 Matlab Mathematica MathCAD
  2. eclipse连接SQL Server数据库(详解很细心)
  3. 【企业架构】企业架构师的战略角色
  4. 2018年最全Go语言实战抽奖系统教程
  5. 如何知道你的app进入了前台还是后台
  6. sql存储过程及应用
  7. Copyright ©的含义
  8. 交行信用卡总经理王卫东:信用卡互联网转型有五大基础
  9. C++学习(四八三)无法从“std::pair<const _Kty,_Ty>”转换为“_Objty”
  10. java int转日期_Java时间日期格式转换