这两周我需要对一个历史遗留的功能做一些扩展,正如很多人不愿意碰这些历史遗留的代码一样,我的内心也同样对这样的任务充满反抗。这些代码中充斥着各种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)的类的更多相关文章

  1. S.O.L.I.D五大原则之单一职责SRP

    转自 : 汤姆大叔的blog Bob大叔提出并发扬了S.O.L.I.D五大原则,用来更好地进行面向对象编程,五大原则分别是: The Single Responsibility Principle(单 ...

  2. 深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP

    前言 Bob大叔提出并发扬了S.O.L.I.D五大原则,用来更好地进行面向对象编程,五大原则分别是: The Single Responsibility Principle(单一职责SRP) The ...

  3. 设计原则:单一职责(SRP)原则

    1 什么是单一职责(SRP)原则 单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP.翻译过来就是:一个类或者模块只负责完成一个职责(或者功能). 所 ...

  4. 面象对象设计原则之一:单一职责原则(Single Responsibility Principle, SRP)

    单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小.单一职责原则定义如下:单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域 ...

  5. 设计模式原则(1)--Single Responsibility Principle(SRP)--单一职责原则

    1.定义: 不要存在多于一个导致类变更的原因.通俗的说,即一个类只负责一项职责.  2.使用场景: 如果类A有两个职责:d1,d2.当职责d1需要修改时,可能会导致原本运行正常的职责d2功能产生问题. ...

  6. 设计模式六大原则(一):单一职责原则(Single Responsibility Principle)

    单一职责(SRP)定义: 不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责. 问题由来: 类T负责两个不同的职责:职责P1,职责P2.当由于职责P1需求发生改变而需要修改类T时,有可 ...

  7. 北风设计模式课程---单一职责原则(Single Responsibility Principle)

    北风设计模式课程---单一职责原则(Single Responsibility Principle) 一.总结 一句话总结: 一个类应该有且只有一个变化的原因:单一职责原则(SRP:Single Re ...

  8. 软件开发中的单一职责(转至INFOQ)

    最近在实践微服务化过程中,对其“单一职责”原则深有体会.那么只有微服务化才可以单一职责,才可以解耦吗?答案是否定的. 单一职责原则是这样定义的:单一的功能,并且完全封装起来. 我们做后端Java开发的 ...

  9. 设计模式课程 设计模式精讲 3-6 单一职责原则Coding

    1 要点讲解 1.1 需要注意 2 代码演练 2.1 类的单一职责原则demo 2.2 接口的单一职责原则demo 2.3 方法的单一职责原则demo 1 要点讲解 1.1 需要注意 1.1.1 实际 ...

随机推荐

  1. Orchard教程索引页

    Orchard官方教程(译)索引 链接标注 原文 则表示未译,其他带有中文标题的表示译文内容. 入门 安装Orchard--Installing Orchard 通过zip包手动安装Orchard-- ...

  2. SQL参数化查询自动生成SqlParameter列表

    string sql = @"INSERT INTO stu VALUES (@id,@name) "; 参数化查询是经常用到的,它可以有效防止SQL注入.但是需要手动去匹配参数@ ...

  3. 整理几种在axure里使页面居中的方法

    1. 用动态面板固定浏览器功能. 很简单方便. 但缺点是 当浏览器窗口大小小于页面时, 由于会强制居中,导致页面2边是在显示范围外并且是无法通过滚动条滚动的(滚动条是没有的). 2. 使用页面属性里的 ...

  4. JavaScript isNaN() 函数

    定义与用法: isNaN() 函数用于检查其参数是否是非数字值. 语法: isNaN(x) 描述:     x是要检测的值. 返回值: 如果 x 是特殊的非数字值 NaN(或者能被转换为这样的值),返 ...

  5. 安卓四核PDA手持PDA智能POS机 打印二维码 分享

    很多项目都会用到 类似的要求  移动手持终端 通过程序 可以生成条码或二维码 打印出小票或标签纸 下面直接上代码 希望对大家有点用处 private void print(){ csys.setTex ...

  6. RTX二次开发集成

    1,rtx服务器端有很多端口,二次发的程序与这些打开的端口交互.打开端口的方法在rtx服务管理器中,默认http服务未启用.需要手动启用http端口如下: 如果打开rtx服务器没有启用http的801 ...

  7. yarn关于app max attempt深度解析,针对长服务appmaster平滑重启

    在YARN上开发长服务,需要注意fault-tolerance,本篇文章对appmaster的平滑重启的一个参数做了解析,如何设置可以有助于达到appmaster平滑重启. 在yarn-site.xm ...

  8. 关于android 加载https网页的问题

    我在加载https网页时出现空白, 因此,我就百度一下,可以发现: webView.setWebViewClient(new WebViewClient(){ @Override public voi ...

  9. 2016 Multi-University Training Contest 1 G. Rigid Frameworks

    Rigid Frameworks Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) ...

  10. CI框架之HOOKS使用流程及原理

        Ci框架中Hooks可以理解:在框架的执行流程过程中,允许开发者在固定的某些时间点上(如:调用控制器前,调用控制器后等时间点上),调用其他函数来扩充CI框架执行流程的一种方法.技术上来就是通过 ...