面向对象设计的六大设计原则(附 Demo & UML类图)

学习初衷与讲解方式

笔者想在 iOS 从业第三年结束以前系统学习一下关于设计模式方面的知识。而在学习设计模式以前,以为更有必要先学习面向对象设计(OOD:Object Oriented Design)的几大设计原则,为后面设计模式的学习打下基础。前端

本篇分享的就是笔者近阶段学习和总结的面向对象设计的六个设计原则:git

缩写 英文名称 中文名称
SRP Single Responsibility Principle 单一职责原则
OCP Open Close Principle 开闭原则
LSP Liskov Substitution Principle 里氏替换原则
LoD Law of Demeter ( Least Knowledge Principle) 迪米特法则(最少知道原则)
ISP Interface Segregation Principle 接口分离原则
DIP Dependency Inversion Principle 依赖倒置原则

注意,一般所说的SOLID(上方表格缩写的首字母,从上到下)设计原则没有包含本篇介绍的迪米特法则,而只有其余五项。另外,本篇不包含合成/聚合复用原则(CARP),由于笔者认为该原则没有其余六个原则典型,并且在实践中也不容易违背。有兴趣的同窗能够自行查资料学习。程序员

在下一章节笔者将分别讲解这些设计原则,讲解的方式是将概念与代码及其对应的UML 类图结合起来说解的方式。github

代码的语言使用的是笔者最熟悉的Objective-C语言。虽然是一个比较小众的语言,可是由于有 UML 类图的帮助,并且主流的面向对象语言关于类,接口(Objective-C里面是协议)的使用在形式上相似,因此笔者相信语言的小众不会对知识的理解产生太大的阻力。编程

另外,在每一个设计模式的讲解里,笔者会首先描述一个应用场景(需求点),接着用两种设计的代码来进行对比讲解:先提供相对很差的设计的代码,再提供相对好的设计的代码。并且两种代码都会附上标准的 UML 类图来进行更形象地对比,帮助你们来理解。同时也能够帮助不了解 UML 类图的读者先简单熟悉一下 UML 类图的语法。后端

六大设计原则

本篇讲解六大设计原则的顺序大体按照难易程序排列。在这里最早讲解开闭原则,由于其在理解上比较简单,并且也是其余设计原则的基石。设计模式

注意:数组

  1. 六个原则的讲解所用的例子之间并无关联,因此阅读顺序能够按照读者的喜爱来定。
  2. Java语言里的接口在Objective-C里面叫作协议。虽然Demo是用Objective-C写的,可是由于协议的叫法比较小众,故后面一概用接口代替协议这个说法。

原则一:开闭原则(Open Close Principle)

定义

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.架构

即:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。框架

定义的解读

  • 用抽象构建框架,用实现扩展细节。
  • 不以改动原有类的方式来实现新需求,而是应该以实现事先抽象出来的接口(或具体类继承抽象类)的方式来实现。

优势

实践开闭原则的优势在于能够在不改动原有代码的前提下给程序扩展功能。增长了程序的可扩展性,同时也下降了程序的维护成本。

代码讲解

下面经过一个简单的关于在线课程的例子讲解一下开闭原则的实践。

需求点

设计一个在线课程类:

因为教学资源有限,开始的时候只有相似于博客的,经过文字讲解的课程。 可是随着教学资源的增多,后来增长了视频课程,音频课程以及直播课程。

先来看一下很差的设计:

很差的设计

最开始的文字课程类:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //课程名称
@property (nonatomic, copy) NSString *courseIntroduction;  //课程介绍
@property (nonatomic, copy) NSString *teacherName;         //讲师姓名
@property (nonatomic, copy) NSString *content;             //课程内容

@end
复制代码

Course类声明了最初的在线课程所须要包含的数据:

  • 课程名称
  • 课程介绍
  • 讲师姓名
  • 文字内容

接着按照上面所说的需求变动:增长了视频,音频,直播课程:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //课程名称
@property (nonatomic, copy) NSString *courseIntroduction;  //课程介绍
@property (nonatomic, copy) NSString *teacherName;         //讲师姓名
@property (nonatomic, copy) NSString *content;             //文字内容


//新需求:视频课程
@property (nonatomic, copy) NSString *videoUrl;

//新需求:音频课程
@property (nonatomic, copy) NSString *audioUrl;

//新需求:直播课程
@property (nonatomic, copy) NSString *liveUrl;

@end
复制代码

三种新增的课程都在原Course类中添加了对应的url。也就是每次添加一个新的类型的课程,都在原有Course类里面修改:新增这种课程须要的数据。

这就致使:咱们从Course类实例化的视频课程对象会包含并不属于本身的数据:audioUrlliveUrl:这样就形成了冗余,视频课程对象并非纯粹的视频课程对象,它包含了音频地址,直播地址等成员。

很显然,这个设计不是一个好的设计,由于(对应上面两段叙述):

  1. 随着需求的增长,须要反复修改以前建立的类。
  2. 给新增的类形成了没必要要的冗余。

之因此会形成上述两个缺陷,是由于该设计没有遵循对修改关闭,对扩展开放的开闭原则,而是反其道而行之:开放修改,并且不给扩展提供便利。

难么怎么作能够遵循开闭原则呢?下面看一下遵循开闭原则的较好的设计:

较好的设计

首先在Course类中仅仅保留全部课程都含有的数据:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //课程名称
@property (nonatomic, copy) NSString *courseIntroduction;  //课程介绍
@property (nonatomic, copy) NSString *teacherName;         //讲师姓名
复制代码

接着,针对文字课程,视频课程,音频课程,直播课程这三种新型的课程采用继承Course类的方式。并且继承后,添加本身独有的数据:

文字课程类:

//================== TextCourse.h ==================

@interface TextCourse : Course

@property (nonatomic, copy) NSString *content;             //文字内容

@end
复制代码

视频课程类:

//================== VideoCourse.h ==================

@interface VideoCourse : Course

@property (nonatomic, copy) NSString *videoUrl;            //视频地址

@end
复制代码

音频课程类:

//================== AudioCourse.h ==================

@interface AudioCourse : Course

@property (nonatomic, copy) NSString *audioUrl;            //音频地址

@end
复制代码

直播课程类:

//================== LiveCourse.h ==================

@interface LiveCourse : Course

@property (nonatomic, copy) NSString *liveUrl;             //直播地址

@end
复制代码

这样一来,上面的两个问题都获得了解决:

  1. 随着课程类型的增长,不须要反复修改最初的父类(Course),只须要新建一个继承于它的子类并在子类中添加仅属于该子类的数据(或行为)便可。
  2. 由于各类课程独有的数据(或行为)都被分散到了不一样的课程子类里,因此每一个子类的数据(或行为)没有任何冗余。

并且对于第二点:或许从此的视频课程能够有高清地址,视频加速功能。而这些功能只须要在VideoCourse类里添加便可,由于它们都是视频课程所独有的。一样地,直播课程后面还能够支持在线问答功能,也能够仅加在LiveCourse里面。

咱们能够看到,正是因为最初程序设计合理,因此对后面需求的增长才会处理得很好。

下面来看一下这两个设计的UML 类图,能够更形象地看出两种设计上的区别:

UML 类图对比

未实践开闭原则:

未实践开闭原则

实践了开闭原则:

实践了开闭原则

在实践了开闭原则的 UML 类图中,四个课程类继承了Course类并添加了本身独有的属性。(在 UML 类图中:实线空心三角箭头表明继承关系:由子类指向其父类)

如何实践

为了更好地实践开闭原则,在设计之初就要想清楚在该场景里哪些数据(或行为)是必定不变(或很难再改变)的,哪些是很容易变更的。将后者抽象成接口或抽象方法,以便于在未来经过创造具体的实现应对不一样的需求。

原则二:单一职责原则(Single Responsibility Principle)

定义

A class should have a single responsibility, where a responsibility is nothing but a reason to change.

即:一个类只容许有一个职责,即只有一个致使该类变动的缘由。

定义的解读

  • 类职责的变化每每就是致使类变化的缘由:也就是说若是一个类具备多种职责,就会有多种致使这个类变化的缘由,从而致使这个类的维护变得困难。

  • 每每在软件开发中随着需求的不断增长,可能会给原来的类添加一些原本不属于它的一些职责,从而违反了单一职责原则。若是咱们发现当前类的职责不只仅有一个,就应该将原本不属于该类真正的职责分离出去。

  • 不只仅是类,函数(方法)也要遵循单一职责原则,即:一个函数(方法)只作一件事情。若是发现一个函数(方法)里面有不一样的任务,则须要将不一样的任务以另外一个函数(方法)的形式分离出去。

优势

若是类与方法的职责划分得很清晰,不但能够提升代码的可读性,更实际性地更下降了程序出错的风险,由于清晰的代码会让bug无处藏身,也有利于bug的追踪,也就是下降了程序的维护成本。

代码讲解

单一职责原则的demo比较简单,经过对象(属性)的设计上讲解已经足够,不须要具体的客户端调用。咱们先看一下需求点:

需求点

初始需求:须要创造一个员工类,这个类有员工的一些基本信息。

新需求:增长两个方法:

  • 断定员工在今年是否升职
  • 计算员工的薪水

先来看一下很差的设计:

很差的设计

//================== Employee.h ==================

@interface Employee : NSObject

//============ 初始需求 ============
@property (nonatomic, copy) NSString *name;       //员工姓名
@property (nonatomic, copy) NSString *address;    //员工住址
@property (nonatomic, copy) NSString *employeeID; //员工ID
 
 
 
//============ 新需求 ============
//计算薪水
- (double)calculateSalary;

//今年是否晋升
- (BOOL)willGetPromotionThisYear;

@end
复制代码

由上面的代码能够看出:

  • 在初始需求下,咱们建立了Employee这个员工类,并声明了3个员工信息的属性:员工姓名,地址,员工ID。
  • 在新需求下,两个方法直接加到了员工类里面。

新需求的作法看似没有问题,由于都是和员工有关的,但却违反了单一职责原则:由于这两个方法并非员工自己的职责

  • calculateSalary这个方法的职责是属于会计部门的:薪水的计算是会计部门负责。
  • willPromotionThisYear这个方法的职责是属于人事部门的:考核与晋升机制是人事部门负责。

而上面的设计将原本不属于员工本身的职责强加进了员工类里面,而这个类的设计初衷(原始职责)就是单纯地保留员工的一些信息而已。所以这么作就是给这个类引入了新的职责,故此设计违反了单一职责原则

咱们能够简单想象一下这么作的后果是什么:若是员工的晋升机制变了,或者税收政策等影响员工工资的因素变了,咱们还须要修改当前这个类。

那么怎么作才能不违反单一职责原则呢?- 咱们须要将这两个方法(责任)分离出去,让本应该处理这类任务的类来处理。

较好的设计

咱们保留员工类的基本信息:

//================== Employee.h ==================

@interface Employee : NSObject

//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;
复制代码

接着建立新的会计部门类:

//================== FinancialApartment.h ==================

#import "Employee.h"

//会计部门类
@interface FinancialApartment : NSObject

//计算薪水
- (double)calculateSalary:(Employee *)employee;

@end
复制代码

人事部门类:

//================== HRApartment.h ==================

#import "Employee.h"

//人事部门类
@interface HRApartment : NSObject

//今年是否晋升
- (BOOL)willGetPromotionThisYear:(Employee*)employee;

@end
复制代码

经过建立了两个分别专门处理薪水和晋升的部门,会计部门和人事部门的类:FinancialApartmentHRApartment,把两个任务(责任)分离了出去,让本该处理这些职责的类来处理这些职责。

这样一来,不只仅在这次新需求中知足了单一职责原则,之后若是还要增长人事部门和会计部门处理的任务,就能够直接在这两个类里面添加便可。

下面来看一下这两个设计的UML 类图,能够更形象地看出两种设计上的区别:

UML 类图对比

未实践单一职责原则:

未实践单一职责原则

实践了单一职责原则:

实践了单一职责原则

能够看到,在实践了单一职责原则的 UML 类图中,不属于Employee的两个职责被分类了FinancialApartment类 和 HRApartment类。(在 UML 类图中,虚线箭头表示依赖关系,经常使用在方法参数等,由依赖方指向被依赖方)

上面说过除了类要遵循单一职责设计原则以外,在函数(方法)的设计上也要遵循单一职责的设计原则。因函数(方法)的单一职责原则理解起来比较容易,故在这里就不提供Demo和UML 类图了。

能够简单举一个例子:

APP的默认导航栏的样式是这样的:

  • 白色底
  • 黑色标题
  • 底部有阴影

那么建立默认导航栏的伪代码多是这样子的:

//默认样式的导航栏
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{
    
    //create white color background view
    
    //create black color title
    
    //create shadow bottom
}
复制代码

如今咱们能够用这个方法统一建立默认的导航栏了。 可是过不久又有新的需求来了,有的页面的导航栏须要作成透明的,所以须要一个透明样式的导航栏:

  • 透明底
  • 白色标题
  • 底部无阴影

针对这个需求,咱们能够新增一个方法:

//透明样式的导航栏
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{
    
    //create transparent color background view
    
    //create white color title
}
复制代码

看出问题来了么?在这两个方法里面,创造background view和 title color title的方法的差异仅仅是颜色不一样而已,而其余部分的代码是重复的。 所以咱们应该将这两个方法抽出来:

//根据传入的颜色参数设置导航栏的背景色
- (void)createBackgroundViewWithColor:(UIColor)color;

//根据传入的标题字符串和颜色参数设置标题
- (void)createTitlewWithColorWithTitle:(NSString *)title color:(UIColor)color;
复制代码

并且上面的制造阴影的部分也能够做为方法抽出来:

- (void)createShadowBottom;
复制代码

这样一来,原来的两个方法能够写成:

//默认样式的导航栏
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{
    
    //设置白色背景
    [self createBackgroundViewWithColor:[UIColor whiteColor]];
    
    //设置黑色标题
    [self createTitlewWithColorWithTitle:title color:[UIColor blackColor]];
    
    //设置底部阴影
    [self createShadowBottom];
}


//透明样式的导航栏
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{
    
    //设置透明背景
    [self createBackgroundViewWithColor:[UIColor clearColor]];
    
    //设置白色标题
    [self createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];
}
复制代码

并且咱们也能够将里面的方法拿出来在外面调用也能够:

设置默认样式的导航栏:

//设置白色背景
[navigationBar createBackgroundViewWithColor:[UIColor whiteColor]];

//设置黑色标题
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor blackColor]];

//设置阴影
[navigationBar createShadowBottom];
复制代码

设置透明样式的导航栏:

//设置透明色背景
[navigationBar createBackgroundViewWithColor:[UIColor clearColor]];

//设置白色标题
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];
复制代码

这样一来,不管写在一个大方法里面调用或是分别在外面调用,都能很清楚地看到导航栏的每一个元素是如何生成的,由于每一个职责都分配到了一个单独的方法里面。并且还有一个好处是,透明导航栏若是遇到浅色背景的话,使用白色字体不如使用黑色字体好,因此遇到这种状况咱们能够在createTitlewWithColorWithTitle:color:方法里面传入黑色色值。 并且从此可能还会有更多的导航栏样式,那么咱们只须要分别改变传入的色值便可,不须要有大量的重复代码了,修改起来也很方便。

如何实践

对于上面的员工类的例子,或许是由于咱们先入为主,知道一个公司的合理组织架构,以为这么设计理所固然。可是在实际开发中,咱们很容易会将不一样的责任揉在一块儿,这点仍是须要开发者注意的。

原则三:依赖倒置原则(Dependency Inversion Principle)

定义

  • Depend upon Abstractions. Do not depend upon concretions.
  • Abstractions should not depend upon details. Details should depend upon abstractions
  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

即:

  • 依赖抽象,而不是依赖实现。
  • 抽象不该该依赖细节;细节应该依赖抽象。
  • 高层模块不能依赖低层模块,两者都应该依赖抽象。

定义解读

  • 针对接口编程,而不是针对实现编程。
  • 尽可能不要从具体的类派生,而是以继承抽象类或实现接口来实现。
  • 关于高层模块与低层模块的划分能够按照决策能力的高低进行划分。业务层天然就处于上层模块,逻辑层和数据层天然就归类为底层。

优势

经过抽象来搭建框架,创建类和类的关联,以减小类间的耦合性。并且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。

代码讲解

下面经过一个模拟项目开发的例子来说解依赖倒置原则。

需求点

实现下面这样的需求:

用代码模拟一个实际项目开发的场景:前端和后端开发人员开发同一个项目。

很差的设计

首先生成两个类,分别对应前端和后端开发者:

前端开发者:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject

- (void)writeJavaScriptCode;

@end



//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeJavaScriptCode{
    NSLog(@"Write JavaScript code");
}

@end
复制代码

后端开发者:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject

- (void)writeJavaCode;

@end



//================== BackEndDeveloper.m ==================

@implementation BackEndDeveloper

- (void)writeJavaCode{
    NSLog(@"Write Java code");
}
@end
复制代码

这两个开发者分别对外提供了本身开发的方法:writeJavaScriptCodewriteJavaCode

接着建立一个Project类:

//================== Project.h ==================

@interface Project : NSObject

//构造方法,传入开发者的数组
- (instancetype)initWithDevelopers:(NSArray *)developers;

//开始开发
- (void)startDeveloping;

@end



//================== Project.m ==================

#import "Project.h"
#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray *_developers;
}


- (instancetype)initWithDevelopers:(NSArray *)developers{
    
    if (self = [super init]) {
        _developers = developers;
    }
    return self;
}



- (void)startDeveloping{
    
    [_developers enumerateObjectsUsingBlock:^(id  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
        
        if ([developer isKindOfClass:[FrondEndDeveloper class]]) {
            
            [developer writeJavaScriptCode];
            
        }else if ([developer isKindOfClass:[BackEndDeveloper class]]){
            
            [developer writeJavaCode];
            
        }else{
            //no such developer
        }
    }];
}

@end
复制代码

Project类中,咱们首先经过一个构造器方法,将开发者的数组传入project的实例对象。而后在开始开发的方法startDeveloping里面,遍历数组并判断元素类型的方式让不一样类型的开发者调用和本身对应的函数。

思考一下,这样的设计有什么问题?

问题一:

假如后台的开发语言改为了GO语言,那么上述代码须要改动两个地方:

  • BackEndDeveloper:须要向外提供一个writeGolangCode方法。
  • Project类的startDeveloping方法里面须要将BackEndDeveloper类的writeJavaCode改为writeGolangCode

问题二:

假如后期老板要求作移动端的APP(须要iOS和安卓的开发者),那么上述代码仍然须要改动两个地方:

  • 还须要给Project类的构造器方法里面传入IOSDeveloperAndroidDeveloper两个类。并且按照现有的设计,还要分别向外部提供writeSwiftCodewriteKotlinCode
  • Project类的startDeveloping方法里面须要再多两个elseif判断,专门判断IOSDeveloperAndroidDeveloper这两个类。

开发安卓的代码也能够用Java,可是为了和后台的开发代码区分一下,这里用了一样能够开发安卓的Kotlin语言。

很显然,在这两种假设的场景下,高层模块(Project)都依赖了低层模块(BackEndDeveloper)的改动,所以上述设计不符合依赖倒置原则

那么该如何设计才能够符合依赖倒置原则呢?

答案是将开发者写代码的方法抽象出来,让Project类再也不依赖全部低层的开发者类的具体实现,而是依赖抽象。并且从下至上,全部底层的开发者类也都依赖这个抽象,经过实现这个抽象来作本身的任务

这个抽象能够用接口,也能够用抽象类的方式来作,在这里笔者用使用接口的方式进行讲解:

较好的设计

首先,建立一个接口,接口里面有一个写代码的方法writeCode

//================== DeveloperProtocol.h ==================

@protocol DeveloperProtocol <NSObject>

- (void)writeCode;

@end
复制代码

而后,让前端程序员和后端程序员类实现这个接口(遵循这个协议)并按照本身的方式实现:

前端程序员类:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject<DeveloperProtocol>
@end



//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeCode{
    NSLog(@"Write JavaScript code");
}
@end
复制代码

后端程序员类:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject<DeveloperProtocol>
@end



//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{
    NSLog(@"Write Java code");
}
@end
复制代码

最后咱们看一下新设计后的Project类:

//================== Project.h ==================

#import "DeveloperProtocol.h"

@interface Project : NSObject

//只需传入遵循DeveloperProtocol的对象数组便可
- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers;

//开始开发
- (void)startDeveloping;

@end


//================== Project.m ==================

#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray <id <DeveloperProtocol>>* _developers;
}


- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers{
    
    if (self = [super init]) {
        _developers = developers;
    }
    return self;
    
}


- (void)startDeveloping{
    
    //每次循环,直接向对象发送writeCode方法便可,不须要判断
    [_developers enumerateObjectsUsingBlock:^(id<DeveloperProtocol>  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
        
        [developer writeCode];
    }];
    
}

@end
复制代码

新的Project的构造方法只需传入遵循DeveloperProtocol协议的对象构成的数组便可。这样也比较符合现实中的需求:只须要会写代码就能够加入到项目中。

而新的startDeveloping方法里:每次循环,直接向当前对象发送writeCode方法便可,不须要对程序员的类型作判断。由于这个对象必定是遵循DeveloperProtocol接口的,而遵循该接口的对象必定会实现writeCode方法(就算不实现也不会引发重大错误)。

如今新的设计接受完了,咱们经过上面假设的两个状况来和以前的设计作个对比:

假设1:后台的开发语言改为了GO语言

在这种状况下,只需更改BackEndDeveloper类里面对于DeveloperProtocol接口的writeCode方法的实现便可:

//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{

    //Old:
    //NSLog(@"Write Java code");
    
    //New:
    NSLog(@"Write Golang code");
}
@end
复制代码

而在Project里面不须要修改任何代码,由于Project类只依赖了接口方法WriteCode,没有依赖其具体的实现。

咱们接着看一下第二个假设:

假设2:后期老板要求作移动端的APP(须要iOS和安卓的开发者)

在这个新场景下,咱们只须要将新建立的两个开发者类:IOSDeveloperAndroidDeveloper分别实现DeveloperProtocol接口的writeCode方法便可。

一样,Project的接口和实现代码都不用修改:客户端只须要在Project的构建方法的数组参数里面添加这两个新类的实例便可,不须要在startDeveloping方法里面添加类型判断,缘由同上。

咱们能够看到,新设计很好地在高层类(Project)与低层类(各类developer类)中间加了一层抽象,解除了两者在旧设计中的耦合,使得在低层类中的改动没有影响到高层类。

一样是抽象,新设计一样也能够用抽象类的方式:建立一个Developer的抽象类并提供一个writeCode方法,让不一样的开发者类继承与它并按照本身的方式实现writeCode方法。这样一来,在Project类的构造方法就是传入已Developer类型为元素的数组了。有兴趣的小伙伴能够本身实现一下~

下面来看一下这两个设计的UML 类图,能够更形象地看出两种设计上的区别:

UML 类图对比

未实践依赖倒置原则:

未实践依赖倒置原则

实践了依赖倒置原则:

实践了依赖倒置原则

在实践了依赖倒置原则的 UML 类图中,咱们能够看到Project仅仅依赖于新的接口;并且低层的FrondEndDevelopeBackEndDevelope类按照本身的方式实现了这个接口:经过接口解除了原有的依赖。(在 UML 类图中,虚线三角箭头表示接口实线,由实现方指向接口)

如何实践

从此在处理高低层模块(类)交互的情景时,尽可能将两者的依赖经过抽象的方式解除掉,实现方式能够是经过接口也能够是抽象类的方式。

原则四:接口分离原则(Interface Segregation Principle)

定义

Many client specific interfaces are better than one general purpose interface.

即:多个特定的客户端接口要好于一个通用性的总接口。

定义解读

  • 客户端不该该依赖它不须要实现的接口。
  • 不创建庞大臃肿的接口,应尽可能细化接口,接口中的方法应该尽可能少。

须要注意的是:接口的粒度也不能过小。若是太小,则会形成接口数量过多,使设计复杂化。

优势

避免同一个接口里面包含不一样类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。

代码讲解

下面经过一个餐厅服务的例子讲解一下接口分离原则。

需求点

如今的餐厅除了提供传统的店内服务,多数也都支持网上下单,网上支付功能。写一些接口方法来涵盖餐厅的全部的下单及支付功能。

很差的设计

//================== RestaurantProtocol.h ==================

@protocol RestaurantProtocol <NSObject>

- (void)placeOnlineOrder;         //下订单:online
- (void)placeTelephoneOrder;      //下订单:经过电话
- (void)placeWalkInCustomerOrder; //下订单:在店里

- (void)payOnline;                //支付订单:online
- (void)payInPerson;              //支付订单:在店里支付

@end
复制代码

在这里声明了一个接口,它包含了下单和支付的几种方式:

  • 下单:

    • online下单
    • 电话下单
    • 店里下单(店内服务)
  • 支付

    • online支付(适用于online下单和电话下单的顾客)
    • 店里支付(店内服务)

这里先不讨论电话下单的顾客是用online支付仍是店内支付。

对应的,咱们有三种下单方式的顾客:

1.online下单,online支付的顾客

//================== OnlineClient.h ==================

#import "RestaurantProtocol.h"

@interface OnlineClient : NSObject<RestaurantProtocol>
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOnlineOrder{
    NSLog(@"place on line order");
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}
@end

复制代码

2.电话下单,online支付的顾客

//================== TelephoneClient.h ==================

#import "RestaurantProtocol.h"

@interface TelephoneClient : NSObject<RestaurantProtocol>
@end



//================== TelephoneClient.m ==================

@implementation TelephoneClient

- (void)placeOnlineOrder{
    //not necessarily
}

- (void)placeTelephoneOrder{
    NSLog(@"place telephone order");
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}

@end
复制代码

3.在店里下单并支付的顾客:

//================== WalkinClient.h ==================

#import "RestaurantProtocol.h"

@interface WalkinClient : NSObject<RestaurantProtocol>
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOnlineOrder{
   //not necessarily
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOnline{
   //not necessarily
}

- (void)payInPerson{
    NSLog(@"pay in person");
}

@end
复制代码

咱们发现,并非全部顾客都必需要实现RestaurantProtocol里面的全部方法。因为接口方法的设计形成了冗余,所以该设计不符合接口隔离原则

注意,Objective-C中的协议能够经过@optional关键字设置不须要必须实现的方法,该特性不与接口分离原则冲突:只要属于同一类责任的接口,均可以放入同一接口中。

那么如何作才符合接口隔离原则呢?咱们来看一下较好的设计。

较好的设计

要符合接口隔离原则,只须要将不一样类型的接口分离出来便可。咱们将原来的RestaurantProtocol接口拆分红两个接口:下单接口和支付接口。

下单接口:

//================== RestaurantPlaceOrderProtocol.h ==================

@protocol RestaurantPlaceOrderProtocol <NSObject>

- (void)placeOrder;

@end
复制代码

支付接口:

//================== RestaurantPaymentProtocol.h ==================

@protocol RestaurantPaymentProtocol <NSObject>

- (void)payOrder;

@end
复制代码

如今有了下单接口和支付接口,咱们就可让不一样的客户来以本身的方式实现下单和支付操做了:

首先建立一个全部客户的父类,来遵循这个两个接口:

//================== Client.h ==================

#import "RestaurantPlaceOrderProtocol.h"
#import "RestaurantPaymentProtocol.h"

@interface Client : NSObject<RestaurantPlaceOrderProtocol,RestaurantPaymentProtocol>
@end
复制代码

接着另online下单,电话下单,店内下单的顾客继承这个父类,分别实现这两个接口的方法:

1.online下单,online支付的顾客

//================== OnlineClient.h ==================

#import "Client.h"
@interface OnlineClient : Client
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOrder{
    NSLog(@"place on line order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
复制代码

2.电话下单,online支付的顾客

//================== TelephoneClient.h ==================
#import "Client.h"
@interface TelephoneClient : Client
@end



//================== TelephoneClient.m ==================
@implementation TelephoneClient

- (void)placeOrder{
    NSLog(@"place telephone order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
复制代码

3.在店里下单并支付顾客:

//================== WalkinClient.h ==================

#import "Client.h"
@interface WalkinClient : Client
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOrder{
    NSLog(@"pay in person");
}

@end
复制代码

由于咱们把不一样职责的接口拆开,使得接口的责任更加清晰,简洁明了。不一样的客户端能够根据本身的需求遵循所须要的接口来以本身的方式实现。

并且从此若是还有和下单或者支付相关的方法,也能够分别加入到各自的接口中,避免了接口的臃肿,同时也提升了程序的内聚性。

下面来看一下这两个设计的UML 类图,能够更形象地看出两种设计上的区别:

UML 类图对比

未实践接口分离原则:

未实践接口分离原则

实践了接口分离原则:

实践了接口分离原则

经过遵照接口分离原则,接口的设计变得更加简洁,并且各类客户类不须要实现本身不须要实现的接口。

如何实践

在设计接口时,尤为是在向现有的接口添加方法时,咱们须要仔细斟酌这些方法是不是处理同一类任务的:若是是则能够放在一块儿;若是不是则须要作拆分。

作iOS开发的朋友对UITableViewUITableViewDelegateUITableViewDataSource这两个协议应该会很是熟悉。这两个协议里的方法都是与UITableView相关的,但iOS SDK的设计者却把这些方法放在不一样的两个协议中。缘由就是这两个协议所包含的方法所处理的任务是不一样的两种:

  • UITableViewDelegate:含有的方法是UITableView的实例告知其代理一些点击事件的方法,即事件的传递,方向是从UITableView的实例到其代理。
  • UITableViewDataSource:含有的方法是UITableView的代理传给UITableView一些必要数据供UITableView展现出来,即数据的传递,方向是从UITableView的代理到UITableView

很显然,UITableView协议的设计者很好地实践了接口分离的原则,值得咱们你们学习。

原则五:迪米特法则(Law of Demeter)

定义

You only ask for objects which you directly need.

即:一个对象应该对尽量少的对象有接触,也就是只接触那些真正须要接触的对象。

定义解读

  • 迪米特法则也叫作最少知道原则(Least Know Principle), 一个类应该只和它的成员变量,方法的输入,返回参数中的类做交流,而不该该引入其余的类(间接交流)。

优势

实践迪米特法则能够良好地下降类与类之间的耦合,减小类与类之间的关联程度,让类与类之间的协做更加直接。

代码讲解

下面经过一个简单的关于汽车的例子来说解一下迪米特法则。

需求点

设计一个汽车类,包含汽车的品牌名称,引擎等成员变量。提供一个方法返回引擎的品牌名称。

很差的设计

Car类:

//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject

//构造方法
- (instancetype)initWithEngine:(GasEngine *)engine;

//返回私有成员变量:引擎的实例
- (GasEngine *)usingEngine;

@end




//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car
{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{
    
    self = [super init];
    
    if (self) {
        _engine = engine;
    }
    return self;
}

- (GasEngine *)usingEngine{
    
    return _engine;
}

@end
复制代码

从上面能够看出,Car的构造方法须要传入一个引擎的实例对象。并且由于引擎的实例对象被赋到了Car对象的私有成员变量里面。因此Car类给外部提供了一个返回引擎对象的方法:usingEngine

而这个引擎类GasEngine有一个品牌名称的成员变量brandName

//================== GasEngine.h ==================
@interface GasEngine : NSObject

@property (nonatomic, copy) NSString *brandName;

@end

复制代码

这样一来,客户端就能够拿到引擎的品牌名称了:

//================== Client.m ==================

#import "GasEngine.h"
#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{

    GasEngine *engine = [car usingEngine];
    NSString *engineBrandName = engine.brandName;//获取到了引擎的品牌名称
    return engineBrandName;
}
复制代码

上面的设计完成了需求,可是却违反了迪米特法则。缘由是在客户端的findCarEngineBrandName:中引入了和入参(Car)和返回值(NSString)无关的GasEngine对象。增长了客户端与 GasEngine的耦合。而这个耦合显然是没必要要更是能够避免的。

接下来咱们看一下如何设计能够避免这种耦合:

较好的设计

一样是Car这个类,咱们去掉原有的返回引擎对象的方法,而是增长一个直接返回引擎品牌名称的方法:

//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject

//构造方法
- (instancetype)initWithEngine:(GasEngine *)engine;

//直接返回引擎品牌名称
- (NSString *)usingEngineBrandName;

@end


//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car
{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{
    
    self = [super init];
    
    if (self) {
        _engine = engine;
    }
    return self;
}


- (NSString *)usingEngineBrandName{
    return _engine.brand;
}

@end
复制代码

由于直接usingEngineBrandName直接返回了引擎的品牌名称,因此在客户端里面就能够直接拿到这个值,而不须要间接地经过原来的GasEngine实例来获取。

咱们看一下客户端操做的变化:

//================== Client.m ==================

#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{
    
    NSString *engineBrandName = [car usingEngineBrandName]; //直接获取到了引擎的品牌名称
    return engineBrandName;
}
复制代码

与以前的设计不一样,在客户端里面,没有引入GasEngine类,而是直接经过Car实例获取到了须要的数据。

这样设计的好处是,若是这辆车的引擎换成了电动引擎(原来的GasEngine类换成了ElectricEngine类),客户端代码能够不作任何修改!由于它没有引入任何引擎类,而是直接获取了引擎的品牌名称。

因此在这种状况下咱们只须要修改Car类的usingEngineBrandName方法实现,将新引擎的品牌名称返回便可。

下面来看一下这两个设计的UML 类图,能够更形象地看出两种设计上的区别:

UML 类图对比

未实践迪米特法则:

未实践迪米特法则

实践了迪米特法则:

实践了迪米特法则

很明显,在实践了迪米特法则的 UML 类图里面,没有了ClientGasEngine的依赖,耦合性下降。

如何实践

从此在作对象与对象之间交互的设计时,应该极力避免引出中间对象的状况(须要导入其余对象的类):须要什么对象直接返回便可,下降类之间的耦合度。

原则六:里氏替换原则(Liskov Substitution Principle)

定义

In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)

即:全部引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象能够替换其父类对象,而程序执行效果不变。

定义的解读

在继承体系中,子类中能够增长本身特有的方法,也能够实现父类的抽象方法,可是不能重写父类的非抽象方法,不然该继承关系就不是一个正确的继承关系。

优势

能够检验继承使用的正确性,约束继承在使用上的泛滥。

代码讲解

在这里用一个简单的长方形与正方形的例子讲解一下里氏替换原则。

需求点

建立两个类:长方形和正方形,均可以设置宽高(边长),也能够输出面积大小。

很差的设计

首先声明一个长方形类,而后让正方形类继承于长方形。

长方形类:

//================== Rectangle.h ==================

@interface Rectangle : NSObject
{
@protected double _width;
@protected double _height;
}

//设置宽高
- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

//获取宽高
- (double)width;
- (double)height;

//获取面积
- (double)getArea;

@end



//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}


- (double)getArea{
    return _width * _height;
}

@end
复制代码

正方形类:

//================== Square.h ==================

@interface Square : Rectangle
@end



//================== Square.m ==================

@implementation Square

- (void)setWidth:(double)width{
    
    _width = width;
    _height = width;
}

- (void)setHeight:(double)height{
    
    _width = height;
    _height = height;
}

@end
复制代码

能够看到,正方形类继承了长方形类之后,为了保证边长永远是相等的,特地在两个set方法里面强制将宽和高都设置为传入的值,也就是重写了父类Rectangle的两个set方法。可是里氏替换原则里规定,子类不能重写父类的方法,因此上面的设计是违反该原则的。

并且里氏替换原则原则里面所属:子类对象可以替换父类对象,而程序执行效果不变。咱们经过一个例子来看一下上面的设计是否符合:

在客户端类写一个方法:传入一个Rectangle类型并返回它的面积:

- (double)calculateAreaOfRect:(Rectangle *)rect{
    return rect.getArea;
}
复制代码

咱们先用Rectangle对象试一下:

Rectangle *rect = [[Rectangle alloc] init];
rect.width = 10;
rect.height = 20;
    
double rectArea = [self calculateAreaOfRect:rect];//output:200
复制代码

长宽分别设置为10,20之后,结果输出200,没有问题。

如今咱们使用Rectange的子类Square的对象替换原来的Rectange对象,看一下结果如何:

Square *square = [[Square alloc] init];
square.width = 10;
square.height = 20;
    
double squareArea = [self calculateAreaOfRect:square];//output:400
复制代码

结果输出为400,结果不一致,再次说明了上述设计不符合里氏替换原则,由于子类的对象square替换父类的对象rect之后,程序执行的结果变了。

不符合里氏替换原则就说明该继承关系不是正确的继承关系,也就是说正方形类不能继承于长方形类,程序须要从新设计。

咱们如今看一下比较好的设计。

较好的设计

既然正方形不能继承于长方形,那么是否可让两者都继承于其余的父类呢?答案是能够的。

既然要继承于其余的父类,它们这个父类确定具有这两种形状共同的特色:有4个边。那么咱们就定义一个四边形的类:Quadrangle

//================== Quadrangle.h ==================

@interface Quadrangle : NSObject
{
@protected double _width;
@protected double _height;
}

- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

- (double)width;
- (double)height;

- (double)getArea;
@end
复制代码

接着,让Rectangle类和Square类继承于它:

Rectangle类:

//================== Rectangle.h ==================

#import "Quadrangle.h"

@interface Rectangle : Quadrangle

@end



//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}


- (double)getArea{
    return _width * _height;
}

@end
复制代码

Square类:

//================== Square.h ==================

@interface Square : Quadrangle
{
    @protected double _sideLength;
}

-(void)setSideLength:(double)sideLength;

-(double)sideLength;

@end



//================== Square.m ==================

@implementation Square

-(void)setSideLength:(double)sideLength{    
    _sideLength = sideLength;
}

-(double)sideLength{
    return _sideLength;
}

- (void)setWidth:(double)width{
    _sideLength = width;
}

- (void)setHeight:(double)height{
    _sideLength = height;
}

- (double)width{
    return _sideLength;
}

- (double)height{
    return _sideLength;
}


- (double)getArea{
    return _sideLength * _sideLength;
}

@end
复制代码

咱们能够看到,RectangeSquare类都以本身的方式实现了父类Quadrangle的公共方法。并且因为Square的特殊性,它也声明了本身独有的成员变量_sideLength以及其对应的公共方法。

注意,这里RectangeSquare并非重写了其父类的公共方法,而是实现了其抽象方法。

下面来看一下这两个设计的UML 类图,能够更形象地看出两种设计上的区别:

UML 类图对比

未实践里氏替换原则:

未实践里氏替换原则

实践了里氏替换原则:

实践了里氏替换原则

如何实践

里氏替换原则是对继承关系的一种检验:检验是否真正符合继承关系,以免继承的滥用。所以,在使用继承以前,须要反复思考和确认该继承关系是否正确,或者当前的继承体系是否还能够支持后续的需求变动,若是没法支持,则须要及时重构,采用更好的方式来设计程序。

最后的话

到这里关于六大设计原则的讲解已经结束了。本篇文章所展现的Demo和UML 类图都在笔者维护的一个专门的GitHub库中:object-oriented-design。想看Demo和UML图的同窗能够点击连接查看。欢迎fork,更欢迎给出更多语言的不一样例子的PR~ 并且后续还会添加关于设计模式的 代码和 UML 类图。

关于这几个设计原则还有最后一点须要强调的是: 设计原则是设计模式的基石,可是很难在使实际开发中的某个设计中所有都知足这些设计原则。所以咱们须要抓住具体设计场景的特殊性,有选择地遵循最合适的设计原则。

本篇已同步到我的博客:面向对象设计的六大设计原则(附 Demo 及 UML 类图)


笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。

  • 编程类文章:包括笔者之前发布的精选技术文章,以及后续发布的技术文章(以原创为主),而且逐渐脱离 iOS 的内容,将侧重点会转移到提升编程能力的方向上。
  • 读书笔记类文章:分享编程类思考类心理类职场类书籍的读书笔记。
  • 思考类文章:分享笔者平时在技术上生活上的思考。

由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。

并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~

扫下方的公众号二维码并点击关注,期待与您的共同成长~