這兩周我需要對一個歷史遺留的功能做一些擴展,正如很多人不願意碰這些歷史遺留的代碼一樣,我的內心也同樣對這樣的任務充滿反抗。這些代碼中充斥着各種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設計的依據。
