我對前端代碼設計的理解

怎麼處理超級大的switch?

考慮有下面的代碼:typescript

interface IRequest {
    request: () => void;
}


class ARequest implements IRequest {
    request() {
        console.log('A request impl.');
    }
}


class BRequest implements IRequest {
    request() {
        console.log('B request impl.');
    }
}

enum RequestType {
    A,
    B,
}

class RequestManager {
    getRequest(type: RequestType) {
        switch(type) {
            case RequestType.A:
                return new ARequest();
            case RequestType.B:
                return new BRequest();
        }
    }
}

複製代碼

上面只是列了兩個case,但實際項目中,上面可能有幾十個case。 想象下,若是這個時候你須要添加一個新的request類的實現,你須要改動RequestType,RequestManager,好比新增一個CRequest。編程

enum RequestType {
    A,
    B,

    C,
}

class CRequest implements IRequest {
    request() {
        console.log('C request impl.');
    }
}

class RequestManager {
    getRequest(type: RequestType) {
        switch(type) {
            case RequestType.A:
                return new ARequest();
            case RequestType.B:
                return new BRequest();
            case RequestType.C:
                return new CRequest();
        }
    }
}
複製代碼

在軟件工程當中,咱們提倡對修改關閉,對新增開放,也就是所謂的開發關閉原則。那上面的例子,其實不符合這個設計原則。由於每次添加新的Requst實現,都會致使去修改RequestManager,咱們更提倡的應該是,面向接口編程,RequestManager應該是一個比較穩定的類,咱們應該保證,在後續添加新的實現的時候,不須要改動到RequestManager類,而是經過RequestManager類提供的接口,來動態改變RequestManager內部的狀態,而不是直接修改RequestManager裏面的代碼。app

還有一個重要的點是,getRequest裏面,隨着request的日益增多,switch代碼會愈來愈長,這也致使這裏是一個很容易出問題的地方。ui

爲了解決上面的兩個問題,試着對RequestManager改造,引入一個對外的接口,內部增長一個map來管理全部的Request對象實現。this

改造以後的代碼以下:spa

class RequestManager {

    private requestMap = new Map<RequestType, IRequest>();

    constructor() {
        this.initDefaultRequest();
    }

    private initDefaultRequest() {
        this.registerRequest(RequestType.A, new ARequest());
        this.registerRequest(RequestType.B, new BRequest());
    }
    getRequest(type: RequestType) {
        return this.requestMap.get(type);
    }

    public registerRequest(type: RequestType, request: IRequest) {
        this.requestMap.set(type, request);
    }
}
複製代碼

上面的代碼中,全部的request對象,經過map來維護,在RequestManager中能夠初始化一些默認的request,供外部直接使用,同時,對外提供registerRequest的方法,供外部擴展。設計

修改爲上面的代碼以後,如今,若是咱們要添加一個request,好比就是剛剛的CRequest。咱們來看下怎麼添加:code

class App {
    public readonly requestManager = new RequestManager();
    init() {
        this.requestManager.registerRequest(RequestType.C, new CRequest());
    }
}


class Bussiness {
  	private app: App = new App();
    init() {
       this.app.requestManager.registerRequest(RequestType.C, new CRequest());
   }
}
複製代碼

好比以上例子,App類表明整個應用,在應用下面,咱們有N個模塊,requestManager是其中的一個模塊,那這個時候,外部若是想用, 咱們只須要要使用app對象下的requestManager提供的registerRequest方法,用來註冊新的Request就好了,這樣,在業務方來看,並無去修改應用自己的代碼,同時,應用提供了統一的對外接口,應用裏面的模塊也更加高內聚,更加穩定。這也就是上面說的,只對新增開放,對修改關閉。就是這個意思。對象

上面的例子中,其實還有一個問題,那就是若是咱們要添加一個類型,咱們須要修改RequestType這個枚舉類,每次新增都須要添加一個枚舉元素,雖然這裏修改很是簡單,引入bug的可能性很是小,可是有沒有辦法杜絕這個問題呢?繼承

咱們來看下面的代碼:

interface IRequest {
    request: () => void;

    getRequestType: () => IRequestType;
}


class ARequest implements IRequest {
    private static REQUEST_TYPE: IRequestType = { type: 'a' };
    request() {
        console.log('A request impl.');
    }

    getRequestType() {
        return ARequest.REQUEST_TYPE;
    }
}


class BRequest implements IRequest {
    private static REQUEST_TYPE: IRequestType = { type: 'b' };
    request() {
        console.log('B request impl.');
    }

    getRequestType() {
        return BRequest.REQUEST_TYPE;
    }
}
interface IRequestType {
    type: string;
}


class CRequest implements IRequest {
    private static REQUEST_TYPE: IRequestType = { type: 'c' };
    request() {
        console.log('C request impl.');
    }

    getRequestType() {
        return CRequest.REQUEST_TYPE;
    }
}

class RequestManager {

    private requestMap = new Map<IRequestType, IRequest>();

    constructor() {
        this.initDefaultRequest();
    }

    private initDefaultRequest() {
        this.registerRequest(new ARequest());
        this.registerRequest(new BRequest());
    }
    getRequest(type: IRequestType) {
        return this.requestMap.get(type);
    }

    public registerRequest(request: IRequest) {
        this.requestMap.set(request.getRequestType(), request);
    }
}


class App {
    public readonly requestManager = new RequestManager();
}


class Bussiness {
  	private app: App = new App();
    init() {
       this.app.requestManager.registerRequest(new CRequest());
   }
}


複製代碼

在上面的代碼中,首先咱們經過給接口IRequest添加getRequestType方法,這個方法主要用來約束實現類的,就是說,實現類必須實現這個方法,而後,返回值類型,咱們定義了IRequestType,這裏一樣也是約束返回值的類型,這樣,在編寫實現的時候,代碼就能夠按照事先定義好的約束來寫,這樣能夠增長代碼的統一性,同時,也可以避免你們代碼寫的不一致,致使維護性不好的問題。由於咱們的約束定義好了,實現的話,那就是「戴着腳鐐跳舞」。

再來看上面的代碼,是否解決了上面的問題呢?我想應該是解決了,如今,若是咱們要添加一個新功能,而這個新功能,咱們已有的request實現沒法知足的話,那就須要添加新的request實現,這個實現,添加一個實現,咱們只須要從新寫一個request的實現,而後調用app.requestManager.registerRequest,來註冊便可,這樣,就真正的實現了上面咱們說的,針對接口編程,對修改關閉,對新增開發的設計原則。

使用instanceof判斷類型,而後添加操做?

考慮以下的代碼:

// 分類接口
interface ICategory {
    doSomething: () => void;
}

// 分類A
class ACategory implements ICategory {
    doSomething() {
        console.log('A doSomething...')
    }
}

// 分類B
class BCategory implements ICategory {
    doSomething() {
        console.log('B doSomething...')
    }
}



class CategoryEngine {
    private categories: ICategory[] = [];
    constructor() {
        this.categories = [new ACategory(), new BCategory()];
    }
    handle() {
        this.categories.forEach((category: ICategory) => {
            if (category instanceof ACategory) {
                this.doSomething2(category);
            }
            category.doSomething();
            // ....
        })
    }
    doSomething2(category: ICategory) {
        // do something else
        return category;
    }
}
複製代碼

上面的代碼中,咱們定義了一個ICategory(類別)的接口,而後有兩個類別的實現,而後在CategoryEngine咱們試圖對全部存在的類別作一些事情,固然這裏只是舉一個例子,實際的項目當中,多是其餘相似的問題。在handle方法中,咱們遍歷全部的類別,來作一些事情,注意觀察,在forEach內部,咱們使用了instanceof來判斷category是否爲ACategory,而後來調用CategoryEngine裏面的方法來特殊處理。

那上面的代碼有什麼問題,其實有問題的。假設,我後面須要添加一個CCategory,而且,這個CCategory也須要特殊處理,那怎麼辦呢?這個時候,就又要修改CategoryEngine中的代碼,修改handle方法中的if判斷,而後判斷是否爲CCategory,好比像下面這樣:

// 分類接口
interface ICategory {
    doSomething: () => void;
}

// 分類A
class ACategory implements ICategory {
    doSomething() {
        console.log('A doSomething...')
    }
}

// 分類B
class BCategory implements ICategory {
    doSomething() {
        console.log('B doSomething...')
    }
}

// 分類C
class CCategory implements ICategory {
    doSomething() {
        console.log('C doSomething...')
    }
}

class CategoryEngine {
    private categories: ICategory[] = [];
    constructor() {
        this.categories = [new ACategory(), new BCategory()];
    }
    handle() {
        this.categories.forEach((category: ICategory) => {
            if (category instanceof ACategory) {
                this.doSomething2(category);
            } else if (category instanceof CCategory) {
                this.doSomething3(category);
            }
            category.doSomething();
            // ....
        })
    }
    doSomething2(category: ICategory) {
        // do something else
        return category;
    }

    doSomething3(category: ICategory) {
        // do something else
        return category;
    }
}
複製代碼

代碼因而變成了上面這個樣子,那後面若是須要繼續判斷呢?那隻能繼續修改這個類了。那這個類,出現問題的風險,也就越大。

對於上面的問題,咱們應該怎麼來處理呢?

看下面的代碼:

// 分類接口
interface ICategory {
    doSomething: () => void;

    preDo: () => ICategory;
}

// 分類A
class ACategory implements ICategory {
    doSomething() {
        console.log('A doSomething...')
    }

    preDo() {
        console.log('A pre do...');
        return this;
    }
}

// 分類B
class BCategory implements ICategory {
    doSomething() {
        console.log('B doSomething...')
    }
    preDo() {
        console.log('B pre do...');
        return this;
    }
}

// 分類C
class CCategory implements ICategory {
    doSomething() {
        console.log('C doSomething...')
    }
    preDo() {
        console.log('C pre do...');
        return this;
    }
}

class CategoryEngine {
    private categories: ICategory[] = [];
    constructor() {
        this.categories = [new ACategory(), new BCategory()];
    }
    handle() {
        this.categories.forEach((category: ICategory) => {
            category.preDo();
            category.doSomething();
            // ....
        })
    }
}
複製代碼

看上面的代碼,咱們在ICategory中添加了一個新的方法,preDo(名字隨便取的)方法。在CategoryEngine中,咱們對於每一個category,都直接先調用preDo,而後調用doSomething方法,能夠發現,如今沒有if判斷了,以前在if判斷以後,針對特定category的處理,都收到category的具體實現內部取了,而對於不須要特殊處理的,咱們只須要在具體的category裏面,實現一個空的方法便可。固然,你也能夠,定義一個AbstractCategory的抽象類,實現ICategory接口,其餘全部的實現,繼承這個抽象類,這樣,若是不須要特殊處理,preDo接口就都不用覆寫了。

上面的例子,一樣也說明了,對修改關閉,對新增開放的設計原則。

依賴怎麼處理?

目前,騰訊文檔表格中,不少以下的代碼:

interface A {

}

interface B {

}

interface C {

}

interface IDependency {
    a: A;
    b:B;
    c: C;
}


class SubModule1 {
    private a: A;
    constructor(dependency: IDependency) {
        this.a = dependency.a;
    }
}

class SubModule2 {
    private a: A;
    private b: B;
    constructor(dependency: IDependency) {
        this.a = dependency.a;
        this.b = dependency.b;
    }
}

class ModuleA {
    private subModule1: SubModule1;
    private subModule2: SubModule2;
    constructor(dependency: IDependency) {
        this.subModule1 = new SubModule1(dependency);
        this.subModule2 = new SubModule2(dependency);
    }
}
複製代碼

上面的代碼中,在ModuleA中,有兩個子模塊,同時,這兩個子模塊,依賴的外部模塊不一樣,可是在ModuleA中的時候,看起來,ModuleA的依賴是全部子模塊的依賴的集合,可是這裏有個問題就是,我須要在每一個模塊以及子模塊中,都定義須要依賴的模塊對象,同時,還須要外部一層層傳遞依賴進來。另外一個問題是,兩個子模塊依賴的東西不同,可是缺同時將全部父模塊的依賴所有傳遞給了子模塊,這就致使一個問題,傳遞進去的全部依賴,哪一個子模塊用了哪些,外部並不清楚,並且假設,後續,有的模塊不要這個依賴了,可是在ModuleA中,你並不敢直接去掉,你須要到每個子模塊中去確認,是否還有使用到。

那針對上面的問題,應該怎麼處理呢?咱們知道,在Angular2中,引入了DI的機制。那若是,咱們的項目當中,也有相似的機制,那代碼就變成以下這樣了:

interface A {

}

interface B {

}

interface C {

}

// 存放全部實例對象(demo)
const Annotation = new Map();
export const Autowired = () => {
    return (target: any, name: string) => {
        target[name] = Annotation.get(name);
        if (!target[name]) {
            throw new Error(`屬性${name}沒有可用的實例。`);
        }
    };
};

class SubModule1 {

    @Autowired()
    private a: A;
}

class SubModule2 {
    @Autowired()
    private a: A;
    @Autowired()
    private b: B;
}

class ModuleA {
    @Autowired()
    private subModule1: SubModule1;
    @Autowired()
    private subModule2: SubModule2;
}
複製代碼

能夠看出,上面的代碼中,每個模塊,對本身依賴的模塊,都很是清楚,並且再也不須要層層傳遞依賴進去,代碼量也減小了很是多。

上面的DI部分,並無徹底寫出,只是做爲示例,能夠參考別的文章本身實現,應該是比較簡單的,或者也能夠直接找我交流。

總結

上面的幾個例子,只是在平時寫代碼中,觀察到的一些問題,而後寫了這篇我的對於這些問題的解決辦法,若是你有更好的辦法,歡迎指出,謝謝!

相關文章
相關標籤/搜索