SOLIDS设计原则

SOLIDS设计原则不是面向对象编程领域所特有的,而是普遍存在于整个软件工程中的指导性原则,涵盖系统级编程和应用级编程。

一、单一指责原则(A class should have only one reason to change.)

1. 定义:一个类仅有一个引起它变化的原因。虽然这一原则明确是在说类的设计,但是实际中在一个模块、一个函数或者方法等程序实体上同样适用。
2. 场景:例如有一个Rectangle类,它有两个方法,其中一个是把矩形画在屏幕上,另一个则是计算它的面积。
  有两个不同的应用会用到Rectangle类,其中一个是用来做几何计算的,它需要知道矩形的面积,但是不会需要画出它。
  另一个应用则是绘图,它需要把矩形画出来。那么上面设计的Rectangle类就违反了单一职责的原则(Violates SRP)。
3. 如果违背了SRP会带来什么问题:
  第一,单纯做计算的应用程序并没有用到任何GUI的东西,但是由于它用到了Rectangle,所以需要引用GUI。既然被引用了,那么GUI就需要跟随计算应用程序被编译和部署。
  第二,如果绘图应用程序的变化需要引起Rectangle的改变,那么计算应用程序也需要重新编译、测试和部署,否则可能引起不可预测的问题。
4. 注意:也有人把它解释为一个程序实体只做一件事情,虽然也说得通,但是这并不是作者的本意。这里的职责并不是负责的事情,而是‘A reason for change’。
  An axis of change is an axis of change only if the changes occur. It is not wise to apply SRP—or any other principle,
  for that matter—if there is no symptom。也就是说不要过度解读这个原则,一个类也可以做不止一件事情,只要让它改变的原因只有一个就好。
  下面是Modem接口,它是否违反SRP就看你怎么用它了。
    public interface Modem {
      public void Dial(string pno);
      public void Hangup();
      public void Send(char c);
      public char Recv();
    }
如果从所做的事情来看的话,这里Modem做了两件事:1)管理连接,Dial和Hangup方法;2)数据通讯,Send和Recv方法。
但是这两个职责应该被分到两个类中吗?那就取决于应用系统需要如何改变了。如果应用系统不会对这两个职责做不同的改变,那也就不需要对它们进行拆分了。

二、开放闭合原则(Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.)

1. 概念:开放闭合原则强调对扩展开放,对修改闭合(同样适用于类、模块和方法等)。看起来说了两点实际上就是一点:为了适应新的需求,尽量不要修改原始代码,而是扩展原有的代码。
  开发的时候一定要谨记这句话。需求会变是正常的,好的系统不会拒绝变化,只会需要添加code或者修改很少的code就能支持这些变化。
2. 场景:要遵循这一原则,最好的办法就是抽象(abstract),最常用的设计模式有策略模式(Strategy)和模板方法模式(Template Method)。
  例如,Client类依赖于Server类,但是一旦将来换一个Server,则Client里面涉及到Server的地方全部要改变。这个时候就可以用到Strategy。
  抽象一个ClientInterface,Client依赖于这个抽象,对于这个抽象的实现则可以是不同的Server对应不同的策略。与Strategy类似,Template Method也是需要一个抽象,
  然后在这个抽象的基础上派生出一些类来做具体的实现。不同的是模板方法在这个抽象中定义了一个抽象方法和一个模板业务方法,模板业务方法会调用抽象方法。
  抽象方法的具体实现在继承这个抽象类的具体子类中。
  面对一些我们不得不对老的code做出一些修改的时候,我们除了痛骂怎么会有这么变态的需求的同时,可以多想想,如果再来一些这么变态的需求,我现在的这种修改方式可以支持吗?
  主要利用语言的多态性,而多态性又具体落实在语言的继承、实现机制上。

三、里氏替换原则(Subtypes must be substitutable for their base types.)

1. 概念:子类型的对象必须能够替换它的基类型对象。
2. OCP的实现机制是抽象和多态,而它们的关键是继承。LSP所强调的就是继承的实现规则。
3. 违反LSP会带来什么问题:如果违反LSP,类继承就会混乱,如果子类作为一个参数传递给参数为基类的方法,将会出现未知行为;
  如果违反LSP,适用于基类的单元测试将不能成功用于测试子类。
4. 现实中很多违反LSP的情况并不像上例这么明显。比如说,我们有一个基类是Rectangle,我们在它的基础上派生出Square。
  一般说派生类满足IS-A(Square is a rectangle)就可以,理论上看也确实满足。但是Rectangle类中有height和width,
  Square类中要求它们必须相等,那么我们可以在Square中对它们的赋值操作进行重写,即任何对height和width的set操作都会设置它们俩为同一个值。
  但是如果用户有一个下面这样的方法来使用Rectangle,则这里的设计就违反了LSP。
    void g(Rectangle r) {
      r.Width = 5;
      r.Height = 4;
      if(r.Area() != 20)
        throw new Exception("Bad area!");
    }

四、接口分离原则(Clients should not be forced to depend on methods they do not use.)

1. 概念:客户端不应该*依赖于它不会用到的方法。
2. 场景:我曾经遇到过这样一个应用场景,在类P1,P2和P3中都需要一些config,这些config需要一些其他的操作来获取,
  而原本这些config的获取散落在P1,P2和P3处理逻辑中间。这导致对获取config相关的代码改动时会影响到毫不相关的处理逻辑。
  所以,我们希望把这些config decouple出来,让P1,P2和P3依赖于一个抽象的config,而具体获取config的方法则实现在这个抽象的派生类中。
  这是下面我们会介绍的另一个原则DIP,但是我要说的是,我当时就试图写一个IConfigure把P1,P2和P3中用到的接口全部定义了,
  然后再对应的去写几个Configure类来实现IConfigure的一部分。当时只觉得这样写就可以只写一个接口了,可是它却需要变得非常“胖”,很显然违背了ISP。

五、依赖倒置原则

A). High-level modules should not depend on low-level modules. Both should depend on abstractions.
B). Abstractions should not depend upon details. Details should depend upon abstractions.

1. 概念:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖于抽象。
2. 由来:如果我们违反了这一原则,修改低层模块会影响高层模块,甚至迫使高层模块(policy decisions and business models)做出相应的修改。
  另外,我们在低层模块上的复用已经做得很好了,但是若高层模块依赖于低层模块,那么高层模块是很难复用的。
3. 实践:在代码中传递参数或关联关系时,尽量引用高层的抽象层类,即使用接口和抽象类进行变量声明、参数类型声明、方法返回类型声明以及数据类型的转换;
  当一个对象和其他对象有依赖关系时,可以利用依赖注入的方法将多个类进行解耦,主要有:构造注入、Set方法注入和接口注入;
  开闭是目的,里氏是基础,依赖倒置是手段;
  低层模块尽量都要有抽象类或接口,或者两者都有;
  变量的声明类型尽量是抽象类或接口;
  使用继承时遵循里氏替换原则。
4. 好处:可降低类之间的耦合性,可提高系统的稳定性,可降低修改程序所造成的风险。
上一篇:Javascript笔记:Object.create, new,原型以及Object.assign


下一篇:Rust程序设计语言(5)