前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >轻松学习设计模式之面向对象的设计原则

轻松学习设计模式之面向对象的设计原则

作者头像
用户1257215
发布2018-07-27 09:48:17
4330
发布2018-07-27 09:48:17
举报
文章被收录于专栏:架构师之旅架构师之旅

对于面向对象软件系统的设计而言,在支持可维护性的同时,提高系统的可复用性是一个至关重要的问题,如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。 面相对象设计的概念大家也都知道,它的设计目标就是希望软件系统能做到以下几点:

可扩展:新特性能够很容易的添加到现有系统中,不会影响原本的东西 可修改:当修改某一部分的代码时,不会影响到其它不相关的部分 可替代:将系统中某部分的代码用其它有相同接口的类替换时,不会影响 到现有系统。 Robert C. Martin提出了面相对象设计的五个基本原则(SOLID): S-单一职责原则 O-开放关闭原则 L-里氏替换原则 I-接口隔离原则 D-依赖倒置原则

单一职责原则:Single Responsibility Principle

单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。单一职责原则定义如下: 单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。 举个栗子:

代码语言:javascript
复制
1@interface DataTransfer : NSObject
2-(void)upload:(NSData *)data; //上传数据
3-(void)download(NSString*)url;  //根据URL下载东西
4@end

DataTransfer包含上传跟下载功能,仔细考虑可以发现这相当于实现了两个功能,一个负责上传的相关逻辑,另一个负责下载的逻辑,而这个两个功能相对对立,当有一个功能改变的时候,比如我们之前是使用AFNetworking,现在想换成其它第三方或者nsurlconnection来实现上传跟下载:

上传方式变更,导致DataTransfer变更 下载方式变更,导致 DataTransfer变更 这就违反了单一职责的原则,所以需要将不同的功能拆解成两个不同的类,来负责各自的职责,不过这个拆的粒度可能因人而已,有时候并不需要拆的过细,不要成了为设计而设计。

开放关闭原则:Open Closed Principle

开闭原则的定义是说一个软件实体如类,模块和函数应该对扩展开放,而对修改关闭,具体来说就是你应该通过扩展来实现变化,而不是通过修改原有的代码来实现变化,该原则是面相对象设计最基本的原则。 在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。 任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。 举个例子:我们需要保存对象到数据库当中,其中有个类似save()的保存方法,这部分应该是不变的,接口相对稳定,而具体保存的实现却有可能不同,我们现在可能是保存在Sqlite数据库中,假如以后如果想保存到一个自己实现的数据库中时,我们只需要实现一个拥有同样接口的扩展类添加进去即可,这就是对扩展开放,不会对之前的代码造成任何影响,就可以实现保存到新数据库的功能,保证了系统的稳定性。

里氏替代原则:Liskov Substitution Principle

里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

使用里氏替换原则时需要注意,子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。 举个栗子

比如有一个鲸鱼的类,我们让鲸鱼继承于鱼类,然后鱼类有个呼吸的功能 然后在水里的时候,鱼能够进行呼吸:

代码语言:javascript
复制
1if(isInwater){
2    //在水中了,开始呼吸
3    fish.breath();
4}

当我们用鲸鱼这个子对象替换原来的基类鱼对象,鲸鱼在水里开始呼吸,这时问题就出现了,鲸鱼是哺乳动物,在水里呼吸是没法呼吸的,一直在水里就GG思密达了,所以这违反了该原则,我们就可以判断鲸鱼继承于鱼类不合理,需要去重新设计。   通常在设计的时候,我们都会优先采用组合而不是继承,因为继承虽然减少了代码,提高了代码的重用性,但是父类跟子类会有很强的耦合性,破坏了封装。

接口隔离原则:Interface Segregation Principle

接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。 根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,比如Java语言中的interface。 建立单一接口,不要建立庞大的接口,尽量细化接口,接口中的方法尽量少。也就是要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的约定,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。  举个简单的例子:比如我们有个自行车接口,这个接口包含了很多方法,包括GPS定位,以及换挡的方法  先看一下这个不满足ISP原则

 然后我们发现即便普通的自行车也需要实现GPS定位以及换挡的功能,显然这违背了接口隔离的原则。遵循接口最小化的原则,我们重新设计

这样一来每个接口的功能相对单一,使用多个专门的接口比使用一个总的接口要好,假如我们的山地车没有没有GPS定位的功能,我们不去继承实现对应的接口即可,在iOS开发中有很多这样的例子,比如UITalbleView的代理有两个不同的接口,UITableViewDataSource专门负责需要显示的内容,UITableViewDelegate专门负责一些view的自定义显示,然后我们会继承多个接口,这就满足了ISP原则。

代码语言:javascript
复制
1@interface ViewController () <UITableViewDataSource,UITableViewDelegate,OtherProtocol>

依赖倒置原则:Dependence Inversion Principle

依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。 在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。 在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入( Setter注入) 和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。  比如在我们项目中有涉及IM的功能,现在这个IM模块采用的是XMPP协议来实现,客户端通过这个模块来实现消息的收发,但是假如后面我们想要换成其它协议,比如MQTT等,针对接口编程的话就可以让我们很轻松的实现模块替换:

代码语言:javascript
复制
 1@protocol MessageDelegate <NSObject>
 2@required
 3-(void)goOnline;
 4-(void)sendMessage:(NSString*)content;
 5@end
 6
 7//xmpp实现
 8@interface XMPPMessageCenter <MessageDelegate>
 9@end
10
11//MQTT实现
12@interface MQTTMessageCenter <MessageDelegate>
13@end
14
15//业务层
16@interface BussinessLayer
17//使用遵循MessageDelegate协议的对象,针对接口编程,以后替换也很方便
18@property(nonatomic,strong)id<MessageDelegate> messageCenter;
19@end

当我们在进行面向对象设计的时候应该充分考虑上面这几个原则,一开始可能设计并不完美,不过可以在重构的过程中不断完善。但其实很多人都跳过了设计这个环节,拿到一个模块直接动手编写代码,更不用说去思考设计了,项目中也有很多这样的例子。当然对于简单的模块或许不用什么设计,不过假如模块相对复杂的话,能够在动手写代码之前好好设计思考一下,养成这个习惯,肯定会对编写出可读性、稳定性以及可扩展性较高的代码有帮助。

参考资料 https://www.jianshu.com/p/e378025920f8 https://blog.csdn.net/yanbober/article/details/45312243 ebook-design-pattern-java.pdf

✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦

作者: Jager 原文:https://blog.csdn.net/CD344549214/article/details/80991351


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构师之旅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档