编程艺术——软件设计模式SOLID原则

编程艺术——软件设计模式SOLID原则

  • SOLID
  • 一. 什么是设计模式?
    • 1. 单一责任原则(SRP)
    • 2. 开放封闭原则(OCP)
    • 3. 里氏替换原则(LSP)
    • 4. 接口分离原则(ISP)
    • 5. 依赖注入或倒置原则(DIP)
    • 6.迪米特法则(Law of Demeter)


原文链接:https://blog.csdn.net/yongwan5637/article/details/80482743

SOLID

设计模式的六大原则有:
Single Responsibility Principle:单一职责原则
Open Closed Principle:开闭原则
Liskov Substitution Principle:里氏替换原则
Law of Demeter:迪米特法则
Interface Segregation Principle:接口隔离原则
Dependence Inversion Principle:依赖倒置原则
把这六个原则的首字母联合起来( L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。

一. 什么是设计模式?

从广义角度讲设计模式是可解决一类软件问题并能重复使用的设计方案;
从狭义角度讲设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述,是在类和对象的层次描述的可重复使用的软件设计问题的解决方案.
模式体现的是程序整体的构思,也会出现在分析或者是概要设计阶段,包括创建型模式、结构型模式和行为型模式.
模式的核心思想是通过增加抽象层,把变化部分从那些不变部分里分离出来.
模式的四大基本要素包括:
1.模式名称(Pattern Name)
2.问题(Problem):描述应该在何时使用模式,解释了设计问题和问题存在的前因后果,可能还描述模式必须满足的先决条件
3.解决方案(Solution):描述了设计的组成成分、相互关系及各自的职责和协作方式.模式就像一个模板,可应用于多种场合,所以解决方案并不描述一个具体的设计或实现,而是提供设计问题的抽象描述和解决问题所采用的元素组合(类和对象).
4.效果(Consequences):描述模式的应用效果及使用模式应权衡的问题.

设计模式就是实现了SOLID原则,从而达到代码复用、增加可维护性的目的.
众所周知,编程最基本的原则就是要追求高内聚和低耦合的解决方案和代码模块设计。 S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。

编程艺术——软件设计模式SOLID原则_第1张图片

1. 单一责任原则(SRP)

当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承担其他类型的责任的时候,就需要分解这个类。 类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题,非常耗时耗力。
示例:

新建一个Rectangle类,该类包含两个方法,一个用于把矩形绘制在屏幕上,一个方法用于计算矩形的面积。如图
编程艺术——软件设计模式SOLID原则_第2张图片
Rectangle类违反了SRP原则。Rectangle类具有两个职责,如果其中一个改变,会影响到两个应用程序的变化。

一个好的设计是把两个职责分离出来放在两个不同的类中,这样任何一个变化都不会影响到其他的应用程序。

这违反了SRP(单一职责原则)。因为Rectangle类做了两件事,在一个方法里它计算了面积,在另外一个方法了它返回一个表示矩形的GUI。这会带来一些有趣的问题:在计算几何应用程序中我们必须包含GUI。也就是在开发几何应用时,我们必须引用GUI库;图形应用程序中Rectangle类的变化可能导致计算几何应用程序的变化,编译和测试,反之亦然。那么,怎么修改才能让其符合单一职责原则呢?

答案是:拆分!拆分职责到两个不同的类中,如:
Rectangle: 这个类应该只定义Area()方法;
RectangleUI: 这个类应继承Rectangle类,并定义Draw()方法。

2. 开放封闭原则(OCP)

软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。
(1)通过增加代码来扩展功能,而不是修改已经存在的代码。
(2)若客户模块和服务模块遵循同一个接口来设计,则客户模块可以不关心服务模块的类型,服务模块可以方便扩展服务(代码)。
(3)OCP支持替换的服务,而不用修改客户模块。

示例:

public boolean sendByEmail(String addr, String title, String content) {
 
}
public boolean sendBySMS(String addr, String content) {
 
}
// 在其它地方调用上述方法发送信息
sendByEmail(addr, title, content);
sendBySMS(addr, content);

如果现在又多了一种发送信息的方式,比如可以通过QQ发送信息,那么不仅需要增加一个方法sendByQQ(),还需要在调用它的地方进行修改,违反了OCP原则,更好的方式是

抽象出一个Send接口,里面有个send()方法,然后让SendByEmail和SendBySMS去实现它既可。这样即使多了一个通过QQ发送的请求,那么只要再添加一个SendByQQ实现类实现Send接口既可。这样就不需要修改已有的接口定义和已实现类,很好的遵循了OCP原则。

3. 里氏替换原则(LSP)

当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系

客户模块不应关心服务模块的是如何工作的;同样的接口模块之间,可以在不知道服务模块代码的情况下,进行替换。即接口或父类出现的地方,实现接口的类或子类可以代入。

示例:

public class Rectangle {
    private double width;
    private double height;
 
     public void setWidth(double value) {
         this.width = value;
     }
 
     public double getWidth() {
         return this.width;
     }
 
     public void setHeight(double value) {
         this.width = value;
     }
 
     public double getHeight() {
         return this.height;
     }
 
     public double Area() {
         return this.width*this.height;
     }
}
 
public class Square extends Rectangle {
 
    /* 由于父类Rectangle在设计时没有考虑将来会被Square继承,所以父类中字段width和height都被设成private,在子类Square中就只能调用父类的属性来set/get,具体省略 */
}
 
// 测试
void TestRectangle(Rectangle r) {
    r.Weight=10;
    r.Height=20;
    Assert.AreEqual(10,r.Weight);
    Assert.AreEqual(200,r.Area);
}
 
// 运行良好
Rectangle r = new Rectangle ();
TestRectangle(r);
 
// 现在两个Assert测试都失败了
Square s = new Square();
TestRectangle(s);

LSP让我们得出一个非常重要的结论:一个模型,如果孤立地看,并不具有真正意义上的有效性,模型的有效性只能通过它的客户程序来表现。例如孤立地看Rectangle和Squre,它们时自相容的、有效的;但从对基类Rectangle做了合理假设的客户程序TestRectangle(Rectangle r)看,这个模型就有问题了。在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案,必须要根据该设计的使用者所作出的合理假设来审视它。

目前也有一些技术可以支持我们将合理假设明确化,例如测试驱动开发(Test-Driven Development,TDD)和基于契约设计(Design by Contract,DBC)。但是有谁知道设计的使用者会作出什么样的合理假设呢?大多数这样的假设都很难预料。如果我们预测所有的假设的话,我们设计的 系统可能也会充满不必要的复杂性。推荐的做法是:只预测那些最明显的违反LSP的情况,而推迟对所有其他假设的预测,直到出现相关的脆弱性的臭味(Bad Smell)时,才去处理它们。我觉得这句话还不够直白,Martin Fowler的《Refactoring》一书中“Refused Bequest”(拒收的遗赠)描 述的更详尽:子类继承父类的methods和data,但子类仅仅只需要父类的部分Methods或data,而不是全部methods和data;当这 种情况出现时,就意味这我们的继承体系出现了问题。例如上面的Rectangle和Square,Square本身长和宽相等,几何学中用边长来表示边, 而Rectangle长和宽之分,直观地看,Square已经Refused了Rectangle的Bequest,让Square继承 Rectangle是一个不合理的设计。

现在再回到面向对象的基本概念上,子类继承父类表达的是一种IS-A关系,IS-A关系这种用法被认为是面向对象分析(OOA)基本技术之一。但正方形的 的确确是一个长方形啊,难道它们之间不存在IS-A关系?关于这一点,《Java与模式》一书中的解释是:我们设计继承体系时,子类应该是可替代的父类的,是可替代关系,而不仅仅是IS-A的关系;而PPP一书中的解释是:从行为方式的角度来看,Square不是Rectangle,对象的行为方式才是软件真正所关注的问题;LSP清楚地指出,OOD中IS-A关系时就行为方式而言的,客户程序是可以对行为方式进行合理假设的。其实二者表达的是同一个意思。

4. 接口分离原则(ISP)

不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

客户模块不应该依赖大的接口,应该裁减为小的接口给客户模块使用,以减少依赖性。如Java中一个类实现多个接口,不同的接口给不用的客户模块使用,而不是提供给客户模块一个大的接口。

示例:

public interface Animal {
 
    public void eat();      // 吃
    
    public void sleep();    // 睡
  
    public void crawl();     // 爬
 
    public void run();      // 跑
}
 
public class Snake implements Animal {
 
    public void eat() {
 
    }
    
    public void sleep() {
 
    }
  
    public void crawl() {
 
    }
 
    public void run(){
 
    }
 
}
 
public class Rabit implements Animal {
 
    public void eat() {
 
    }
    
    public void sleep() {
 
    }
  
    public void crawl() {
 
    }
 
    public void run(){
 
    }
 
}

上面的例子,Snake并没有run的行为而Rabbit并没有crawl的行为,而这里它们却必须实现这样不必要的方法,更好的方法是crawl()和run()单独作为一个接口,这需要根据实际情况进行调整,反正不要把什么功能都放在一个大的接口里,而这些功能并不是每个继承该接口的类都所必须的。

5. 依赖注入或倒置原则(DIP)

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

这个设计原则的亮点在于任何被DI框架注入的类很容易用mock对象进行测试和维护,因为对象创建代码集中在框架中,客户端代码也不混乱。有很多方式可以实现依赖倒置,比如像AspectJ等的AOP(Aspect Oriented programming)框架使用的字节码技术,或Spring框架使用的代理等。

(1).高层模块不要依赖低层模块;
(2).高层和低层模块都要依赖于抽象;
(3).抽象不要依赖于具体实现;
(4).具体实现要依赖于抽象;
(5).抽象和接口使模块之间的依赖分离。

先让我们从宏观上来看下,举个例子,我们经常会用到宏观的一种体系结构模式–layer模式,通过层的概念分解和架构系统,比如常见得三层架构等。那么依赖关系应该是自上而下,也就是上层模块依赖于下层模块,而下层模块不依赖于上层,如下图所示。
编程艺术——软件设计模式SOLID原则_第3张图片
这应该还是比较容易理解的,因为越底层的模块相对就越稳定,改动也相对越少,而越上层跟需求耦合度越高,改动也会越频繁,所以自上而下的依赖关系使上层发生变更时,不会影响到下层,降低变更带来的风险,保证系统的稳定。
上面是立足在整体架构层的基础上的结果,再换个角度,从细节上再分析一下,这里我们暂时只关注UI和Service间的关系,如下面这样的依赖关系会有什么样的问题?
编程艺术——软件设计模式SOLID原则_第4张图片
第一,当需要追加提供一种新的Service时,我们不得不对UI层进行改动,增加了额外的工作。
第二,这种改动可能会影响到UI,带来风险。
第三,改动后,UI层和Logic层都必须重新再做Unit testing。

那么具体怎么优化依赖关系才能让模块或层间的耦合更低呢?想想前面讲的OCP原则吧,观点是类似的。
我们可以为Service追加一个抽象层,上层UI不依赖于Service的details,UI和Service同时依赖于这个Service的抽象层。如下图是我们的改进后的结果。
编程艺术——软件设计模式SOLID原则_第5张图片
这样改进后会有什么好处呢?
第一,Service进行扩展时,一般情况下不会影响到UI层,UI不需要改动。
第二,Service进行扩展时,UI层不需要再做Unit testing。

总结:
一个对象只承担一种责任,所有服务接口只通过它来执行这种任务。
程序实体,比如类和对象,向扩展行为开放,向修改行为关闭。
子类应该可以用来替代它所继承的类。
一个类对另一个类的依赖应该限制在最小化的接口上。
依赖抽象层(接口),而不是具体类。
这几条原则是非常基础而且重要的面向对象设计原则。正是由于这些原则的基础性,理解、融汇贯通这些原则需要不少的经验和知识的积累。实习或工作中一定要遵循这些原则,不能让自己写的代码让别人批的一无是处然后胎死腹中。

6.迪米特法则(Law of Demeter)

迪米特法则也叫最少知道原则(Least Knowledge Principle, LKP ),虽然名称不同,但都是同一个意思:一个对象应该对其他对象有最少的了解。

该原则也很好理解,我们在写一个类的时候,应该尽可能的少暴露自己的接口。什么意思呢?就是说,在写类的时候,能不 public 就不 public ,所有暴露的属性或是接口,都是不得不暴露的,这样的话,就能保证其他类对这个类有最少的了解了。

这个原则也没什么需要多讲的,调用者只需要知道被调用者公开的方法就好了,至于它内部是怎么实现的或是有其他别的方法,调用者并不关心,调用者只关心它需要用的。反而,如果被调用者暴露太多不需要暴露的属性或方法,那么就可能导致调用者滥用其中的方法,或是引起一些其他不必要的麻烦。

你可能感兴趣的:(设计模式,算法,数据结构,设计模式)