这两周我需要对一个历史遗留的功能做一些扩展,正如很多人不愿意碰这些历史遗留的代码一样,我的内心也同样对这样的任务充满反抗。这些代码中充斥着各种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. php结合md5的加密解密算法实例

    <?php /* * Created on 2016-12-22 * */ function encrypt($data, $key){ $key = md5($key); $x = 0; $l ...

  2. 外接Hdmi没有声音怎么设置

    参考 http://www.3lian.com/edu/2013/07-16/81152.html 在小喇叭图标上右键->播放设备->禁用HDMI

  3. Swif - 可选型

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px Menlo; color: #4dbf56 } p.p2 { margin: 0.0px 0. ...

  4. 常用的sublime text插件(很爽哦)

    个人比较懒,平时喜欢用webstorm,但是因为webstorm打开实在太慢了,并且太看设备,所以本人编辑简单的文件依然会选择使用sublime,虽然网上有很多关于此类插件的分享了,但是感觉都是片段, ...

  5. 使用Packet Sniffer抓包和分析(z-stack协议)

    以下内容仅是自己学习总结,可能会有错误,有发现问题的欢迎指正(图片可以自己放大,还是比较清晰的). 1.协调器上电,其他设备均不上电,抓包如下: 通过观察可以发现,协调器建立网络成功后,会以15秒为周 ...

  6. Open 语法的使用

    我们通常会需要在命令中,打开文件输入信息,在python中我们就会使用open语法,进行此方面的操作.详细方式如下:#Python open 函数# 作用:打开一个文件# 语法:open(file[, ...

  7. ListFragment源码 (待分析)

    /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Versi ...

  8. 获取终端ip地址

    网上找的,记录下 import java.io.*; import java.net.*; import java.util.*; //import org.apache.http.conn.util ...

  9. ssh自动输入密码脚本 切换目录脚本

    利用expect的,首先查看expect,命令:which expect #!/usr/bin/expect -f spawn ssh 用户名@ip地址 expect "assword:&q ...

  10. 踏上Salesforce的学习之路(一)

    相信通过前面的学习,大家已经拥有了一个属于自己的Salesforce开发者账号,下面,我们将用这个账号正式踏上Salesforce的学习之路. 首先,点击网址:https://developer.sa ...