这两周我需要对一个历史遗留的功能做一些扩展,正如很多人不愿意碰这些历史遗留的代码一样,我的内心也同样对这样的任务充满反抗。这些代码中充斥着各种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中通过“发送消息”来代替“方法调用”。虽然他们最终结果是一样的,但是“发送消息”这一思想需要我们来仔细品味:
使用Objective-C编写的用户注册类:
@implementation UserRegistry - (void)reg:(User *)user{ BOOL *isInvalidEmail=[self isInvalidEmail:user.email];//①向用户中心发送isInvalidEmail消息 BOOL *isInvalidPhone=[self isInvalidPhone:user.phone];//②向用户中心发送isInvalidPhone消息 //下面的代码用来抛出异常,不用关心 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 alloc] init];//初始化一个UserRepository对象 [userRepository saveWithUser:user];//③向userRepository发送saveWithUser消息 } - (BOOL *)isInvalidEmail:(NSString *)email{ return false; } - (BOOL *)isInvalidPhone:(NSString *)phone{ return false; } @end
由于代码着色工具不能对上面的代码着色,所以不愿意阅读这段代码的朋友只需要理解在Objective-C使用发送消息的方式而不是方法调用即可。让我们通过“发送消息”的方式来解读这一段代码:
①:向“用户注册中心”发送“验证email”的消息;
②向“用户注册中心”发送“验证电话号码”的消息;
③向“用户仓储”发送“保存用户”的消息;
我们推断一下“用户注册中心”应该具备的能力可能是:“注册用户”,“注销用户”,“这样的用户能够注册吗?”。推断一个对象应该具备的能力跟对象的命名有很大关系,一个名叫UserSearchService的对象应该具备的能力可能是“搜索用户”,一个名叫UserProvider的对象应该具备的能力可能是“获取用户”。
当你向一个名叫“用户注册中心”的对象发送一个“验证email”的消息后,他很可能不会搭理你,这意味这我们写的代码职责不够清晰,同时也暗示我们需要增加能够响应此消息的抽象:
经过一番分析,代码变成了:
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容器实现注入即可,另外此时的代码也具备很好的测试性。
当你熟悉“发送消息”这种思想之后,相同的思路也可以用在“方法调用”上。
不过每次我觉得我对面向对象思想已经领会的很好了,但是隔一段时间又会有新的体会。所以本文所描述的想法仅代表目前阶段我对单一职责的理解,也许一段时间过后我会对单一职责又有新的看法。
你是如何看待本文所描述的想法呢?