Inversión del control en TypeScript desnudo sin dolor

Hola, mi nombre es Dmitry Karlovsky y (desde que tengo memoria) estoy luchando con mi entorno. Después de todo, es tan hueso, roble, y nunca entiende lo que quiero de él. Pero en algún momento me di cuenta de que era suficiente para soportarlo y algo había que cambiar. Por tanto, ahora no es el entorno el que me dicta lo que puedo y no puedo hacer, sino que yo le dicto al entorno lo que debe ser.





Como ya entendió, más adelante hablaremos sobre la inversión del control a través del "contexto ambiental". Mucha gente ya está familiarizada con este enfoque a partir de las "variables de entorno": se establecen cuando se inicia el programa y, por lo general, se heredan para todos los programas que inicia. Usaremos este concepto para organizar nuestro código TypeScript.





Entonces, lo que queremos obtener:





  • Las funciones, cuando se llaman, heredan el contexto de la función que llama.





  • Los objetos heredan el contexto de su objeto propietario





  • Un sistema puede tener muchas opciones de contexto al mismo tiempo





  • Los cambios en los contextos derivados no afectan al original





  • Los cambios en el contexto original se reflejan en derivados





  • Las pruebas se pueden ejecutar en un contexto aislado y no aislado





  • Mínimo de caldera





  • Rendimiento máximo





  • El tipo de verificación de todo





, - :





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