为了账号安全,请及时绑定邮箱和手机立即绑定

六大设计原则下

标签:
iOS 面试

阅读须知:

如果此文有幸被读者您看到,请您跟上则一起看。

依赖倒置原则

定义:

High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details.Details should depend upon abstractions.

翻译过来,就是高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。核心思想就是:要面向接口编程而不是面向实现编程。

依赖倒置原则是实现开闭原则的途径之一,它降低了客户与实现模块之间的耦合。由于在软件设计中,细节具有多变性,而抽象层则相对稳定,所以以抽象层为基础搭建起来的架构要比以细节为基础打出的架构要稳定得多。这里的抽象指的是OC中的接口(协议)等没有实际功能的抽象类(严格来说,OC没有抽象类),细节则指具体的实现类。使用接口或抽象类的目的是指定好规范和契约,而不去涉及任何具体的操作,把展示细节的任务交给他们的实现类完成。

注:Objective-C 没有抽象类只是指语言的语法构造上没有C++/Java那样专门的abstract class定义,但是我们可以把没有实现具体功能,交给子类去实现的类,比如协议,当做抽象类。

优点

  • 降低类间的耦合性
  • 提高系统的稳定性
  • 减少并行开发的风险
    因为耦合性降低,所以一个程序分配多人开发,这就是并行开发。如果A开发速度快,开发完早下班,就不用等待慢一点的B开发完他那边的功能才可以下班。因为A负责的类不依赖B负责的类,就算B没开发完也不影响A开发。
  • 提高代码的可读性和和维护性

实现

依赖倒置的目的是要面向接口的编程来降低类间的耦合性。

不好的设计

我们都很喜欢喝奶茶,假如有一个顾客,长期喝一点点奶茶。我们可以定义一个CustomerYidiandianShop类。

Customer类:

//------ Customer.h ------
#import "YidiandianShop.h"

@interface Customer : NSObject
- (void)shopping:(YidiandianShop *)shop;
@end


//------ Customer.m ------
- (void)shopping:(YidiandianShop *)shop {
    [shop sell];
}

YidiandianShop类:

//------ YidiandianShop.h ------
@interface YidiandianShop : NSObject
- (void)sell;
@end


//------ YidiandianShop.m ------
@implementation YidiandianShop
- (void)sell {
    NSLog(@"一点点奶茶!");
}
@end

这种设计不好,因为这位顾客突然想喝coco奶茶,岂不是要修改代码如下?

@interface Customer : NSObject
- (void)shopping:(CocoShop *)shop;
@end

其类图如下:
图片.png

万一下次还要换喜茶等其他奶茶呢?岂不是每次换奶茶,都要修改很多代码,在《六大设计原则上》我们讲过开闭原则——软件实体应当对扩展开放,对修改关闭,这很明显违反了开闭原则。

我们稍微修改下,增加一个接口(协议)

较好的设计1

接着上面的例子来讲,上面的顾客类每次都跟具体的奶茶店绑死了。应该定义奶茶店的共同接口(协议),顾客面向此接口编程。

ShopProtocol协议:

//------ ShopProtocol.h ------
@protocol ShopProtocol <NSObject>

- (void)sell;

@end

Customer类:

//------ Customer.h ------
#import "ShopProtocol.h"
#import "YidiandianShop.h"
#import "CocoShop.h"

@interface Customer : NSObject
- (void)sellWithShop:(id<ShopProtocol>)shop;
@end


//------ Customer.m ------
@implementation Customer

- (void)sellWithShop:(id<ShopProtocol>)shop {
    if (self == [super init]) {
        [shop sell];
    }
}
@end

YidiandianShop类几乎不变,只是头文件不再需要声明sell方法,因为遵从了已经声明sell方法的协议ShopProtocol,根据场景需求,顾客想喝Coco奶茶,所以新增CocoShop

CocoShop类:

//------ CocoShop.h ------
 #import "ShopProtocol.h"

@interface CocoShop : NSObject<ShopProtocol>

@end

//------ CocoShop.m ------
@implementation CocoShop
- (void)sell {
    NSLog(@"coco奶茶!");
}
@end

YidiandianShop类:

//------ YidiandianShop.h ------
#import "ShopProtocol.h"

@interface YidiandianShop : NSObject<ShopProtocol>

@end


//------ YidiandianShop.m ------
@implementation YidiandianShop
- (void)sell {
    NSLog(@"一点点奶茶!");
}
@end

运行程序:

Customer *customer = [Customer new];
[customer sellWithShop:[CocoShop new]];
//[customer sellWithShop:[YidiandianShop new]];

结果为:
coco奶茶!

其类图如下:

我们可以看到,当我们增加一个类,我们的高层模块Customer几乎不用修改,而我们暴露的协议ShopProtocol也不用修改,这就使得高层模块跟底层模块解耦,而旧设计就显得很恶心啦,每添加一个类,就要修改或者添加高层模块的方法

较好的设计2

还有种方法是通过创建奶茶类的父类milkTeaShop,声明方法sell,具体的奶茶店比如YidiandianShopCocoShop继承它,作为milkTeaShop的实现类。其实这设计跟协议那种做法很像,如果非要为这种设计起一个名字,可以叫做抽象做法。因为做法很简单,在此笔者就不列代码了。

两种好的设计都是为了解耦高低层模块,中间添加抽象或者协议,减少高低层的直接依赖。

接口隔离原则

定义:

1、Clients should not be forced to depend upon interfaces that they don’ t use. (客户端不应该
依赖它不需要的接口。)
2、The dependency of one class to another one should depend on the smallest possible interface. (类间的依赖关系应该建立在最小的接口上。)

解释一下这两种定义:客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,对接口进行细化,保证接口纯洁。也就是说简历单一接口,不要建立臃肿庞大的接口,接口中的方法尽量少。

注意:这跟单一职责审视角度不一样,单一职责要求的是类和接口职责单一,注重的是职责。比如说,一个类里面有播放古典音乐方法、流行音乐方法、摇滚音乐方法…这符合单一职责,因为都是播放音乐。而接口隔离原则则要求接口方法尽量少。例如一个接口的职责可能包含10个方法,这10个方法都放在一个接口中,并且提供给多个模块访问,这不符合接口隔离原则,符合单一职责原则。

接口隔离要求我们尽量使用多个专门接口。专门是指提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大而臃肿的接口,容纳所有的客户端访问。

实现

我们以编程语言为例。究竟怎样的编程语言才是好的编程语言呢?众说纷纭,运行性能高的、跨平台的、开发速度快的。如果你准备选择一门语言开发校务系统,我们可以用类图表示一下。

(虚线普通箭头—> 是依赖的意思,由依赖方指向被依赖方)
(虚线空心箭头则是实现的意思,由实现方指向协议)

可以看出,ProgrammingLanguage类遵从了ProgrammingLanguageProtocol协议,协议方法有好性能,好开发速度,跨平台等。Developer类则依赖协议,选择一门语言。下面用代码表示此关系:

ProgrammingLanguageProtocol类:

@protocol ProgrammingLanguageProtocol <NSObject>

- (instancetype)initWithLangauageName:(NSString *)name;

- (void)getGoodPerformance;
- (void)getGoodSpeed;
- (void)getMultiPlatform;

@end

ProgrammingLanguage类:

//------ ProgrammingLanguage.h ------
@interface ProgrammingLanguage : NSObject<ProgrammingLanguageProtocol>
{
    @protected
        NSString *langauageName;
}

@end

//------ ProgrammingLanguage.m ------
@implementation ProgrammingLanguage

- (instancetype)initWithLangauageName:(NSString *)name {
    if (self == [super init]) {
        langauageName = name;
    }
    return self;
}

- (void)getGoodSpeed {
    NSLog(@"%@开发速度很快", langauageName);
}

- (void)getGoodPerformance {
    NSLog(@"%@性能不错", langauageName);
}

- (void)getMultiPlatform {
    NSLog(@"%@跨平台", langauageName);
}

@end

Developer类:

//------ Developer.h ------
#import "ProgrammingLanguage.h"

@interface Developer : NSObject
{
    @private
        ProgrammingLanguage *language;
}
- (instancetype)initWithAdoptLanguage:(ProgrammingLanguage *)languageName;
- (void)showLanguage;
@end

//------ Developer.m ------
@implementation Developer

- (instancetype)initWithAdoptLanguage:(ProgrammingLanguage *)languageName {
    if (self == [super init]) {
        language = languageName;
    }
    return self;
}
- (void)showLanguage {
    [language getGoodPerformance];
    [language getMultiPlatform];
    [language getGoodSpeed];
    
}
@end

运行程序:

ProgrammingLanguage *language = [[ProgrammingLanguage alloc] initWithLangauageName:@"Java语言"];
Developer *developer = [[Developer alloc] initWithAdoptLanguage:language];
[developer showLanguage];

结果为:
Java语言性能不错
Java语言跨平台
Java语言开发速度很快

较好的设计

我们都知道,一般语言性能高跟开发速度很难兼得,如果老板A更倾向性能,他认为只有稳扎稳打才能扎根市场。那么开发速度可能就得舍弃一点,开发没那么快速,毕竟一般是用到更底层、未封装过的语言。而如果老板B更倾向开发速度,说产品快速迭代,推向市场,才能成功。这时就要选择开发速度优先的语言。但上面把所有的特性都放在一个协议中,属于封装过度。

为了解决这种问题,应该把上面略显臃肿的协议细化,把性能和开发速度分开。这样设计后,不管要选择哪种特性优先的编程语言,都可以保持接口的稳定。

把一个臃肿的接口拆分为两个独立的接口所依照的原则就是接口隔离原则。

新建两个类GoodSpeedProtocolGoodPerformanceProtocol分别代表好的开发速度的协议和好的性能的协议,再用ProgrammingLanguage去实现他们。如果要优先开发速度,则遵从GoodSpeedProtocol,如果要优先性能,则遵从GoodPerformanceProtocol

GoodSpeedProtocol类:

// 好的开发速度的协议
@protocol GoodSpeedProtocol <NSObject>
- (instancetype)initWithLangauageName:(NSString *)name;
- (void)getGoodSpeed;
@end

GoodPerformanceProtocol类:

// 好的性能的协议
@protocol GoodPerformanceProtocol <NSObject>
- (instancetype)initWithLangauageName:(NSString *)name;
- (void)getGoodPerformance;
@end

ProgrammingLanguage类:

//------ ProgrammingLanguage.h ------
#import "GoodSpeedProtocol.h"
#import "GoodPerfermanceProtocol.h"

@interface ProgrammingLanguage : NSObject<GoodSpeedProtocol, GoodPerformanceProtocol>
{
    @protected
        NSString *langauageName;
}

@end


//------ ProgrammingLanguage.m ------
@implementation ProgrammingLanguage

- (instancetype)initWithLangauageName:(NSString *)name {
    if (self == [super init]) {
        langauageName = name;
    }
    return self;
}

- (void)getGoodSpeed {
    NSLog(@"%@开发速度很快,选择它", langauageName);
}

- (void)getGoodPerformance {
    NSLog(@"%@性能不错,选择它", langauageName);
}

- (void)getMultiPlatform {
    NSLog(@"%@跨平台,选择它", langauageName);
}

@end

Developer类:

//------ Developer.h ------
#import "ProgrammingLanguage.h"

@interface Developer : NSObject
{
    @private
        ProgrammingLanguage *language;
}
- (instancetype)initWithAdoptLanguage:(ProgrammingLanguage *)languageName;
- (void)showLanguage;
@end

//------ Developer.m ------
@implementation Developer

- (instancetype)initWithAdoptLanguage:(ProgrammingLanguage *)languageName {
    if (self == [super init]) {
        language = languageName;
    }
    return self;
}
- (void)showLanguage {
    [language getGoodPerformance];
    //[language getMultiPlatform];
    //[language getGoodSpeed]; 
}
@end

运行程序:

ProgrammingLanguage *language = [[ProgrammingLanguage alloc] initWithLangauageName:@"Objective-C语言"];
Developer *developer = [[Developer alloc] initWithAdoptLanguage:language];
[developer showLanguage];

结果为:
Objective-C语言性能不错,选择它

其类图如下:

(虚线普通箭头—> 是依赖的意思,由依赖方指向被依赖方)
(虚线空心箭头则是实现的意思,由实现方指向协议)

接口隔离原则含义:

1、接口隔离原则核心定义是接口要尽量小,不出现臃肿的接口,但是“小”有一定的限度,不能违背单一职责原则。举个单一职责原则中的例子,从下面类图可以看到,照相功能和播放音乐功能依赖手机。我们都知道,播放音乐,可以播放高音质音乐、标准音质,说到这里,是不是认为播放音乐还可以继续细分为两个独立协议?但实际上,这是不对的,因为从业务逻辑上来讲,播放音乐已经是最小的业务单位了。根据接口隔离原则拆分接口时,首先必须满足单一职责原则。

2、拆分粒度越小,系统越灵活,没错,但是灵活的同时也带来结构的复杂化,开发难度增加,可维护性降低,所以接口设计要适度。

迪米特法则

定义:

Talk only to your immediate friends and not to strangers(只与你的直接朋友交谈,不跟“陌生人”说话)

解释一下定义:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的相合度,提高模块的相对独立性。

从迪米特法则它强调以下两点 :

  • 从依赖者的角度来说,只依赖应该依赖的对象 ;
  • 从被依赖者的角度说,只暴露应该暴露的方法 。

实现:

以小明想要结婚为例.

不好的设计

小明亲自出马,去找满意的姑娘,去约会。如果小明身份还是个生意人,见顾客的行为难道要跟相亲的行为都设计在Man类?这样造成的后果就是类太冗余,模块不够独立。可复用性和扩展性降低。

较好的设计

这里跟上面不同的就是多了“中介”,相亲中介必然是媒人,生意中介可以是秘书等。

小明去找媒人帮忙介绍姑娘相亲,这里的媒人是小明认识的,而介绍的姑娘是陌生人,所以这里适用迪米特法则,其类图如下:

(虚线空心箭头是“依赖”的意思,由依赖方指向被依赖方)

代码如下:

Man类:

//------ Man.h ------
@interface Man : NSObject

@property (nonatomic, strong) NSString *name;
- (instancetype)initWithName:(NSString *)name;
@end

//------ Man.m ------
@implementation Man
- (instancetype)initWithName:(NSString *)name {
    if (self == [super init]) {
        _name = name;
    }
    return self;
}
@end

Woman类:

//------ Woman.h ------
@interface Woman : NSObject

@property (nonatomic, strong) NSString *name;
- (instancetype)initWithName:(NSString *)name;
@end

//------ Woman.m ------
@implementation Woman
- (instancetype)initWithName:(NSString *)name {
    if (self == [super init]) {
        _name = name;
    }
    return self;
}
@end

Matchmaker类:

//------ Matchmaker.h ------
#import "Man.h"
#import "Woman.h"

@interface Matchmaker : NSObject
@property (nonatomic, strong) Man *man;
@property (nonatomic, strong) Woman *woman;
- (void)haveDate;
@end

//------ Matchmaker.m ------
@implementation Matchmaker
- (void)haveDate {
    NSLog(@"%@和%@约会", self.man.name, self.woman.name);
}
@end

运行程序:

Matchmaker *matchmaker = [Matchmaker new];
Man *man = [[Man alloc] initWithName:@"小明"];
Woman *woman = [[Woman alloc] initWithName:@"林志玲"];
[matchmaker setMan:man];
[matchmaker setWoman:woman];
[matchmaker haveDate]; //约会

结果为:
小明和林志玲约会

上面的Man和Woman类初始化代码一样,当然可以考虑使用继承,但在这里并不是重点,这里要说的是迪米特原则。需要注意的是,迪米特原则不可过度使用,不然会产生太多“中介”,从而增加系统的复杂性,使模块通信效率降低。

写在最后

六大设计原则是软件设计模式必须尽量遵循的原则,每个原则的侧重点不同。

  • 开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;
  • 里氏替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要面向接口编程;
  • 单一职责原则告诉我们实现类要职责单一;
  • 接口隔离原则告诉我们在设计接口的时候要精简单一;
  • 迪米特法则告诉我们要降低耦合度。
点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
软件工程师
手记
粉丝
6445
获赞与收藏
781

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消