考慮有下面的代碼: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,來註冊便可,這樣,就真正的實現了上面咱們說的,針對接口編程,對修改關閉,對新增開發的設計原則。
考慮以下的代碼:
// 分類接口
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部分,並無徹底寫出,只是做爲示例,能夠參考別的文章本身實現,應該是比較簡單的,或者也能夠直接找我交流。
上面的幾個例子,只是在平時寫代碼中,觀察到的一些問題,而後寫了這篇我的對於這些問題的解決辦法,若是你有更好的辦法,歡迎指出,謝謝!