编写具有单一职责(SRP)的类
这两周我需要对一个历史遗留的功能做一些扩展,正如很多人不愿意碰这些历史遗留的代码一样,我的内心也同样对这样的任务充满反抗。这些代码中充斥着各种null判断(你写的return null正确吗?),不规范的变量命名,层层嵌套的if…else语句。显然面对这样的代码我无从下手,更别提什么重构、单元测试了。我需要的是尽量别动之前的代码,再小心意义的加上if…else语句,我已经无暇顾及下一个维护者的感受了。
造成今天这个局面的原因不在于旧代码没有使用多态、继承、封装,更不是前人没有使用设计模式,在我看来根本原因在于这些历史遗留的代码不符合单一职责(SRP)原则,没有合理的抽象。
当我站在这些遗留代码作者的角度去看待这些代码,我并不能抱怨什么,因为就我个人而言也写了不少不够SRP的代码,我有很长一段时间都写不出SRP的代码,我并不能灵活应用这项指导原则,虽然我知道SRP是怎么回事。似乎面向对象的这些原则看起来很简单,但是只有经过大量“刻意”的实践才能掌握其中的精髓。“刻意”一词是想说明只是沉浸在业务中不加思索的编码并不能提高面向对象的能力。
我感到幸运的是我的师傅经常把SOLID原则挂在嘴边,一遍遍的提醒我们SRP有多重要,不断review我的代码,帮我找到我自己本身意识不到的问题,经过一段时间的实践,我现如今才能够熟练写出SRP的代码。
对于SRP,一个简单的指导原则是:尽可能编写短小的类。不要担心类的数量由此而急剧增长,经验告诉我们很多具有良好文件结构并且命名清晰的类比只有一个文件而写了好几千行的类要更利于阅读和理解。
有人也许会问,难道一个类中只包含了两个方法就符合SRP吗?或者说BCL中就有写了几百行的类,难道就不符合SRP吗?有没有一种简单可靠的办法促使我们写出SRP的代码呢?
我们先用C#写一个名叫“用户注册中心”的类:
public class UserRegistry
{
public void Register(User user)
{
if (IsInvalidEmail(user.Email))
throw new Exception(string.Format("invalid email:{0}", user.Email));
if(IsInvalidPhoneNumber(user.Phone))
throw new Exception(string.Format("invalid phone number:{0}",user.Phone)); new UserRepository().Save(user);
} private bool IsInvalidEmail(string email)
{
//verify this email
return false;
} private bool IsInvalidPhoneNumber(string phoneNumber)
{
//verify this phoneNumber
return false;
}
}
这个类符合SRP原则吗?让我们看看Objective-C能否给我们一个思路:
使用Objective-C输出”Hello world”:
@implementation Greeter
//定义一个sayHello方法
- (void)sayHello{
NSLog(@"Hello world");
}
@end //向Greeter发送alloc消息,再发送init消息,得到一个Greeter实例
Greeter *greeter=[[Greeter alloc] init]; //向greeter对象发送sayHello消息
[greeter sayHello];
如你所见,Objective-C中通过“发送消息”来代替“方法调用”。虽然他们最终结果是一样的,但是“发送消息”这一思想需要我们来仔细品味:
1、向某个对象发送消息意味着对象之间通过消息来交流,更加松耦合。
2、向某个对象发送消息似乎是对象之间进行对话,使得对象更加具有生命力,有了生命力才能让我们更容易判断对象的职责。
3、“方法调用”意味着我需要知道你能干什么,然后不假思索的调用即可;“发送消息”意味着我知道你有什么能力,我给你发送你一个你能力范围之外的消息,你也许不会响应。
使用Objective-C编写的用户注册类:
@implementation UserRegistry
- (void)reg:(User *)user{
//①向用户中心发送isInvalidEmail消息
BOOL *isInvalidEmail=[self isInvalidEmail:user.email];
//②向用户中心发送isInvalidPhone消息
BOOL *isInvalidPhone=[self isInvalidPhone:user.phone];
//下面的代码用来抛出异常,不用关心
if(isInvalidEmail)
{
NSException *invalidEmailException=
[NSException exceptionWithName:@"regUserException"
reason:@"invalid email"
userInfo:nil];
@throw invalidEmailException;
}
if (isInvalidPhone) {
NSException *invalidPhoneException=
[NSException exceptionWithName:@"regUserException"
reason:@"invalid phone"
userInfo:nil];
@throw invalidPhoneException;
}
//初始化一个UserRepository对象
UserRepository *userRepository=[[UserRepository alloc] init];
//③向userRepository发送saveWithUser消息
[userRepository saveWithUser:user];
}
- (BOOL *)isInvalidEmail:(NSString *)email{
return false;
}
- (BOOL *)isInvalidPhone:(NSString *)phone{
return false;
}
@end
由于代码着色工具不能对上面的代码着色,所以不愿意阅读这段代码的朋友只需要理解在Objective-C使用发送消息的方式而不是方法调用即可。让我们通过“发送消息”的方式来解读这一段代码:
①:向“用户注册中心”发送“验证email”的消息;
②向“用户注册中心”发送“验证电话号码”的消息;
③向“用户仓储”发送“保存用户”的消息;
这三行代码合不合适呢,我们通过三个疑问来确定:
你认为消息接收者具备这样的能力吗?
发送这样的消息人家愿意响应吗?
你有考虑过消息接收者收到这样的消息后人家的感受吗?
我们推断一下“用户注册中心”应该具备的能力可能是:“注册用户”,“注销用户”,“这样的用户能够注册吗?”。推断一个对象应该具备的能力跟对象的命名有很大关系,一个名叫UserSearchService的对象应该具备的能力可能是“搜索用户”,一个名叫UserProvider的对象应该具备的能力可能是“获取用户”。
当你向一个名叫“用户注册中心”的对象发送一个“验证email”的消息后,他很可能不会搭理你,这意味这我们写的代码职责不够清晰,同时也暗示我们需要增加能够响应此消息的抽象:
应该向“Email验证器“发送”验证email”的消息——增加抽象EmailValidator;
应该向“电话号码验证器”发送“验证电话号码”的消息——增加抽象PhoneValidator;
向“用户仓储”发送“保存用户”的消息——没有问题,用户仓储应该具备这样的能力;
经过一番分析,代码变成了:
public class UserRegistry
{
private readonly EmailValidator _emailValidator;
private readonly PhoneValidator _phoneValidator;
private readonly UserRepository _userRepository; public UserRegistry(EmailValidator emailValidator, PhoneValidator phoneValidator, UserRepository userRepository)
{
_emailValidator = emailValidator;
_phoneValidator = phoneValidator;
_userRepository = userRepository;
} public void Register(User user)
{
if (_emailValidator.IsInvalid(user.Email))
throw new Exception(string.Format("invalid email:{0}", user.Email));
if (_phoneValidator.IsInvalid(user.Phone))
throw new Exception(string.Format("invalid phone number:{0}", user.Phone)); _userRepository.Save(user);
}
}
public class EmailValidator
{
public bool IsInvalid(string email)
{
//verify this email
return false;
}
} public class PhoneValidator
{
public bool IsInvalid(string phone)
{
//verify this phone
return false;
}
}
之前的一个类变成了现在的3个,每一个类都具有自己独立的职责。在实际应用中,我们很可能需要对这三个类扩充更多的能力。在这期间,你也许想赋予PhoneValidator更多的行为,PhoneValidator这样一个名称已经无法满足需求,你也许会抽象出Phone类代替之前的字符串,也许会抽象出MessageSender来向该电话号码发送验证码。
有着清晰职责和合理抽象的代码很好的诠释了“代码即注释”。对上面的任何一行代码增加注释都是多余的。
目前的代码无意之中已经符合了OCP(开闭原则),要想符合DIP(依赖倒置原则)只需要使用IOC容器实现注入即可,另外此时的代码也具备很好的测试性。
当你熟悉“发送消息”这种思想之后,相同的思路也可以用在“方法调用”上。
不过每次我觉得我对面向对象思想已经领会的很好了,但是隔一段时间又会有新的体会。所以本文所描述的想法仅代表目前阶段我对单一职责的理解,也许一段时间过后我会对单一职责又有新的看法。
你是如何看待本文所描述的想法呢?
本文描述的例子只是为了降低阅读门槛。有人会觉得这样会不会把事情搞复杂了,其实在这个例子中,两个验证方法修饰为private还是可行的,因为这个逻辑太简单了,简单到职责不够单一也能足够清晰。正如我文章中提到,在正式场景下需求绝对不会有这么简单,如果抛开这个前提我这个例子也许成了过度设计的反面教材。正因为业务逻辑越来越复杂,面向对象的一系列模式和原则才变得有意义,复杂的业务逻辑需要若干具有清晰职责且健壮的类组合而成——这一思想是我们进行SRP设计的依据。
编写具有单一职责(SRP)的类的更多相关文章
- S.O.L.I.D五大原则之单一职责SRP
转自 : 汤姆大叔的blog Bob大叔提出并发扬了S.O.L.I.D五大原则,用来更好地进行面向对象编程,五大原则分别是: The Single Responsibility Principle(单 ...
- 深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP
前言 Bob大叔提出并发扬了S.O.L.I.D五大原则,用来更好地进行面向对象编程,五大原则分别是: The Single Responsibility Principle(单一职责SRP) The ...
- 设计原则:单一职责(SRP)原则
1 什么是单一职责(SRP)原则 单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP.翻译过来就是:一个类或者模块只负责完成一个职责(或者功能). 所 ...
- 面象对象设计原则之一:单一职责原则(Single Responsibility Principle, SRP)
单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小.单一职责原则定义如下:单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域 ...
- 设计模式原则(1)--Single Responsibility Principle(SRP)--单一职责原则
1.定义: 不要存在多于一个导致类变更的原因.通俗的说,即一个类只负责一项职责. 2.使用场景: 如果类A有两个职责:d1,d2.当职责d1需要修改时,可能会导致原本运行正常的职责d2功能产生问题. ...
- 设计模式六大原则(一):单一职责原则(Single Responsibility Principle)
单一职责(SRP)定义: 不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责. 问题由来: 类T负责两个不同的职责:职责P1,职责P2.当由于职责P1需求发生改变而需要修改类T时,有可 ...
- 北风设计模式课程---单一职责原则(Single Responsibility Principle)
北风设计模式课程---单一职责原则(Single Responsibility Principle) 一.总结 一句话总结: 一个类应该有且只有一个变化的原因:单一职责原则(SRP:Single Re ...
- 软件开发中的单一职责(转至INFOQ)
最近在实践微服务化过程中,对其“单一职责”原则深有体会.那么只有微服务化才可以单一职责,才可以解耦吗?答案是否定的. 单一职责原则是这样定义的:单一的功能,并且完全封装起来. 我们做后端Java开发的 ...
- 设计模式课程 设计模式精讲 3-6 单一职责原则Coding
1 要点讲解 1.1 需要注意 2 代码演练 2.1 类的单一职责原则demo 2.2 接口的单一职责原则demo 2.3 方法的单一职责原则demo 1 要点讲解 1.1 需要注意 1.1.1 实际 ...
随机推荐
- C 标准库系列之locale.h
locale.h 区域设置相关,主要针对时间日期.货币格式.字符控制.数字格式等以满足某区域的设置需要. locale设置类别主要包括以下几个宏定义的类别: LC_ALL:设置所有的类别: LC_CO ...
- Git版本控制管理学习笔记3-基本的Git概念
为了更近一步的学习和理解Git的理念,这一节介绍一下Git中的一些基本概念. 基本概念 对象库图示 Git在工作时的概念 一.基本概念: 1.版本库: Git的版本库就是一个简单的数据库,其中 ...
- grafana
metrics+grafana elk 这两套系统居家旅游必备啊
- 各类坐标系相互之间的转换(84互转GC02,GC02互转BD09)
在遥感行业我们经常会用到各类的坐标系相互之间的转换,常见的度分秒转化为度很简单,直接上代码: //经纬度 ////118度48分54.152秒=118+(48/60)+(54.152/3600)=11 ...
- iOS中的生命周期
对于一个iOS app来讲,生命周期是一个十分至关重要的东西.对于一个app来讲控制着app的开启.睡眠.关闭等状态:对于一个页面的来讲,控制页面的加载.显示.消失:对于一个View或者一个普通的类来 ...
- CGI与fastcgi与php-fpm与php-cgi的关系
cgi是一个协议,它规定了服务器Nginx会将那些数据传送给PHP-cgi fastcgi也可以说是一个协议.fastcgi是对cgi的性能的一次提高.fastcgi会先启动一个master,解析配置 ...
- UVA 12300 Smallest Regular Polygon(正多边形)
题意:给出两点,求经过这两点的正n边形的最小面积 题解:这两点一定是最长的弦,我们设正多边形中点c,找到c到每个点的距离(都相同) 我们知道那个等腰三角形的底与每个角度就使用余弦定理 #include ...
- DXUT源码阅读笔记
14.GetCapture() 函数功能:该函数取得捕获了鼠标的窗口(如果存在)的句柄.在同一时刻,只有一个窗口能捕获鼠标:此时,该窗口接收鼠标的输入,无论光标是否在其范围内.函数原型:HWND Ge ...
- 第二章 Matlab面向对象编程基础
DeepLab是一款基于Matlab面向对象编程的深度学习工具箱,所以了解Matlab面向对象编程的特点是必要的.笔者在做Matlab面向对象编程的时候发现无论是互联网上还是书店里卖的各式Matlab ...
- 通过监听键盘,实现对UITextView的内容移动
视图出现时,增加观察 - (void)viewWillAppear:(BOOL)animated { // 增加对键盘的监听 [[NSNotificationCenter defaultCenter] ...