编写具有单一职责(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 实际 ...
随机推荐
- 剑指Offer-【面试题05:从头到尾打印链表】
package com.cxz.question5; import java.util.Stack; /* * 输入个链表的头结点,从尾到头反过来打印出每个结点的值. * */ public clas ...
- 条码固定资产管理PDA应用
条码固定资产管理解决方案 一.客户挑战与需求 随着企业经营管理的不断升级,固定资产管理的高效化.智能化管理越来越受到企业管理人员的重视.然而,固定资产具有数量大.种类多.价值高.使用周期长.使用地点分 ...
- Greenplum安装
最近需要安装Greenplum测试一些东西,在安装过程中出现了许多问题,所以在这里将安装过程整理一下,主要参考<Greenplum企业应用实践>和http://jxzhfei.blog.5 ...
- 华为荣耀6 H60-L02/L12(联通版)救砖包【适用于无限重启】
本帖最后由 HOT米粒 于 2014-11-16 20:43 编辑 华为荣耀6 H60-L02/L12(联通版)救砖包[适用于无限重启]说明: 1.本工具包用于华为荣耀6 H60-L02(联通版): ...
- ipad上自定义view的旋转适配
ios8横屏时宽高会自动转换,但是ios7不是 CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat scree ...
- js模拟抛出球运动
js练手之模拟水平抛球运动 -匀加速运动 -匀减速运动 模拟运动有些基本的思路,当前所在点的坐标,元素的长宽是多少,向右/向下运动x/y增加,向上/向左运动x/y减少,运动的路程是多少,用什么方程进行 ...
- 利用DelegatingHandler实现Web Api 的Api key校验
客户端在请求Web Api时可以有以下两种方式提供API key 基于Querystring提供Api key http://localhost:57967/Api/Values?key=12345 ...
- Swift的关键字
在声明中使用关键字 let :声明一个常量 var :声明一个变量 class :声明一个类 static :静态的 deinit :反初始化方法?析构方法 init :构造方法?初始化方法 en ...
- java的基础知识运算符
一.运算符. 1.算数运算符:+,-,*,/,% 2.自增自减 :++ ,-- ++在前 先运算在赋值 ++在后 先赋值后运算 -- 减减同上. 3.赋值运算符 : = ,+=,-=,*=,/= 4. ...
- 《HTTP权威指南》大块儿头
看到这样的一本书,胡乱翻开看看里面的内容,我觉得我又浮躁了.真厚啊!能学多少就学多少吧. 看看提要,这本书主要想讲的是HTTP和相关Web技术的.关于这方面的内容,知道的不多.