我对前端代码设计的理解

2,108 阅读9分钟

怎么处理超级大的switch?

考虑有下面的代码:

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里面的代码。

还有一个重要的点是,getRequest里面,随着request的日益增多,switch代码会越来越长,这也导致这里是一个很容易出问题的地方。

为了解决上面的两个问题,试着对RequestManager改造,引入一个对外的接口,内部增加一个map来管理所有的Request对象实现。

改造之后的代码如下:

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。我们来看下怎么添加:

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部分,并没有完全写出,只是作为示例,可以参考别的文章自己实现,应该是比较简单的,或者也可以直接找我交流。

总结

上面的几个例子,只是在平时写代码中,观察到的一些问题,然后写了这篇个人对于这些问题的解决办法,如果你有更好的办法,欢迎指出,谢谢!