首先,在上篇中总结了面向对象设计的前四个设计模式,分别是:开闭原则、单一职责原则、依赖倒置原则、接口分离原则。由于文章篇幅过长,遗留了后两个设计原则,这次在剩余的时间里补上。嘻嘻
原则五:迪米特法则(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
我们可以看到,Rectangle和Square类都以自己的方式实现了父类Quadrangle的公共方法。而且由于Square的特殊性,它也声明了自己独有的成员变量_sideLength以及其对应的公共方法。
注意,这里Rectangle和Square并不是重写了其父类的公共方法,而是实现了其抽象方法。
下面来看一下这两个设计的UML类图,可以更形象地看出来两种设计上的区别:
UML类图对比
未实践里氏替换原则:
实践了里氏替换原则:
如何实践
里氏替换原则是对继承关系的一种检验:检验是否真正符合继承关系,以避免继承的滥用。因此,在使用继承之前,需要反复思考和确认该继承关系是否正确,或者当前的继承体系是否还支持后续的需求变更,如果无法支持,则需要及时重构,采用更好的方式来设计程序。
最后的话
到这里关于面向对象的六大设计原则的讲解已经结束了。本篇文章所展示的Demo和UML类图都在笔者维护的一个专门的Github库中:Object-Oriented-Design。想看Demo和想看Demo和UML图的同学可以点击链接查看。欢迎fork,更欢迎给出更多语言的不同例子的PR~ 而且后续还会添加关于设计模式的 代码和 UML 类图。
关于这几个设计原则还有最后一点需要强调的是:设计原则是设计模式的基石,但是很难在使实际开发中的某个设计中全部都满足这些设计原则。因此我们需要抓住具体设计场景的特殊性,有选择地遵循最合适的设计原则。
笔者在近期开通了个人公众号,主要分享编程,读书笔记,思考类的文章。
- 编程类文章:包括笔者以前发布的精选技术文章,以及后续发布的技术文章(以原创为主),并且逐渐脱离 iOS 的内容,将侧重点会转移到提高编程能力的方向上。
- 读书笔记类文章:分享编程类,思考类,心理类,职场类书籍的读书笔记。
- 思考类文章:分享笔者平时在技术上,生活上的思考。
因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的
而且因为各大博客平台的各种限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~
Comments | NOTHING