首页 > 解决方案 > Typescript - How to correctly achieve code decoupling?

问题描述

My little project is setup with vanilla Typescript and Webpack to build and bundle everything in a single output script.

What i'm trying to achieve is a Factory class that would manage the instantiation of some objects for my main application, i also wish to add a new class that the Factory would be capable of managing without ever having to modify the Factory itself.

For this purpose since Typescript doesn't support reflection to get all implementations of an Interface i have followed the approach suggested in the accepted answer to this question, through annotations and a registry: Typescript - Get all implementations of interface

Which seemed to work fine at first, but now since the modules containing the classes aren't imported anywhere, thus seemingly "unlinked" from the application's entry point, tsc's Tree-Shaking actually prunes these modules from the build process and the code doesn't make it into the final output script. I have read about Annotations breaking Tree-Shaking in Typescript, but this time it looks like it's working fine

This issue reminds me of Components in Angular which have to be manually registered in the application module after declaration

The last thing i've tried was to group all the classes that need to be registered and the Registry in a Namespace, this did work as everything in the namespace was built and made it in the output script, at the expense of having everything in a single File, since from what i have tried and what i understand Multifile namespaces don't really work well in Typescript

Reference: Typescriptlang Namespaces DOC and: How do I use namespaces with TypeScript external modules?

So ultimately, is there something conceptually wrong in my approach? Should i compromise with building a registry of all the modules manually? Or Reference them all directly in my Factory and update it everytime i add a new module?

EDIT 1: I will post an example as requested by @captain-yossarian, the code is almost identical to the first linked question so i thought this would have been superfluous

ITest.ts

export interface ITest{}

TestBase.ts

export class TestBase implements ITest{}

TeastA.ts

import { TestBase } from "TestBase";
import { register } from "TestRegistry";
        
@register("A")
export class TestA extends TestBase{
    constructor(){
        super()
        console.log("Hi, i'm TEST A")
    }
}

TestB.ts and TestC.ts are identical to A, changing just the register and output letter

TestRegistry.ts

import { TestBase } from "TestBase"

export type Constructor<T> = {
    new(): T
    readonly prototype: T
}

export const registry: Map<string, Constructor<TestBase>> = new Map<string, Constructor<TestBase>> ()

export function register(id: string){
    return function<T extends Constructor<TestBase>>(test: T) {
        registry.set(id, test)
    }
}

TestNamespace.ts (contains everything seen above in a separate namespace)

export namespace TestNamespace{

    export interface ITest{}

    export class TestBase implements ITest{}

    @register("A")
    export class TestA extends TestBase{
        constructor(){
            super()
            console.log("Hi, i'm TEST A")
        }
    }

    @register("B")
    export class TestB extends TestBase{
        constructor(){
            super()
            console.log("Hi, i'm TEST B")
        }
    }

    @register("C")
    export class TestC extends TestBase{
        constructor(){
            super()
            console.log("Hi, i'm TEST C")
        }
    }

    export type Constructor<T> = {
        new(): T
        readonly prototype: T
    }
    
    export const registry: Map<string, Constructor<TestBase>> = new Map<string, Constructor<TestBase>> ()
    
    export function register(id: string){
        return function<T extends Constructor<TestBase>>(test: T) {
            registry.set(id, test)
        }
    }
}

TestFactory.ts

import { ITest } from "ITest"
import { TestBase } from "TestBase"
import { TestNamespace } from "TestNamespace"
import { Constructor, registry } from "TestRegistry"

export class TestFactory{
    public constructor(){

    }

    public getTestInstance(id: string): ITest{
        let testConstructor: Constructor<TestBase> = registry.get(id)
        let test: ITest = new testConstructor() || new TestBase() 
        return test
    }

    public getTestInstanceFromNamespace(id: string) : TestNamespace.ITest{
        let testConstructor: Constructor<TestNamespace.TestBase> = TestNamespace.registry.get(id)
        let test: TestNamespace.ITest = new testConstructor() || new TestNamespace.TestBase() 
        return test
    }
}

TestScript.ts

import {TestFactory} from "TestFactory"

var testFactory = new TestFactory();

testFactory.getTestInstance("A") // When compiled and run this should return errors for TestFactory
testFactory.getTestInstance("B") // line 12 "testConstructor is not a constructor" as the registry will be empty
testFactory.getTestInstance("C") // the code for TestA, TestB and TestC is not built

testFactory.getTestInstanceFromNamespace("A")//Output should be: "Hi, i'm TEST A"
testFactory.getTestInstanceFromNamespace("B")//Hi, i'm TEST B
testFactory.getTestInstanceFromNamespace("C")//Hi, i'm TEST C
//The second approach should work because once referenced the whole Namespace is built into the final output file

If the provided code is too much i could make a sample project on github

标签: typescriptwebpack

解决方案


推荐阅读