面向对象编程的solid原则

  1. S - Single-responsibility Principle(单一职责原则)
  2. O - Open-closed Principle(开闭原则)
  3. L - Liskov substitution principle(里式替换原则)
  4. I - Interface segregation principle(接口隔离原则)
  5. DDependency Inversion principle Conclusion(依赖反转原则)

使用solid原则,可使代码易于维护、扩展、测试和重构。html

总的来讲,刚开始看可能很差掌握,可是随着持续的使用和思考,这些原则将会成为你的一部分。java

并且了解这5个原则后,再去看开源库,会发现颇有优秀开源库都默默遵照这些规则。编程

1、单一职责原则(Single responsibility principle)

一个类只负责一项职责。

举例:假若有一个图形数组,计算数组中图形的总面积。json

1. 圆形类设计模式

#import "BXCircle.h"

@implementation BXCircle

- (instancetype)initWithRadius:(CGFloat)radius
{
    self = [super init];
    if (self) {
        self.radius = radius;
    }
    return self;
}

@end复制代码

2. 正方形类数组

#import "BXSquare.h"

- (instancetype)initWithLength:(CGFloat)length
{
    self = [super init];
    if (self) {
        self.length = length;
    }
    return self;
}

@end复制代码

3. 计算面积总和的类bash

#import "BXAreaCalculator.h"

- (instancetype)initWithShapes:(NSArray *)shapes
{
    self = [super init];
    if (self) {
        self.shapes = shapes;
    }
    return self;
}

- (CGFloat)sumOfShapes
{
    CGFloat sum = 0.0;
    for (id shape in _shapes) {
        if ([shape isKindOfClass:[BXCircle class]]) {
            CGFloat radius = (BXCircle *)shape.radius;
            sum = sum + radius * radius * M_PI;
        } else if ([shape isKindOfClass:[BXSquare class]]) {
            CGFloat length = (BXSquare *)shape.lenght;
            sum = sum + length * length;
        }
    }
    return sum;
}

- (NSString *)output
{
    CGFloat sumOfShapes = [self sumOfShapes];
    return [NSString stringWithFormat:@"面积总和为%f", sumOfShapes];
}

@end复制代码

实例化BXAreaCalculator类,传进去一个图形的数组,调用outPut方法就能输出结果。架构

面积总和为8复制代码

而后后面来了个需求:须要用json,html的形式输出结果框架

此时,BXAreaCalculator类就负责了两个职责:1. 计算面积总和;2. 输出各类形式。  测试

这时就与单一职责原则相违背了。BXAreaCalculator类应该只关心面积总和的计算,而不该该关心输出json仍是html。 

有两种修改方法:

  1. 直接在BXAreaCalculator类中添加各类输出方法。(违背SRP原则)
  2. 新建一个专门负责输出的类,在其中添加各类输出方法。(遵循SRP原则)

 因此,我再建立一个BXSumCalculatorOutputter类,专门负责输出各类形式。

#import "BXSumCalculatorOutputter.h"

@implementation BXSumCalculatorOutputter 

- (instancetype)initWithAreas:(CGFloat)areas
{
    self = [super init];
    if (self) {
        _areas = areas;
    }
    return self;
}

#pragma mark - public methods

- (id)JSON
{
    // 输出json
}

- (id)HTML
{
    // 输出HTML
}

- (id)JADE
{
    // 输出JADE
}

- (id)XML
{
    // 输出XML
}

@end复制代码


以上所举的这个例子太简单了,它只有几个方法,因此,不管是在代码级别上违反单一职责原则,仍是在方法级别上违反,都不会形成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而须要修改类时,除非这个类自己很是简单,不然仍是遵循单一职责原则的好。 

遵循单一职责原的优势有: 

  1. 能够下降类的复杂度,一个类只负责一项职责,其逻辑确定要比负责多项职责简单的多
  2. 提升类的可读性,提升系统的可维护性
  3. 变动引发的风险下降,变动是必然的,若是单一职责原则遵照的好,当修改一个功能时,能够显著下降对其余功能的影响。 

2、开闭原则(Open-closed Principle)

This simply means that a class should be easily extendable without modifying the class itself. Let's take a look at the AreaCalculator class, especially it's sum method.
类应该能在不修改类自己的状况下轻松扩展

如今看一下BXAreaCalculator类的计算面积总和的方法,sumOfShapes方法:

- (CGFloat)sumOfShapes
{
    CGFloat sum = 0.0;
    for (id shape in _shapes) {
        if ([shape isKindOfClass:[BXCircle class]]) {
            CGFloat radius = (BXCircle *)shape.radius;
            sum = sum + radius * radius * M_PI;
        } else if ([shape isKindOfClass:[BXSquare class]]) {
            CGFloat length = (BXSquare *)shape.lenght;
            sum = sum + length * length;
        }
    }
    return sum;
}复制代码

若是想要添加其余图形,例如长方形,梯形等等,就要扩展sumOfShapes,在其中添加大量的if else逻辑。

应该将计算每一个图形面积的逻辑从方法中移除,放到他自身的图形类中。

#import "BXCircle.h"

@implementation BXCircle

- (instancetype)initWithRadius:(CGFloat)radius
{
    self = [super init];
    if (self) {
        self.radius = radius;
    }
    return self;
}

- (CGFloat)area
{
    return self.radius * self.radius * M_PI;
}

@end复制代码

#import "BXSquare.h"

- (instancetype)initWithLength:(CGFloat)length
{
    self = [super init];
    if (self) {
        self.length = length;
    }
    return self;
}

- (CGFloat)area
{    
    return self.radius * self.radius * M_PI;
}
@end复制代码

这样一来,BXAreaCalculator中计算总和的方法就会很简单

- (CGFloat)sumOfShapes
{
    // 这里简单这样写
    CGFloat sum = 0.0;
    for (id shape in _shapes) {
        SEL areaFunction = NSSelectorFromString(@"area");
        CGFloat area = ((CGFloat(*)(id,SEL))objc_msgSend)(shape, areaFunction);
        sum = sum + area;
    }
    return sum;
}复制代码

若是再建立长方形、梯形,就不须要修改BXAreaCalculator中的代码了。

可是又出现一个新的问题:咱们怎么知道传给BXAreaCalculator的数组中对象是图形类、有area方法呢?

编写接口是solid一个重要部分

咱们能够建立一个协议,让全部的图形类都去实现它。

#import <Foundation/Foundation.h>

@protocol BXShapeInterface <NSObject>
@required

- (CGFloat)area;

@end复制代码

BXCircle和BXSquare都实现这个协议

@interface BXCircle : NSObject <BXShapeInterface>

- (instancetype)init NS_UNAVAILABLE;

- (instancetype)initWithRadius:(CGFloat)radius NS_DESIGNATED_INITIALIZER;

@end复制代码

@interface BXSquare : NSObject <BXShapeInterface>

- (instancetype)init NS_UNAVAILABLE;

- (instancetype)initWithLength:(CGFloat)length NS_DESIGNATED_INITIALIZER;

@end复制代码

而后规定BXAreaCalculator中传进来的shapes数组中的元素都遵照这个协议

- (instancetype)initWithShapes:(NSArray<id<BXShapeInterface>> *)shapes
{
    self = [super init];
    if (self) {
        _shapes = shapes;
    }
    return self;
}复制代码



开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。由于抽象灵活性好,适应性广,只要抽象的合理,能够基本保持软件架构的稳定。而软件中易变的细节,咱们用从抽象派生的实现类来进行扩展,当软件须要发生变化时,咱们只须要根据需求从新派生一个实现类来扩展就能够了。固然前提是咱们的抽象要合理,要对需求的变动有前瞻性和预见性才行。

3、里氏替换原则(Open-closed Principle)

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
对于 class T 的实例对象 x,可使q(x)成立;那么,对于class T 的子类 classS 的实例对象 y,也要是q(y)成立。(意思是要兼容子类???)

意思是说全部的子类/派生类都应该能够替代他的父类/基类。

确定有很多人跟我刚看到这项原则的时候同样,对这个原则的名字充满疑惑。其实缘由就是这项原则最先是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。

全部引用基类的地方必须能透明地使用其子类的对象。

当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽可能不要重写父类A的方法,也尽可能不要重载父类A的方法。

继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),其实是在设定一系列的规范和契约,虽然它不强制要求全部的子类必须听从这些契约,可是若是子类对这些非抽象方法任意修改,就会对整个继承体系形成破坏。而里氏替换原则就是表达了这一层含义。

继承做为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。好比使用继承会给程序带来侵入性,程序的可移植性下降,增长了对象间的耦合性,若是一个类被其余的类所继承,则当这个类须要修改时,必须考虑到全部的子类,而且父类修改后,全部涉及到子类的功能都有可能会产生故障。

若是咱们想实现 求体积和 的 BXVolumeCalculator类,继承自 BXAreaCalculator类。

- (NSArray *)sumOfShapes
{
    return @[@3, @5, @2];
}复制代码

返回的是一个数组,而不是一个CGFloat类型。因此以后output的时候就会出现bug。

- (CGFloat)sumOfShapes
{
    return @6;
}复制代码

这个例子举得不太好,简而言之就是在继承父类后,不要影响子类具有的父类原有的功能。


在实际编程中,咱们经常会经过重写父类的方法来完成新的功能,这样写起来虽然简单,可是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率很是大。若是非要重写父类的方法,比较通用的作法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

里氏替换原则通俗的来说就是:子类能够扩展父类的功能,但不能改变父类原有的功能。它包含如下4层含义:

  • 子类能够实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中能够增长本身特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

看上去很难以想象,由于咱们会发如今本身编程中经常会违反里氏替换原则,程序照样跑的好好的。因此你们都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?

后果就是:你写的代码出问题的概率将会大大增长。

4、接口隔离原则

A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use.
客户端不该该依赖它不须要的接口;一个类对另外一个类的依赖应该创建在最小的接口上。

将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们须要的接口创建依赖关系。也就是采用接口隔离原则。

未遵循接口隔离原则:


这个图的意思是:类A依赖接口I中的方法一、方法二、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法一、方法四、方法5,类D是对类C依赖的实现。对于类B和类D来讲,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但因为实现了接口I,因此也必需要实现这些用不到的方法。

接口拆分:


接口隔离原则的含义是:创建单一接口,不要创建庞大臃肿的接口,尽可能细化接口,接口中的方法尽可能少。也就是说,咱们要为各个类创建专用的接口,而不要试图去创建一个很庞大的接口供全部依赖它的类去调用。

说到这里,不少人会觉的接口隔离原则跟以前的单一职责原则很类似,其实否则。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序总体框架的构建。

采用接口隔离原则对接口进行约束时,要注意如下几点:

  • 接口尽可能小,可是要有限度。对接口进行细化能够提升程序设计灵活性是不挣的事实,可是若是太小,则会形成接口数量过多,使设计复杂化。因此必定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它须要的方法,它不须要的方法则隐藏起来。只有专一地为一个模块提供定制服务,才能创建最小的依赖关系。
  • 提升内聚,减小对外交互。使接口用最少的方法去完成最多的事情。

运用接口隔离原则,必定要适度,接口设计的过大或太小都很差。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

5、依赖倒置原则

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.
高层模块不该该直接依赖具体的底层模块。二者都应该依赖其抽象,而不是去依赖具体细节。细节应该依赖抽象。

定义:高层模块不该该依赖低层模块,两者都应该依赖其抽象;抽象不该该依赖细节;细节应该依赖抽象。

问题由来:类A直接依赖类B,假如要将类A改成依赖类C,则必须经过修改类A的代码来达成。这种场景下,类A通常是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操做;假如修改类A,会给程序带来没必要要的风险。

解决方案:将类A修改成依赖接口I,类B和类C各自实现接口I,类A经过接口I间接与类B或者类C发生联系,则会大大下降修改类A的概率。

依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操做,把展示细节的任务交给他们的实现类去完成。

依赖倒置原则的核心思想是面向接口编程,咱们依旧用一个例子来讲明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,母亲给孩子讲故事,只要给她一本书,她就能够照着书给孩子讲故事了。代码以下:

class Book{
	public String getContent(){
		return "好久好久之前有一个阿拉伯的故事……";
	}
}

class Mother{
	public void narrate(Book book){
		System.out.println("妈妈开始讲故事");
		System.out.println(book.getContent());
	}
}

public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
	}
} 复制代码

运行结果:

妈妈开始讲故事
好久好久之前有一个阿拉伯的故事……复制代码

运行良好,假若有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码以下:

class Newspaper{
	public String getContent(){
		return "林书豪38+7领导尼克斯击败湖人……";
	}
} 复制代码

这位母亲却办不到,由于她竟然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,竟然必需要修改Mother才能读。假如之后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。缘由就是Mother与Book之间的耦合性过高了,必须下降他们之间的耦合度才行。

咱们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

interface IReader{
	public String getContent();
} 复制代码

Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改成:

class Newspaper implements IReader {
	public String getContent(){
		return "林书豪17+9助尼克斯击败老鹰……";
	}
}
class Book implements IReader{
	public String getContent(){
		return "好久好久之前有一个阿拉伯的故事……";
	}
}

class Mother{
	public void narrate(IReader reader){
		System.out.println("妈妈开始讲故事");
		System.out.println(reader.getContent());
	}
}

public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
		mother.narrate(new Newspaper());
	}
}复制代码

运行结果:

运行结果:
妈妈开始讲故事
好久好久之前有一个阿拉伯的故事……
妈妈开始讲故事
林书豪17+9助尼克斯击败老鹰……复制代码

这样修改后,不管之后怎样扩展Client类,都不须要再修改Mother类了。这只是一个简单的例子,实际状况中,表明高层模块的Mother类将负责完成主要的业务逻辑,一旦须要对它进行修改,引入错误的风险极大。因此遵循依赖倒置原则能够下降类之间的耦合性,提升系统的稳定性,下降修改程序形成的风险。

采用依赖倒置原则给多人并行开发带来了极大的便利,好比上例中,本来Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才能够进行编码,由于Mother类依赖于Book类。修改后的程序则能够同时开工,互不影响,由于Mother与Book类一点关系也没有。参与协做开发的人越多、项目越庞大,采用依赖致使原则的意义就越重大。如今很流行的TDD开发模式就是依赖倒置原则最成功的应用。

传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信用过Spring框架的,对依赖的传递方式必定不会陌生。

在实际编程中,咱们通常须要作到以下3点:

  • 低层模块尽可能都要有抽象类或接口,或者二者都有。
  • 变量的声明类型尽可能是抽象类或接口。
  • 使用继承时遵循里氏替换原则。

依赖倒置原则的核心就是要咱们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

6、总结

说到这里,再回想一下前面说的5项原则,偏偏是告诉咱们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉咱们实现类要职责单一;里氏替换原则告诉咱们不要破坏继承体系;依赖倒置原则告诉咱们要面向接口编程;接口隔离原则告诉咱们在设计接口的时候要精简单一;迪米特法则告诉咱们要下降耦合。而开闭原则是总纲,他告诉咱们要对扩展开放,对修改关闭。

最后说明一下如何去遵照这六个原则。对这六个原则的遵照并非是和否的问题,而是多和少的问题,也就是说,咱们通常不会说有没有遵照,而是说遵照程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是同样,制定这六个原则的目的并非要咱们刻板的遵照他们,而须要根据实际状况灵活运用。对他们的遵照程度只要在一个合理的范围内,就算是良好的设计。咱们用一幅图来讲明一下。


图中的每一条维度各表明一项原则,咱们依据对这项原则的遵照程度在维度上画一个点,则若是对这项原则遵照的合理的话,这个点应该落在红色的同心圆内部;若是遵照的差,点将会在小圆内部;若是过分遵照,点将会落在大圆外部。一个良好的设计体如今图中,应该是六个顶点都在同心圆中的六边形。


在上图中,设计一、设计2属于良好的设计,他们对六项原则的遵照程度都在合理的范围内;设计三、设计4设计虽然有些不足,但也基本能够接受;设计5则严重不足,对各项原则都没有很好的遵照;而设计6则遵照过渡了,设计5和设计6都是迫切须要重构的设计。

参考

scotch.io/bar-talk/s-…

www.uml.org.cn/sjms/201211…

相关文章
相关标签/搜索