Inversão de controle no TypeScript simples sem dor

Olá, meu nome é Dmitry Karlovsky e (desde que me lembro) estou lutando com o que me rodeia. Afinal, é tão osso, carvalho, e nunca entende o que quero dele. Mas em algum momento percebi que era o suficiente para agüentar e algo tinha que ser mudado. Portanto, agora não é o ambiente que me dita o que posso e não posso fazer, mas eu dito ao ambiente o que deve ser.





Como você já entendeu, ainda falaremos sobre a inversão de controle por meio do “contexto do ambiente”. Muitas pessoas já estão familiarizadas com essa abordagem das "variáveis ​​de ambiente" - elas são definidas quando o programa é iniciado e geralmente são herdadas por todos os programas que ele inicia. Usaremos esse conceito para organizar nosso código TypeScript.





Então, o que queremos obter:





  • As funções, quando chamadas, herdam o contexto da função de chamada.





  • Os objetos herdam o contexto de seu objeto proprietário





  • Um sistema pode ter muitas opções de contexto ao mesmo tempo





  • Mudanças em contextos derivados não afetam o original





  • Mudanças no contexto original são refletidas nos derivados





  • Os testes podem ser executados em contexto isolado e não isolado





  • Boilerplate mínimo





  • Performance máxima





  • A verificação de tipo de tudo





, - :





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