面向对象设计的六大设计原则

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


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

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

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

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

需求点
设计一个在线课程类:

由于教学资源有限,开始的时候只有类似于博客的,通过文字讲解的课程。 但是随着教学资源的增多,后来增加了视频课程,音频课程以及直播课程。

先来看一下不好的设计:

不好的设计
最开始的文字课程类:

@interface K_Course_N : NSObject
//K_Course_N类声明了最初的在线课程所需要包含的数据:
@property (nonatomic, copy) NSString *courseTitle;         //课程名称
@property (nonatomic, copy) NSString *courseIntroduction;  //课程介绍
@property (nonatomic, copy) NSString *teacherName;         //讲师姓名
@property (nonatomic, copy) NSString *content;             //课程内容
@end

K_Course_N类声明了最初的在线课程所需要包含的数据:
课程名称
课程介绍
讲师姓名
文字内容
接着按照上面所说的需求变更:增加了视频,音频,直播课程:

@interface K_Course_N : NSObject
//K_Course_N类声明了最初的在线课程所需要包含的数据:
@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类实例化的视频课程对象会包含并不属于自己的数据:audioUrl和liveUrl:这样就造成了冗余,视频课程对象并不是纯粹的视频课程对象,它包含了音频地址,直播地址等成员。
很显然,这个设计不是一个好的设计,因为(对应上面两段叙述):

1、随着需求的增加,需要反复修改之前创建的类。
2、给新增的类造成了不必要的冗余。

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

下面看一下遵循开闭原则的较好的设计:

较好的设计
首先在K_Course_Y类中仅仅保留所有课程都含有的数据

@interface K_Course_Y : NSObject
@property (nonatomic, copy) NSString *courseTitle;         //课程名称
@property (nonatomic, copy) NSString *courseIntroduction;  //课程介绍
@property (nonatomic, copy) NSString *teacherName;         //讲师姓名
@end

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

文字课程类:

@interface TextCourse : K_Course_Y
@property (nonatomic, copy) NSString *content;             //文字内容
@end

视频课程类:

@interface VideoCourse : K_Course_Y
@property (nonatomic, copy) NSString *videoUrl;            //视频地址
@end

音频课程类:

@interface AudioCourse : K_Course_Y
@property (nonatomic, copy) NSString *audioUrl;            //音频地址
@end

直播课程类:

@interface LiveCourse : K_Course_Y
@property (nonatomic, copy) NSString *liveUrl;             //直播地址
@end

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

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

而且对于第二点:或许今后的视频课程可以有高清地址,视频加速功能。而这些功能只需要在VideoCourse类里添加即可,因为它们都是视频课程所独有的。同样地,直播课程后面还可以支持在线问答功能,也可以仅加在LiveCourse里面。
我们可以看到,正是由于最初程序设计合理,所以对后面需求的增加才会处理得很好。


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


定义
一个类只允许有一个职责,即只有一个导致该类变更的原因。

定义的解读
a、类职责的变化往往就是导致类变化的原因:也就是说如果一个类具有多种职责,就会有多种导致这个类变化的原因,从而导致这个类的维护变得困难。
b、往往在软件开发中随着需求的不断增加,可能会给原来的类添加一些本来不属于它的一些职责,从而违反了单一职责原则。如果我们发现当前类的职责不仅仅有一个,就应该将本来不属于该类真正的职责分离出去。
c、不仅仅是类,函数(方法)也要遵循单一职责原则,即:一个函数(方法)只做一件事情。如果发现一个函数(方法)里面有不同的任务,则需要将不同的任务以另一个函数(方法)的形式分离出去。

优点
如果类与方法的职责划分得很清晰,不但可以提高代码的可读性,更实际性地更降低了程序出错的风险,因为清晰的代码会让bug无处藏身,也有利于bug的追踪,也就是降低了程序的维护成本。

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

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

新需求:增加两个方法:
1、判定员工在今年是否升职
2、计算员工的薪水
先来看一下不好的设计:

不好的设计

@interface D_Employee_N : 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这个方法的职责是属于人事部门的:考核与晋升机制是人事部门负责。

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

我们可以简单想象一下这么做的后果是什么:如果员工的晋升机制变了,或者税收政策等影响员工工资的因素变了,我们还需要修改当前这个类。

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

较好的设计
我们保留员工类的基本信息:

@interface D_Employee_Y : NSObject//也针对方法
//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;
@end

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

#import "D_Employee_Y.h"
@interface FinancialApartment : NSObject
//计算薪水
- (double)calculateSalary:(D_Employee_Y *)employee;
@end

和人事部门类:

#import "D_Employee_Y.h"
@interface HRApartment : NSObject
//今年是否晋升
- (BOOL)willGetPromotionThisYear:(D_Employee_Y *)employee;
@end

通过创建了两个分别专门处理薪水和晋升的部门,会计部门和人事部门的类:FinancialApartment 和 HRApartment,把两个任务(责任)分离了出去,让本该处理这些职责的类来处理这些职责。
这样一来,不仅仅在此次新需求中满足了单一职责原则,以后如果还要增加人事部门和会计部门处理的任务,就可以直接在这两个类里面添加即可。


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


定义
a、依赖抽象,而不是依赖实现。
b、抽象不应该依赖细节;细节应该依赖抽象。
c、高层模块不能依赖低层模块,二者都应该依赖抽象。

定义解读
a、针对接口编程,而不是针对实现编程。
b、尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现。
c、关于高层模块与低层模块的划分可以按照决策能力的高低进行划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。
优点
通过抽象来搭建框架,建立类和类的关联,以减少类间的耦合性。而且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。


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


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

定义解读

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

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

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


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


定义
一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。

定义解读

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

优点
实践迪米特法则可以良好地降低类与类之间的耦合,减少类与类之间的关联程度,让类与类之间的协作更加直接。


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


定义
所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。

定义的解读

  • 在继承体系中,子类中可以增加自己特有的方法,也可以实现父类的抽象方法,但是不能重写父类的非抽象方法,否则该继承关系就不是一个正确的继承关系。

优点
可以检验继承使用的正确性,约束继承在使用上的泛滥。

你可能感兴趣的:(面向对象设计的六大设计原则)