Change is the eternal truth.

SOLID

  • SRP (The Single-Responsibility Principle)
  • OCP (The Open-Closed Principle)
  • LSP (The Liskov Substitution Principle)
  • ISP (The Interface-Segregation Principle)
  • DIP (The Dependency-Inversion Principle)

(Implementation:LKP(The Least Knowledge Principle))


1. 单一职责原则(SRP)

定义: 不要存在多于一个导致类变更的原因。
针对问题: 当类T同时负责两个问题时,因职责P1需求改变而导致T改变,可能会对原本正常P2产生故障。
解决方案: 一个类只负责一项职责。
认知: 当出现职责扩散(P被分化为粒度更细的P1和P2)而未对类进行进一步细分时,会违背SRP。但适当地违背SRP时,反而能提高开发效率。
优点:

  • 降低类的复杂度
  • 提高类可读性
  • 降低变更引起的风险

案例:image.png在手机设计中,将dial,hangup(通话协议功能)和chat,listen(数据传入功能)放在一个接口中,而两个功能不管哪个功能影响,都需要对类产生影响。
故最好拆分为:
image.png


2. 开闭原则(OCP)

定义:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
软件实体(class,module,function,…)应对扩展开放,对修改(modification)关闭
针对问题: 在软件的生命周期内,由于变化、升级、维护等原因对原代码进行修改时,会给旧代码引入错误,使得不得不对代码进行重构与重新测试。
解决方案: 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化, 而不是修改已有代码
认知:
① 开闭原则更像是对于软件设计结果的约束,而未提出具体的实现方法。在某些博客中可以看到,若遵循设计模式的其余5大原则,则设计出的软件是符合开闭原则。
用抽象构建框架,用实现扩展细节。 抽象保证软件架构的稳定,易变细节由抽象派生类实现。
案例:
一个工厂生产宝马和奥迪两种车

  1. 1 internal class Program
  2. 2 {
  3. 3 private static void Main(string[] args)
  4. 4 {
  5. 5 AutomobileFactory carf = new AutomobileFactory();
  6. 6 carf.createAuto(AutomobileFactory.AutoType.BMW);
  7. 7 carf.createAuto(AutomobileFactory.AutoType.AUDI);
  8. 8 }
  9. 9 }
  10. 10
  11. 11 internal interface IAutomobile
  12. 12 {
  13. 13 }
  14. 14
  15. 15 internal class Audi : IAutomobile
  16. 16 {
  17. 17 public Audi()
  18. 18 {
  19. 19 Console.WriteLine("hi 我是奥迪");
  20. 20 }
  21. 21 }
  22. 22
  23. 23 internal class BMW : IAutomobile
  24. 24 {
  25. 25 public BMW()
  26. 26 {
  27. 27 Console.WriteLine("hello 我是宝马");
  28. 28 }
  29. 29 }
  30. 30
  31. 31 internal class AutomobileFactory
  32. 32 {
  33. 33 public enum AutoType
  34. 34 {
  35. 35 AUDI, BMW
  36. 36 }
  37. 37
  38. 38 public IAutomobile createAuto(AutoType carType)
  39. 39 {
  40. 40 switch (carType)
  41. 41 {
  42. 42 case AutoType.AUDI:
  43. 43 return new Audi();
  44. 44
  45. 45 case AutoType.BMW:
  46. 46 return new BMW();
  47. 47 }
  48. 48 return null;
  49. 49 }
  50. 50 }

问题: 当工厂中需要生产一种新车型时,则会修改源代码中 Autotype 等处,导致不满足开闭原则,实际上是抽象和实现不合理。修改例如下:

  1. 1 internal class Program
  2. 2 {
  3. 3 private static void Main(string[] args)
  4. 4 {
  5. 5 IAutomobileFactory audi = new AudiFactory();
  6. 6 audi.createAuto();
  7. 7 IAutomobileFactory bmw = new BMWFactory();
  8. 8 bmw.createAuto();
  9. 9 }
  10. 10 }
  11. 11
  12. 12 internal interface IAutomobile
  13. 13 {
  14. 14 }
  15. 15
  16. 16 internal class Audi : IAutomobile
  17. 17 {
  18. 18 public Audi()
  19. 19 {
  20. 20 Console.WriteLine("hi 我是奥迪");
  21. 21 }
  22. 22 }
  23. 23
  24. 24 internal class BMW : IAutomobile
  25. 25 {
  26. 26 public BMW()
  27. 27 {
  28. 28 Console.WriteLine("hello 我是宝马");
  29. 29 }
  30. 30 }
  31. 31
  32. 32 internal interface IAutomobileFactory
  33. 33 {
  34. 34 IAutomobile createAuto();
  35. 35 }
  36. 36
  37. 37 internal class AudiFactory : IAutomobileFactory
  38. 38 {
  39. 39 public IAutomobile createAuto()
  40. 40 {
  41. 41 return new Audi();
  42. 42 }
  43. 43 }
  44. 44
  45. 45 internal class BMWFactory : IAutomobileFactory
  46. 46 {
  47. 47 public IAutomobile createAuto()
  48. 48 {
  49. 49 return new BMW();
  50. 50 }
  51. 51 }

此时 AutomobileFactory 抽象为一个抽象类,而具体的BM,Audi以及其他车辆为抽象派生类实现。

3. 里氏替换原则(LSP)

定义: 子类应该可以替换父类并出现在父类能够出现的地方。

  1. LSP针对的是继承,若继承是为了代码复用,共享父类的方法就应保持不变,不能被重新定义,子类只能通过添加新方法来扩展功能。父类调用方法地方,子类自然可替换。
  2. 若继承是为了多态,与以上不同的是, 多态前提就是子类覆盖并重新定义父类的方法 ,则为了符合LSP: 应该将父类定义为抽象类,定义抽象方法,而让子类重新定义这些方法 。而抽象类不能被实例化。

以上两点说明LSP原则。

要求:

  • 子类可实现父类抽象方法,不能覆盖父类非抽象方法
  • 子类增加新方法添加功能
  • 子类方法重载父类方法时,方法形参比父类输入参数更宽松
  • 子类方法重载父类方法时,返回值比父类更严格

4. 接口隔离原则(ISP)

定义: 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
针对问题: 类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案: 将接口I 拆分成独立的几个接口,类A与类C分别与他们所需要的接口建立依赖关系。
image.png
p1:类B和类D都存在冗余方法(红色标记)
image.png
解决方法:由于只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口。
*要求:

  • 接口尽量小,但要适量。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,不需要的方法隐藏。
  • 提高内聚,减少对外交互,使接口用最少的方法完成最多的事情 ?

5. 依赖倒置原则(DIP)

定义:

  1. 高层模块不应该依赖低层模块,二者都应该依赖抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象

针对问题: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案: 将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
认知:

  • 依赖倒置原则基于事实:以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。(Java中抽象的是接口或者抽象类,细节是具体实现类)
  • 核心思想: 面向接口编程

问题: 母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:

  1. class Book{
  2. public String getContent(){
  3. return "很久很久以前有一个阿拉伯的故事……";
  4. }
  5. }
  6. class Mother{
  7. public void narrate(Book book){
  8. System.out.println("妈妈开始讲故事");
  9. System.out.println(book.getContent());
  10. }
  11. }
  12. public class Client{
  13. public static void main(String[] args){
  14. Mother mother = new Mother();
  15. mother.narrate(new Book());
  16. }
  17. }

然而有一天当阅读物改成 newpaper 时,代码不能正常运行。
则需要引入一个抽象接口 IReader ,表示带字的读物。

  1. interface IReader{
  2. public String getContent();
  3. }

此时Mother类与接口 IReader 类形成依赖关系:

  1. class Newspaper implements IReader {
  2. public String getContent(){
  3. return "林书豪17+9助尼克斯击败老鹰……";
  4. }
  5. }
  6. class Book implements IReader{
  7. public String getContent(){
  8. return "很久很久以前有一个阿拉伯的故事……";
  9. }
  10. }
  11. class Mother{
  12. public void narrate(IReader reader){
  13. System.out.println("妈妈开始讲故事");
  14. System.out.println(reader.getContent());
  15. }
  16. }

方式:

  • 接口传递(常用)
  • 构造方法传递
  • setter方法传递

要求:

  • 底层模块尽量要有抽象类或接口
  • 变量的声明类型尽量是抽象类或接口
  • 继承遵循里氏替换法则

6.迪米特法则

Talk only to your immediate friends
定义: 一个对象应该对其他对象保持最少的了解
针对问题: 类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
解决方案: 尽量降低类与类之间的耦合。
问题: 有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计

  1. //总公司员工
  2. class Employee{
  3. private String id;
  4. public void setId(String id){
  5. this.id = id;
  6. }
  7. public String getId(){
  8. return id;
  9. }
  10. }
  11. //分公司员工
  12. class SubEmployee{
  13. private String id;
  14. public void setId(String id){
  15. this.id = id;
  16. }
  17. public String getId(){
  18. return id;
  19. }
  20. }
  21. class SubCompanyManager{
  22. public List getAllEmployee(){
  23. List list = new ArrayList();
  24. for(int i=0; i<100; i++){
  25. SubEmployee emp = new SubEmployee();
  26. //为分公司人员按顺序分配一个ID
  27. emp.setId("分公司"+i);
  28. list.add(emp);
  29. }
  30. return list;
  31. }
  32. }
  33. class CompanyManager{
  34. public List getAllEmployee(){
  35. List list = new ArrayList();
  36. for(int i=0; i<30; i++){
  37. Employee emp = new Employee();
  38. //为总公司人员按顺序分配一个ID
  39. emp.setId("总公司"+i);
  40. list.add(emp);
  41. }
  42. return list;
  43. }
  44. public void printAllEmployee(SubCompanyManager sub){
  45. List list1 = sub.getAllEmployee();
  46. for(SubEmployee e:list1){
  47. System.out.println(e.getId());
  48. }
  49. List list2 = this.getAllEmployee();
  50. for(Employee e:list2){
  51. System.out.println(e.getId());
  52. }
  53. }
  54. }

此处的问题出在 CompanyManager 上。根据迪米特法则,只与直接的朋友发生通信,而此处调用得到了 SubEmployee 的List,增加了不必要的耦合。
应修改为:分公司增加打印ID方法,总公司直接调用
认知: 迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个”中介”来发生联系,例如本例中,总公司就是通过分公司这个”中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。