首先,在上篇中总结了面向对象设计的前四个设计模式,分别是:开闭原则、单一职责原则、依赖倒置原则、接口分离原则。由于文章篇幅过长,遗留了后两个设计原则,这次在剩余的时间里补上。嘻嘻

原则五:迪米特法则(law of demeter)

定义:

You only ask for objects which you directly need.
即:一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。

定义解读

  • 迪米特法则也叫最少知道法则,一个类应该只和它的成员变量、方法的输入、返回参数中的类接触。而不该引入其他的类做间接的接触。

优点

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

代码讲解

下面通过一个简单的例子来讲解一下迪米特法则。

需求点

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

不好的设计

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类给外部提供了一个返回引擎对象的方法:
function: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的耦合。而这个耦合显然是不必要更是可以避免的。

接下来我们看一下如何设计可以避免这种耦合:

较好的设计

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

//============= Car.h ==============
@class GasEngine;

@interface Car : NSObject

//构造方法
- (instacetype)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类图里面,没有了Client对GasEngine的依赖,耦合性降低。

如何实践

今后在做对象与对象之间交互的设计时,应该极力避免引出中间对象的情况(需要导入其他对象的类):需要什么对象直接返回即可,降低类之间的耦合度。

原则六:里氏替换原则(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)seyHeight:(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 Squar

- (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,没有问题。

现在我们使用Rectangle的子类Square的对象替换原来的Rectangle对象,看一下结果如何:

Square *square = [[Square alloc] init];
square.width = 10;
square.height = 20;
double squareArea = [self calculateAreaOfRect:square];

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

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

我们现在看一下比较好的设计。

较好的设计

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

既然要继承于其他的父类,他们这个父类肯定具备这两种形状共同的特点:有四个边。那么我们就定义一个四边形的类: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

我们可以看到,RectangleSquare类都以自己的方式实现了父类Quadrangle的公共方法。而且由于Square的特殊性,它也声明了自己独有的成员变量_sideLength以及其对应的公共方法。

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

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

UML类图对比

未实践里氏替换原则:

实践了里氏替换原则:

如何实践

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

最后的话

到这里关于面向对象的六大设计原则的讲解已经结束了。本篇文章所展示的Demo和UML类图都在笔者维护的一个专门的Github库中:Object-Oriented-Design。想看Demo和想看Demo和UML图的同学可以点击链接查看。欢迎fork,更欢迎给出更多语言的不同例子的PR~ 而且后续还会添加关于设计模式的 代码和 UML 类图。

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


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

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

因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的

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

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


你远道而来这人世间,想必也是因为热爱吧 ||