擴展性是衡量代碼質量最重要的標準之一,代碼的擴展性好了,出bug的幾率就小很多。在經典的設計模式中,大部分設計模式都是為了解決代碼的擴展性而存在的,主要遵從的原則就是開閉原則,所以理解開閉原則并能靈活應用很重要。
如何理解開閉原則
英文全稱:Open Closed Principle(OCP)。開閉原則是對什么開放又是對什么關閉呢?是對擴展開放,對修改關閉。詳細描述一下就是:添加一個新的功能時,應該在已有代碼的基礎上擴展代碼(新增模塊、類、方法等),而非修改已有代碼(修改模塊、類、方法等)。
這條原則的意義是什么呢?對擴展開放是為了應對需求變化,對修改關閉是為了保證原有代碼的穩定性
。在識別出代碼可變部分和不可變部分之后,要將可變部分封裝起來,隔離變化,提供抽象化的不可變接口,給上層使用。
我們來看一個栗子,理解一下這個原則。
我們在登陸一個系統的時候,都會輸入用戶名和密碼然后點擊登陸。在正式登陸之前,都會對輸入的用戶名和密碼做校驗,看是否符合規則,比如:
function checkLogin() {
const username = document.querySelector('#username').value;
if (!username) {
// ...
} else {
// ...
}
const pwd = document.querySelector('#pwd').value;
if (!pwd){
// ...
} else {
// ...
}
}
我們只提取了主要的業務邏輯check函數,里面對兩個字段做了規則校驗。
現在我們需要增加一個在登陸前輸入驗證碼的功能。該如何改動代碼呢?在這里就是在checkLogin函數中再增加一段對驗證碼做校驗的代碼。
function checkLogin() {
const username = document.querySelector('#username').value;
if (!username) {
// ...
} else {
// ...
}
const pwd = document.querySelector('#pwd').value;
if (!pwd){
// ...
} else {
// ...
}
const captcha = document.querySelector('#captcha').value;
if (!captcha) {
// ...
} else {
// ...
}
}
這樣實現的問題是什么呢?一方面,如果驗證規則很多,那這段代碼就會很長很復雜,降低了可讀性和可維護性。再者,如果有對應的單元測試,那每次修改單元測試也要修改。
上面的代碼是基于“修改”的方式來實現新功能的,因為新功能的實現是在原有的方法中進行了修改,而非新增模塊、類、方法等。那如何通過擴展的方式實現上面的功能呢?
class Verification {
private verifiedHandlers: VerifiedHandler[] = [];
public addVerifiedHandler(handler: VerifiedHandler) {
this.verifiedHandlers.push(handler)
}
public checkAll() {
for (let i = 0; i < this.verifiedHandlers.length; i++) {
this.verifiedHandlers[i].check();
}
}
}
interface VerifiedHandler {
check(): void;
}
class UsernameVerifiedHandler implements VerifiedHandler {
public check() {
console.log('username');
}
}
class PwdVerifiedHandler implements VerifiedHandler {
public check() {
console.log('pwd');
}
}
class CaptchaVerifiedHandler implements VerifiedHandler {
public check() {
console.log('captcha');
}
}
在上面的實現中,定義接口VerifiedHandler,里面包含一個check方法,將對各個字段的校驗移到每個handler中,并實現接口VerifiedHandler。
定義Verification類,暴露出addVerifiedHandler方法,以便添加不同的handler,并定義checkAll方法,調用所有handler的check方法。
各個handler已經定義好了,具體該如何使用呢?
class ApplyVerification {
private verification: Verification = new Verification();
private static instance: ApplyVerification;
private constructor() {
this.init();
}
public init() {
this.verification.addVerifiedHandler(new UsernameVerifiedHandler());
this.verification.addVerifiedHandler(new PwdVerifiedHandler());
this.verification.addVerifiedHandler(new CaptchaVerifiedHandler());
}
public getVerification() {
return this.verification;
}
public static getInstance() {
if (!ApplyVerification.instance) {
ApplyVerification.instance = new ApplyVerification();
}
return ApplyVerification.instance;
}
}
function checkLogin() {
ApplyVerification.getInstance().getVerification().checkAll();
}
ApplyVerification是一個單例類,創建Verification并添加不同的handler。
現在的代碼再想添加新的校驗邏輯只需要創建新的handler即可,最容易變化的部分是基于擴展的方式添加新功能而非修改原有代碼。
如果覺得上述實現方式太過復雜,把不同的校驗邏輯封裝到不同的方法中也是可以的。
有時候,代碼的可擴展性跟可讀性不能兩者兼顧,要做一些權衡和取舍。在if判斷不是很多的時候,寫在一起也沒有太大問題,當if判斷很多很復雜的時候,自然要選用可擴展性更好的方式。
如何做到“對擴展開放、對修改關閉”
最常用來提高代碼擴展性的方法有:多態、基于接口而非實現編程、依賴注入,以及大部分設計模式(比如:裝飾、策略、模板、職責鏈、狀態等)。
最重要的是要時刻具備擴展意識、抽象意識、封裝意識。寫代碼的時候多向前思考,可能會有哪些需求變更。不過也經常會有考慮不全的情況,只能通過不斷重構來保持代碼的可擴展性。