Inversion de contrôle sur TypeScript nu sans douleur

Bonjour, je m'appelle Dmitry Karlovsky et (pour autant que je m'en souvienne) je me bats avec mon environnement. Après tout, il est tellement os, chêne, et il ne comprend jamais ce que j'en veux. Mais à un moment donné, j'ai réalisé qu'il suffisait de le supporter et qu'il fallait changer quelque chose. Par conséquent, ce n'est pas maintenant l'environnement qui me dicte ce que je peux et ne peux pas faire, mais je dicte à l'environnement ce qu'il devrait être.





Comme vous l'avez déjà compris, nous parlerons plus loin de l'inversion du contrôle à travers le "contexte d'environnement". Beaucoup de gens connaissent déjà cette approche à partir des «variables d'environnement» - elles sont définies au démarrage du programme et sont généralement héritées pour tous les programmes qu'il démarre. Nous utiliserons ce concept pour organiser notre code TypeScript.





Alors ce que nous voulons obtenir:





  • Les fonctions, lorsqu'elles sont appelées, héritent du contexte de la fonction appelante.





  • Les objets héritent du contexte de leur objet propriétaire





  • Un système peut avoir plusieurs options de contexte en même temps





  • Les changements dans les contextes dérivés n'affectent pas l'original





  • Les changements dans le contexte d'origine se reflètent dans les dérivés





  • Les tests peuvent s'exécuter dans un contexte isolé et non isolé





  • Chaudière minimum





  • Performance maximum





  • Le typecheck de tout





, - :





namespace $ {
    export let $user_name: string = 'Anonymous'
}

      
      



- . , :





namespace $ {
    export function $log( this: $, ... params: unknown[] ) {
        console.log( ... params )
    }
}

      
      



this



. , :





$log( 123 ) // Error

      
      



- . , :





$.$log( 123 ) // OK

      
      



, $



- , . :





namespace $ {
    export type $ = typeof $
}

      
      



this



, . , , :





namespace $ {
    export function $hello( this: $ ) {
        this.$log( 'Hello ' + this.$user_name )
    }
}

      
      



. , . , , , :





namespace $ {
    export function $ambient(
        this: $,
        over = {} as Partial< $ >,
    ): $ {
        const context = Object.create( this )
        for( const field of Object.getOwnPropertyNames( over ) ) {
            const descr = Object.getOwnPropertyDescriptor( over, field )!
            Object.defineProperty( context, field, descr )
        }
        return context
    }
}

      
      



Object.create



, , . Object.assign



, , , . , :





namespace $.test {
    export function $hello_greets_anon_by_default( this: $ ) {

        const logs = [] as unknown[]
        this.$log = logs.push.bind( logs )

        this.$hello()
        this.$assert( logs, [ 'Hello Anonymous' ] )

    }
}

      
      



, - $log



, . , , , , . :





namespace $ {
    export function $assert< Value >( a: Value, b: Value ) {

        const sa = JSON.stringify( a, null, '\t' )
        const sb = JSON.stringify( b, null, '\t' )

        if( sa === sb ) return
        throw new Error( `Not equal\n${sa}\n${sb}`)

    }
}

      
      



, $.$test



. , :





namespace $ {
    export async function $test_run( this: $ ) {

        for( const test of Object.values( this.$test ) ) {
            await test.call( this.$isolated() )
        }

        this.$log( 'All tests passed' )
    }
}

      
      



, . , , ( , , , , ..). , :





namespace $ {
    export function $isolated( this: $ ) {
        return this.$ambient({})
    }
}

      
      



$log



, - . , $isolated



, $log



:





namespace $ {
    const base = $isolated
    $.$isolated = function( this: $ ) {
        return base.call( this ).$ambient({
            $log: ()=> {}
        })
    }
}

      
      



, $log



.





, :





namespace $.test {
    export function $hello_greets_overrided_name( this: $ ) {

        const logs = [] as unknown[]
        this.$log = logs.push.bind( logs )

        const context = this.$ambient({ $user_name: 'Jin' })
        context.$hello()
        this.$hello()

        this.$assert( logs, [ 'Hello Jin', 'Hello Anonymous' ] )

    }
}

      
      



. :





namespace $ {
    export class $thing {
        constructor( private _$: $ ) {}
        get $() { return this._$ }
    }
}

      
      



. , . , . , , , :





namespace $ {
    export class $hello_card extends $thing {

        get $() {
            return super.$.$ambient({
                $user_name: super.$.$user_name + '!'
            })
        }

        get user_name() {
            return this.$.$user_name
        }
        set user_name( next: string ) {
            this.$.$user_name = next
        }

        run() {
            this.$.$hello()
        }

    }
}

      
      



, , :





namespace $.test {
    export function $hello_card_greets_anon_with_suffix( this: $ ) {

        const logs = [] as unknown[]
        this.$log = logs.push.bind( logs )

        const card = new $hello_card( this )
        card.run()

        this.$assert( logs, [ 'Hello Anonymous!' ] )

    }
}

      
      



, , . , , . , :





namespace $ {
    export class $hello_page extends $thing {

        get $() {
            return super.$.$ambient({
                $user_name: 'Jin'
            })
        }

        @ $mem
        get Card() {
            return new this.$.$hello_card( this.$ )
        }

        get user_name() {
            return this.Card.user_name
        }
        set user_name( next: string ) {
            this.Card.user_name = next
        }

        run() {
            this.Card.run()
        }

    }
}

      
      



. . $mem



. :





namespace $ {
    export function $mem(
        host: object,
        field: string,
        descr: PropertyDescriptor,
    ) {
        const store = new WeakMap< object, any >()

        return {
            ... descr,
            get() {

                let val = store.get( this )
                if( val !== undefined ) return val

                val = descr.get!.call( this )
                store.set( this, val )

                return val
            }
        }

    }
}

      
      



WeakMap



, . , , , :





namespace $.test {
    export function $hello_page_greets_overrided_name_with_suffix( this: $ ) {

        const logs = [] as unknown[]
        this.$log = logs.push.bind( logs )

        const page = new $hello_page( this )
        page.run()

        this.$assert( logs, [ 'Hello Jin!' ] )

    }
}

      
      



, . - . , , .





namespace $ {
    export class $app_card extends $.$hello_card {

        get $() {
            const form = this
            return super.$.$ambient({
                get $user_name() { return form.user_name },
                set $user_name( next: string ) { form.user_name = next }
            })
        }

        get user_name() {
            return super.$.$storage_local.getItem( 'user_name' ) ?? super.$.$user_name
        }
        set user_name( next: string ) {
            super.$.$storage_local.setItem( 'user_name', next )
        }

    }
}

      
      



- :





namespace $ {
    export const $storage_local: Storage = window.localStorage
}

      
      



, , , :





namespace $ {
    const base = $isolated
    $.$isolated = function( this: $ ) {

        const state = new Map< string, string >()
        return base.call( this ).$ambient({

            $storage_local: {
                getItem( key: string ){ return state.get( key ) ?? null },
                setItem( key: string, val: string ) { state.set( key, val ) },
                removeItem( key: string ) { state.delete( key ) },
                key( index: number ) { return [ ... state.keys() ][ index ] ?? null },
                get length() { return state.size },
                clear() { state.clear() },
            }

        })

    }
}

      
      



, , , $hello_card



$app_card



, .





namespace $ {
    export class $app extends $thing {

        get $() {
            return super.$.$ambient({
                $hello_card: $app_card,
            })
        }

        @ $mem
        get Hello() {
            return new this.$.$hello_page( this.$ )
        }

        get user_name() {
            return this.Hello.user_name
        }

        rename() {
            this.Hello.user_name = 'John'
        }

    }
}

      
      



, , , , , , , , :





namespace $.$test {
    export function $changable_user_name_in_object_tree( this: $ ) {

        const name_old = this.$storage_local.getItem( 'user_name' )
        this.$storage_local.removeItem( 'user_name' )

        const app1 = new $app( this )
        this.$assert( app1.user_name, 'Jin!' )

        app1.rename()
        this.$assert( app1.user_name, 'John' )

        const app2 = new $app( this )
        this.$assert( app2.user_name, 'John' )

        this.$storage_local.removeItem( 'user_name' )
        this.$assert( app2.user_name, 'Jin!' )

        if( name_old !== null ) {
            this.$storage_local.setItem( 'user_name', name_old )
        }

    }
}

      
      



, , . .





, , :





namespace $ {
    $.$test_run()
}

      
      



, , , . , $isolated



, - :





namespace $ {
    $.$ambient({
        $isolated: $.$ambient
    }).$test_run()
}

      
      



, , , localStorage, $storage_local



.





, , , , .





TechLeadConf: .





$mol, . …





c import/export, : Fully Qualified Names vs Imports. , : PascalCase vs camelCase vs kebab case vs snake_case.





TypeScript .








All Articles