Pembalikan kontrol pada TypeScript telanjang tanpa rasa sakit

Halo, nama saya Dmitry Karlovsky dan (selama saya ingat) saya berjuang dengan lingkungan saya. Bagaimanapun, itu sangat tulang, kayu ek, dan tidak pernah mengerti apa yang saya inginkan darinya. Tetapi pada titik tertentu saya menyadari bahwa itu cukup untuk menahannya dan sesuatu harus diubah. Oleh karena itu, sekarang bukan lingkungan yang mendikte saya apa yang bisa dan tidak bisa saya lakukan, tetapi saya mendikte lingkungan apa yang seharusnya.





Seperti yang sudah Anda pahami, selanjutnya kita akan berbicara tentang inversi kendali melalui "konteks lingkungan". Banyak orang sudah terbiasa dengan pendekatan ini dari "variabel lingkungan" - variabel tersebut ditetapkan saat program dimulai dan biasanya diwariskan untuk semua program yang dimulai. Kami akan menggunakan konsep ini untuk mengatur kode TypeScript kami.





Jadi apa yang ingin kami dapatkan:





  • Fungsi, saat dipanggil, mewarisi konteks dari fungsi panggilan.





  • Objek mewarisi konteks dari objek pemiliknya





  • Suatu sistem dapat memiliki banyak opsi konteks pada saat yang bersamaan





  • Perubahan dalam konteks turunan tidak mempengaruhi aslinya





  • Perubahan dalam konteks asli tercermin dalam turunannya





  • Pengujian dapat dijalankan dalam konteks terisolasi dan tidak terisolasi





  • Minimum boilerplate





  • Penampilan maksimal





  • Cek ketik dari semuanya





, - :





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