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
.
, , , , .
$mol, . โฆ
c import/export, : Fully Qualified Names vs Imports. , : PascalCase vs camelCase vs kebab case vs snake_case.