Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Поддержка типов в контекстах #256

Closed
zerkalica opened this issue Sep 11, 2017 · 34 comments
Closed

Поддержка типов в контекстах #256

zerkalica opened this issue Sep 11, 2017 · 34 comments

Comments

@zerkalica
Copy link
Collaborator

zerkalica commented Sep 11, 2017

@ $mol_mem()
context_sub( ) {
    const context = this.context()
    const subContext : $mol_view_context = Object.create( context )
    subContext.alert = ( message ; string )=> this.$.$my_dialogs.alert( message )
    return subContext
}

Реестр или service locator дешево и сердито, но типы не заработают при таком подходе, как и в vue, react. Чуть в лучшую сторону здесь отличается angular.

Нужен полноценный di, который бы поддерживал компоненты.

Синглтоны, контексты, плагины в mol_view - это все частные случаи, которые могут быть реализованы в едином подходе на di. В общем то суть di - не какое-то там удобное unit-тестирование, как некоторые считают, а автоматизация превращения всего приложения в точки расширения.

Тут можно вывести некоторые общие принципы:

Типы

Метаданные выводятся из типов, которыми описываются зависимости компонента

Зависимости компонента - по большей части реальные классы

Интерфейсы как зависимости

Интерфейсы через babel/ts плагины возможны примерно так:

function Child(props, service: IService) { return <div>...</div> }

import _ from 'babel-plugin-transform-metadata/_'

class MyService implements IService {}
function Parent() { return <Child/> }
Parent.register = [ (_: IService): MyService]

Но, если честно, с этим больше проблем, чем плюсов. Полноценных интерфейсов в js нет, а в ts/flow поддержка только в compile-time, а надо в run-time, поэтому ради типизации приходится так "обманывать" flow/ts.

Да и обязательная регистрация зависимостей ради интерфейсов и даже компонент, как в angular - это клиника.

Generics влияют на метаданные

Я использую css in js, на нем и покажу что это:

// стили
function ATheme() {
    return {
         some: {padding: '1em'}
    }
}

function A(theme: ThemeOf<typeof ATheme>) {
  return <div className={theme.some}></div>
}

// generated by babel-plugin:
A._r = [{id: 'theme', v: ATheme}]

Не уверен, насколько это чисто, но кода меньше чем с декораторами и типы поддерживаются.

Иерархичность

Нижележащий компонент наследует зависимости вышележащего, но может переопределить что-либо, не влияя на вышележащий.

Open/close принцип через алиасинг

Вышестоящий контейнер может переопределить для всех нижестоящих любую зависимость, зная ее интерфейс / класс.

Цель такая же как в вашем tree - дать удобные точки расширения для всего по-умолчанию, только применимо к любой сущности в программе, а не только компонента.

Нет прямой работы с контекстом

В вашем примере:

    const context = this.context()
    const subContext : $mol_view_context = Object.create( context )

this.context - это типоопасная свалка, а прямая работа с ней черевата локином к конкретному фреймворку (хотя у вас и так локин от вашей экосистемы).

Работа с контекстом должна быть только через регистрацию/объявление классов/функций/интерфейсов. По мне так всякие ключи строковые не имеют смысла, да и подобных решений как грязи (тот же vue).

Например, как я попытался адаптировать эти идеи к реакту:

function FirstCounterView(
    props: {},
    counter: FirstCounterService
) {
    return <div></div>
}

Через babel plugin создается мета для конструкторов и компонент:

FirstCounterView._r = [FirstCounterService]

Ради поддержки flow, прибитого к долбаному реакту, первый аргумент нельзя было использовать для описания контекста, поэтому пришлось научить babel отличать компоненты от функций.

Подменяя createElement на свой, я могу управлять инициализацией компонента и помимо свойств, прокидывать контекст в точном соотвествии с типом в аргументе. При этом компоненты - чистые функции + простые метаданные.

@nin-jin
Copy link
Member

nin-jin commented Sep 11, 2017

Я видимо плохо объяснил в чём суть...

this.$ имеет тип $mol_view_context, который соединяется из всех корневых неймспейсов:

	export type $mol_view_context = ( Window )&( typeof $.$$ )&( typeof $ )

Далее мы можем объявить пару классов наиболее естественным образом, без нагромождения интерфейсов, инъекторов, ресолверов и пр:

class $my_aaa extends $mol_view { }

class $my_bbb extends $mol_view {

    @ $mol_mem()
    aaa() {
        return new this.$.$my_aaa
    }

}

Теперь любой класс выше по иерархии может переопределить зависимость для всех вложенных компонент:

class $my_ccc extends $mol_view {

    @ $mol_mem()
    context_sub( ) {
        const context = this.context()
        const subContext : $mol_view_context = Object.create( context )
        subContext.$my_aaa = class extends this.$.$my_aaa { }
        return subContext
    }

    @ $mol_mem()
    bbb() {
        return new this.$.$my_bbb
    }

}

bbb() будет использовать нашу версию $my_aaa. TS при этом проверит:

  1. указанное имя есть в контексте.
  2. интерфейс переопределения совместим с интерфейсом переопределяемого значения.

Разумеется работает и автодополнение. При этом мы не выходим за рамки стандартных возможностей TS.

Я думаю сделать так, чтобы можно было переопределять вложенный контекст в упрощённой форме:

    @ $mol_mem()
    context_sub( ) {
        return {
            $my_aaa : class extends this.$.$my_aaa { }
        }
    }

Так же думаю лучше вынести это дело из $mol_view в $mol_object, чтобы работало не только с визуальными компонентами.

@zerkalica
Copy link
Collaborator Author

Смущает то, что это все завязано на соглашение по именованию модулей, получается mol вещь в себе, как такое использовать с npm тем же?

Еще получается, все доступно для всех, this.$ это как копия реестра глобального пространства имен.
Хотя и выглядит просто, да.

@nin-jin
Copy link
Member

nin-jin commented Sep 11, 2017

Реестр никак не завязан на именование. Именование - оно для автоматического подтягивания зависимостей.

@zerkalica
Copy link
Collaborator Author

Там контексты не будут поддерживаться в ts, если не будет соглашений по импортам: что все обращения только через $-неймспейс, никаких import / export.

@nin-jin
Copy link
Member

nin-jin commented Sep 11, 2017

А, ну это да.

@zerkalica
Copy link
Collaborator Author

zerkalica commented Sep 12, 2017

Все же, при вашем подходе, отвественность по инциализации лежит на компоненте, нет автоматизированного IoC.

Например, что будет если в:

subContext.$my_aaa = class extends this.$.$my_aaa { }

Будет изменена сигнатура конструктора?

Ts ведь не отловит это и ошибка всплывет только в рантайме:

class $my_bbb extends $mol_view {

    @ $mol_mem()
    aaa() {
        return new this.$.$my_aaa
    }

}

PS

Хотя это и не очень страшно, ts не отлавливает только такие случаи:

namespace $ { 
    export class A {
        constructor(p: string) { }
        a() { }
    }
}

class B {
    constructor() { }
    a() { }
}

$.A = B

Но все же, как быть с зависимостями, необходимыми для инициализации класса.
Как положить в контекст объект, уже настроенный данными, полученными с сервера и что б ts это поддерживал?

PS2:

Вроде смог подобрать рабочую конструкцию:

namespace $ { 
    export let a: { 
        some: string;
    }
}

class A {
    constructor(public some: string) { }
}

$.a = new A('test')

@nin-jin
Copy link
Member

nin-jin commented Sep 12, 2017

В $mol конструктор обычно пустой, ибо все вычисления делаются лениво. А конфигурирование происходит через переопределение свойств. через свойства же и происходит получение данных, если они требуются. И обычно, если одному объекту требуется другой, то он сам его по умолчанию и создаёт. Даже если разумного дефолта нет - просто создаётся мок, декларирующий интерфейс и упрощающий тестирование.

@zerkalica
Copy link
Collaborator Author

Тут смущает один момент.

Переопределение свойств где происходит, сразу после инициализации класса? Если да, то тогда проблема остается - знание о всех зависимостях класса сосредотачивается в точке его инстанциирования, а размазывание этой ответственности по фабрикам - уменьшение возможностей кастомизации.

Если нет, то размазывается целостность класса. Отдельно создаем инстанс и где-то спустя N строк - переопределяем методы.

Логичнее, если информации о создании класса будет где-то в нем самом, как в DI. Т.к. может быть куча деталей, которые требуются для его работы, но фабрике о них знать не требуется вовсе. А в вашем случае получается компонент-фабрика будет знать.

Почему кстати через переопределение свойств, обычно через конструктор, он для этого и предназначен.

@nin-jin
Copy link
Member

nin-jin commented Sep 12, 2017

Переопределение происходит сразу после создания. Есть и фабрика:

class $my_foo extends $mol_object {
    bar() { return 1 }
}
const foo = $my_foo.make({
   bar : 2
   // baz : 3 - это выдаст ошибку, ибо такого свойства нет.
})

К сожалению, тайпскрипт не позволяет сделать так:

const foo = new $my_foo({
   bar : 2
   // baz : 3 - это выдаст ошибку, ибо такого свойства нет.
})

microsoft/TypeScript#16703

А возможность переопределения любого свойства даёт:

  1. возможность кастомизации любого аспекта поведения компонента
  2. отсутствие необходимости писать объявление и обработку развесистых конфигов в конструкторе
  3. простое и лаконичное одно и двустороннее связывание

@zerkalica
Copy link
Collaborator Author

zerkalica commented Sep 12, 2017

Да, я тоже с таким сталкивался в flow, хотелось case классов из scala.

facebook/flow#3016

Фейсбуковцам пофиг.

Переопределение мне не нравится мутабельностью и не атомарностью, а так норм.
В итоге, я писал развесистые объявления, подстраиваясь под flow, т.к. это меньшее зло, на мой взгляд.

Про фабрику я имел в виду не make, а то, что о параметрах, которые передаются в make, знает сущность, которая объект $my_foo создает, т.е. параметры она не использует в вычислениях результата, они ей нужны только для создания $my_foo.

PS.

Хотя в flow вроде починили

@nin-jin
Copy link
Member

nin-jin commented Sep 13, 2017

Ну, владельцу объекта виднее как его настраивать же.

@zerkalica
Copy link
Collaborator Author

Я все пытаюсь объяснить, что эта ответственность по инстанциированию чего-либо должна быть на каркасе, а не забиваться вручную в объекте-владельце.

В противном случае в слои, где используется зависимость, просачиваются детали реализации этой зависимости - аргументы конструктора или make.

Пусть A - определяет в контексте класс C
B - дочерний от A, использует C
C - требует D, E для своей инициализации.

Тут 2 проблемы:

  1. Просачиваются детали реализации - D, E, аргументы конструктора или make. D, E надо прокидывать в B, при этом A - не использует D, E, просто в контекст добавляет, B - не использует D, E, оно создает C и его использует для вычислений. Вместо одной настроенной сущности instance C, мы тащим С, D, E.

  2. Если мы где-то меняем реализацию с C на C1 extends C, и меняется сигнатура конструктора или make, т.е. для настройки нового C1 надо передать данные дополнительные: F, мы в B без рефакторинга не узнаем о том, что интерфейс изменился и нужно кроме D, E передать еще и F

@nin-jin
Copy link
Member

nin-jin commented Sep 13, 2017

Так в том-то и дело, что ни у какого класса нет "обязательных параметров" и нет никакой превентивной инициализации. :-)

"А" объявляет в контексте только себя. "C" только себя. "В" создавая экземпляр "С" указывает лишь те параметры, что нужны лично ему. Об остальных параметрах думать не его забота. Если мы хотим всем экземплярам "С" в контексте прописать какие-то параметры, то мы так и пишем:

context_sub() {
    return {
        C : class extends this.$.C {
            D : ()=> new D1
            E : ()=> new E1
        }
    }
}

Теперь, когда В создаст C у него уже будут прописаны D и E, а он добавит к ним ещё и нужный ему foo.

@zerkalica
Copy link
Collaborator Author

Настройка через наследование решает, однако ценой генерации классов в рантайме, даже если у меня просто строчка настройки, нужно метод создавать.

Вместо

new C('some')

Нужно делать:

class C1 extends C {
  some: () => 'some'
}

Невозможность использовать конструктор - это ограничение же, примерно как в React можно передать только props в конструктор.
js, как и многие другие, задумался из расчета, что конструктор будет по-назначению использоваться - для настройки инстанса.

@nin-jin
Copy link
Member

nin-jin commented Sep 13, 2017

Ну а что бы вы сделали, например, в конструкторе? Кроме как переложить аргументы в свойства там делать особо и нечего.

@zerkalica
Copy link
Collaborator Author

Ну да, я бы просто объявил бы контракт:

class A {
  constructor(public some: string) {}
}

Ощущение, что вы боретесь с убогостью js/ts, придумывая разные нестандартные выкрутасы.
Я пока не понимаю в чем преимущества вашего способа.

@nin-jin
Copy link
Member

nin-jin commented Sep 13, 2017

Я исхожу из следующих посылок:

  1. У компонента могут быть десятки свойств. Большую часть из них владелец может захотеть настроить под себя. Биндинги там всякие и тп.
  2. Ленивые вычисления предполагают, что они происходят строго по требованию. Создание объекта - это всего лишь требование иметь объект, но не требование каких-либо вычислений. А значит вычислений в конструкторе не должно быть.
  3. Чтобы с компонентом было легко работать, он должен иметь разумное поведение по умолчанию, не требующее сложной подготовки, чтобы начать им пользоваться. Значит все свои потребности он должен удовлетворить сам. В простейшем случае, чтобы начать работать с компонентом достаточно просто создать его экземпляр и всё. Подстроить под себя его можно в дальнейшем.

@zerkalica
Copy link
Collaborator Author

Ваш подход я бы назвал di на контексто-переопределяемых, наследуемых синглтонах.

По п.3. Если нет подготовки во вне, значит она упрятана в класс. Понятно, что в голой пустыне запустить такой класс проще, однако запускается все в какой-то среде, почему бы ее не сделать богатой, а не переносить эту ответственность на прикладной код.

В полноценном di не надо создавать объекты, только объявить зависимость. Зачем создавать в коде что либо прикладному программисту, если достаточно декларировать, а каркас все сам сделает. Представьте, что ts автоматом бы создавал зависимости при инициализации класса.

В общем то цели одни и теже, а вот почему выбран этот механизм, непонятно, реализовать проще?

Вопрос не связанный c mol: А когда кастомизировать сущность наследованием, а когда новым инстансом и настройкой через конструктор? Ведь каждый инструмент для своих целей, у вас получается второй не используется, в силу некоторых упрощений.

@nin-jin
Copy link
Member

nin-jin commented Sep 13, 2017

Это сервис локатор же.

Ну да, и реализовать проще, понять проще, поддерживаются не только классы, но и функции и просто константы. Хотя я согласен - было бы лучше иметь фабрики, а не классы. Хотя разница между ними в js лишь в слове new.

Второй используется, только настройка через свойства. Используется для instanse-специфичных настроек.

@zerkalica
Copy link
Collaborator Author

Похоже, да, правда в каноничном service locator один реестр без контекстов и типизации нет, инфраструктурный код просачивается в приложение.

У вас без первых 2х недостатков.

А когда настройки instance-специфичны, а когда наследованием надо? В mol мне как-то грань эту трудно уловить пока.

@nin-jin
Copy link
Member

nin-jin commented Sep 15, 2017

Если для группы инстансов должна быть одна и та же настройка не зависящаяя от точки инстанцирования, то лучше вынести в отдельный класс и наследовать эту настройк через прототипы, а не станавливать каждому инстансу.

@zerkalica
Copy link
Collaborator Author

А почему лучше, это как-то оптимальнее работает? Или просто неудобно каждый раз передавать настройку, т.к. нет инструментов для автоматизации этого? Или есть еще какой-то кейс использования такого (облегчение преобразования из tree, например)

@nin-jin
Copy link
Member

nin-jin commented Sep 18, 2017

Быстрее, ест меньше памяти, да и удобней.

@zerkalica
Copy link
Collaborator Author

zerkalica commented Sep 20, 2017

Так то оно так, только вот мозг надо переломать, если привык использовать инверсию контроля.

Про tree еще более менее понятно из статьи Идеальный UI фреймворк. Но про все остальное не очень.

Может быть статью напишите про особенности реализации паттернов, которые использованы в mol? Или какое-нибудь сравнение, что б например, люди привыкшие к реакту/ангулару смогли адаптироваться? Именно про архитектурные приемы.

@nin-jin
Copy link
Member

nin-jin commented Sep 20, 2017

Так это тоже инверсия контроля. Просто она ненавязчивая - по умолчанию компонент сам всё делает. А компонент выше по иерархии может приказать "сюда не ходи - туда ходи" :-)

В ближайшее время как-раз собираюсь что-то такое написать, да.

@zerkalica
Copy link
Collaborator Author

Просто она ненавязчивая - по умолчанию компонент сам всё делает

А в чем по-вашему заключается ненавязчивость?

Мне кажется, правильнее говорить, компонент с нуль-конфигурацией, когда не надо настраивать среду для его работы, в нем уже есть инфраструктурный код, обеспечивающий связывание.

По мне, ненавязчивость - это когда нет extends mol_view, this.context, локаторов и прочих завязок на инфраструктуру. Т.е. когда только средствами ядра языка и возможностей компилятора достигается инверсия, связывание и в идеале, даже реактивность. У вас же достаточно завязок, что б нельзя было сказать - вот сферический компонент, который чистая функция и который я могу использовать с любым фреймворком (хоть с реактом), получающим верстку через функции.

А компонент выше по иерархии может приказать "сюда не ходи - туда ходи" :-)

Это похоже на иерархический di в ангуларе.

Я хочу сказать, что почти на все что вы делаете, уже есть аналоги (может, кроме tree). И тут для меня важнее понять ход мыслей, почему выбран такой нетрадиционный способ. Чем это лучше классического di, привычного SOLID и т.д.

@nin-jin
Copy link
Member

nin-jin commented Sep 20, 2017

Ну, если совсем без завязок на инфраструктуру - приходится много кода писать. Ненавязчивость я имел ввиду именно DI. Хочешь - провайди зависимость, не хочешь - не провайди. А вот у Ангуляра навязчивость DI сильная - приходится делать много хитрых пасов (интерфейсы объявлять, повайдеров регистрировать, инъекторы указывать), чтобы всё заработало. В $mol же ты берёшь и используешь любую глобальную переменную (но через this.$) и получаешь DI, всё.

@zerkalica
Copy link
Collaborator Author

Если бы this.$ было в спецификации языка, я бы наверное согласился. Но так это не di, а сервис локатор. Отличие в явной завязанности на инфраструктуру, пускай простой и небольшой, но делающей невозможным запускать компоненты где-то еще, кроме mol. Об этом много статей, например вот.

интерфейсы объявлять, повайдеров регистрировать, инъекторы указывать

Не все так сложно, умолчания тоже работают.

В целом конечно, ангулар не пример для подражания, т.к. там плохо переосмыслили backend-подходы, когда привносили их с java/c#. Legacy-мышление.

В моем велосипеде не надо ничего регистрировать: Например, autocomplete

/* @jsx lom_h */
export function AutocompleteView(
    _: IAutocompleteProps,
    service: AutocompleteService
) {
    return <div>
        <div>
            Filter:
            <input value={service.nameToSearch} onInput={service.setValue}/>
        </div>
        Values:
        <AutocompleteResultsView searchResults={service.searchResults} />
    </div>
}

Я просто объявил service: AutocompleteService и среда его настроила и передала компоненту.
При этом компонент можно использовать (хоть и не удобно) вне reactive-di, достаточно контекст в реакте правильно сформулировать.

@nin-jin
Copy link
Member

nin-jin commented Sep 20, 2017

class A { ... }

class B {
    $ = window
    aaa() {
        const a = new this.$.A
        a.$ = this.$
        return a
    }
}

class C {
    $ = window
    bbb() {
        const b = new this.$.B
        b.$ = Object.create( this.$ )
        b.$.A = class extends A { ... }
        return b
    }
}

В принципе никакой завязки на инфраструктуру. Просто инъекция сервис локатора. Реактовые контексты - это ж то же самое.

@zerkalica
Copy link
Collaborator Author

В mol_view же не в таком виде используется, там как раз есть завязка.

		@ $mol_mem
		context( next? : $mol_view_context ) {
			return next || $ as any
		}
		
		get $() {
			return this.context()
		}
		set $( next : $mol_view_context ) {
			this.context( next )
		}
		
		context_sub() {
			return this.context()
		}

Реактовые контексты - это ж то же самое.

Реактовые контексты плохой пример, как и сам реакт - это недоразумение.

Но в случае компонент-функций, которых в mol нет, в реакте это уже похоже на di.

const PropTypes = require('prop-types');

const Button = ({children}, context) =>
  <button style={{background: context.color}}>
    {children}
  </button>;

Button.contextTypes = {color: PropTypes.string};

Если убрать псевдотипизацию в Button.contextTypes, то будет вполне себе нативно.

@nin-jin
Copy link
Member

nin-jin commented Sep 21, 2017

Ну, как $mol_view внутри себя использует инъектированный локатор - это уже дело $mol_view :-) Внешне апи эквивалентно тому, что я писал.

@zerkalica
Copy link
Collaborator Author

zerkalica commented Oct 26, 2017

Идея тут возникла, а что если написать плагин для ts или бабела (он теперь может в ts), который бы $. в методах всех mol_object классов подменял бы на this.$.

Это дало бы возможность абстрагироваться от факта наличия контекстов и уменьшило бы вероятность ошибки в случаях использования контекста. Присвоение бы осталось прежнее, через this.$.

@nin-jin
Copy link
Member

nin-jin commented Oct 27, 2017

Но завязало бы на бабель. Это на мой взгляд ещё хуже.

@zerkalica
Copy link
Collaborator Author

zerkalica commented Oct 27, 2017

У вас и так все на ts завязано, какая разница - поменять ts на бабел. Анализатор вообще отдельно можно держать. Фейсбуку осталось добавить в flow поддержку ts и он убьет конкурента, будет в 3 раза быстрее и выведение типов лучше.

Ладно, можно и не менять, просто в ts пока нет нормальных плагинов. Но, что бы тоже самое сделать, можно попробовать свой компилятор построить на его основе через compiler api.

@nin-jin nin-jin closed this as completed Jan 8, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants