Closed zerkalica closed 6 years ago
В Chrome Developer Tools вы можете включить HTTP Throttling на вкладке Network. Если выбрать там GPRS, то вся коммуникация будет идти крайне медленно.
У нас есть небольшое демо-приложение, как раз показывающее работу с индикаторами ожидания. Код: https://github.com/eigenmethod/mol/tree/master/app/users Онлайн: http://mol.js.org/app/users/
Честно говоря, сложно понять. Не нашел в коде явной работы со статусами.
Насколько я понял, есть глобальный mol_atom_wait, о котором знают все кнопки и компоненты. Когда любой атом в pending, mol рисует вместо компонента какой-то дефолтный загрузчик и кнопке этот класс устанавливает.
Вопрос был как с этим явно работать и как разделять wait для каждого компонента на загрузку и на сохранение.
В том-то и дело, что явная работа с ними как правило не нужна.
Явная работа со статусами есть в компоненте, выводящем сообщение об ошибке: https://github.com/eigenmethod/mol/blob/master/status/status.view.ts
Если произошла ошибка при доступе к свойству, то рисуется текст ошибки (вместо текста, могут быть любые компоненты). Но если мы "в ожидании", то исключение пробрасывается и ловится в обобщённом рендеринге: https://github.com/eigenmethod/mol/blob/master/view/view.ts#L146
То есть по умолчанию, в случае ошибки рендерящемуся компоненту устанавливается атрибут, а через css на этот атрибут навешивается либо "индикатор ожидания", либо "индикатор ошибки". При желании их можно стилизовать по другому просто переопределив стили. Ну а если нужно что-то совсем кастомное, то перехватываем исключение как в $mol_status и творим любое непотребство.
Тут я рассказывал, как это примерно работает: https://github.com/nin-jin/slides/tree/master/orp#Реактивный-рендеринг
Спасибо, более менее понятно.
Как только мы обращаемся к свойству - происходит актуализация, бросается exception. Однако через try/catch можно показать либо ошибку, либо wait, если приходит специальный Error, который бросается в недрах http.
Именно так. Бросается он тут: https://github.com/eigenmethod/mol/blob/master/http/http.ts#L76
А с observable такую штуку можно подружить?
Например, до инициализации сокета у нас состояние не актуально, бросаем exception, после инициализации через сокеты постоянно что-то обновляется, а когда данные становятся не нужны - отписываемся от observable.
Как встроиться в жизненный цикл pull/put/reap?
Примерно так...
Делаем обёртку над сокетом, которая инкапсулирует в себе всю асинхронность:
class $my_socket extends $mol_object {
uri() {
return ''
}
@ $mol_mem()
native( next? : WebSocket , force? : $mol_atom_force ) {
const native = next || new WebSocket( this.uri() )
native.onopen = () => {
this.native( native , $mol_atom_force ) // успех - пишем в кеш объект сокета
} )
native.onerror = event => {
this.native( event.error , $mol_atom_force ) // ошибка - пишем в кеш исключение
} )
native.onmessage = event => {
this.data( event.data , $mol_atom_force ) // сообщение - обновляем кеш данных
}
throw new $mol_atom_wait( 'Connecting...' )
}
@ $mol_mem()
data( next? : any , force? : $mol_atom_force ) {
this.native() // ждём, пока установится соединение
return {} // возвращаем дефолтное значение, пока не пришло обновлений
}
destroyed( next? : boolean ) { // вызывается, когда от сокета никто не зависит и он уничтожается
if( next ) {
this.native().close()
}
return super( next )
}
}
Потом, используем её где-нибудь в другом классе:
// создаём и настраиваем сокет, декоратор уничтожит его, когда не будет зависимостей
@ $mol_mem()
socket() {
const socket = new $my_socket
socket.uri = ()=> 'ws://example.org'
return socket
}
user_name() {
return this.socket().data().user_name // синхронно запрашиваем данные
}
А какая цель этого куска кода?
native.onopen = () => {
this.native( native , $mol_atom_force ) //
} )
this.native не вызывается, а вызывается цепочка $mol_atom -> value -> push -> slaves.check
Т.е. мы просто говорим всем зависимым - поменяй статус на checking, а разве это не вытекает из первого вызова this.native, когда exception бросается?
$mol_atom_force передаем, что б вызать push. Почему, кстати $mol_atom_force, а не true?
В первой версии атомов было очевидное апи с merge, тут не очень понятна разница межу set и push. Мне кажется, та бородатая статья https://habrahabr.ru/post/235121/ нуждается в рефакторинге.
А какая цель этого куска кода?
Чтобы вернуть native не сразу, а лишь когда соединение будет установлено. Пушим значение и зависимые атомы начинают обновляться, на этот раз получая не исключение, а установленное вебсокет соединение.
Почему, кстати $mol_atom_force, а не true?
Чтобы код был самодокументируемым. Абстрактный true
не понятно что делает.
В первой версии атомов было очевидное апи с merge, тут не очень понятна разница межу set и push.
Честно сказать, сейчас апи $mol_atom мне не очень нравится (надо бы навести порядок там), поэтому лучше использовать $mol_mem. Вместо merge сейчас есть метод normalize.
тут не очень понятна разница межу set и push.
Она осталась прежней: push безусловно записывает значение в кеш, а set - предлагает новое значение, а пользовательская функция уже решает, что с этим значением делать.
Мне кажется, та бородатая статья https://habrahabr.ru/post/235121/ нуждается в рефакторинге.
Она настолько устарела, что что-то в ней менять уже бессмысленно. Предлагаемое там решение имеет проблемы с порядком вычисления зависимостей. Новая реализация их уже не имеет, но она и существенно сложнее.
Вместо merge сейчас есть метод normalize.
В отличие от merge тут не понятно, как его переопределять. В atom.ts просто вызывается конкретный this.normalize, который только массивы проверяет для отброса дублей.
а set - предлагает новое значение, а пользовательская функция уже решает, что с этим значением делать.
set -> obsolete (переводим в checking всех зависимых) -> get -> actualize -> pull -> handler с next без force
Это видимо и есть мерж, когда приходит next и мы отдаем его же или новое значение.
Получается такая карта:
handler() - режим актуализации и бросание exception handler(some, true) - выполняется самим хэндлером, когда актуализация завершена, сетим some в кэш handler(some, false) - выполняется из вне, в прикладном коде, когда надо предложить новое значение
Лаконично, но все-таки это как-то сильно завязано на соглашения с фреймворком. геттеро-сеттер и force провоцируют на наличие if-ов в коде, вместо декларативных методов.
force - техническая деталь реализации атомов, которая просачивается в прикладной код. Параметр публичный, однако используется только для работы с асинхронщиной, когда хэндлер вызывает сам себя с ним, что бы засетить сырое значение в кэш.
А если завязываться на Promise или Observable и возвращать только его в случае необходимости актуализации? Вроде можно force спрятать, т.к. в then/next/complete уже будет ясно что надо делать push.
Есть еще неявный destroyed( next? : boolean ), который mol_atom вызывает у хоста свойства, для native хостом является экземпляр $my_socket. Observable тут решил бы, мы логику unsubscribe описывали бы в методе native.
В отличие от merge тут не понятно, как его переопределять.
В случае прямого использования атомов, как обычно - путём наследования от $mol_atom. В случае $mol_mem - да, пока никак. Надо бы добавить. Хотя, особой потребности пока в этом не было. А вам это для каких случаев нужно?
set -> obsolete (переводим в checking всех зависимых) -> get -> actualize -> pull -> handler с next без force
На самом деле вызов obsolete тут - адский костыль, чтобы вызов get привёл к вызову pull :-)
handler(some, true) - выполняется самим хэндлером, когда актуализация завершена, сетим some в кэш
Нет, handler в этом случае не вызывается.
Свойство может вызываться в следующих формах:
.prop() // приводит к вызову handler( undefined , undefined )
.prop( undefined , $mol_atom_force ) // приводит к вызову handler( undefined , $mol_atom_force )
.prop( next ) // приводит к вызову handler( next , undefined )
.prop( next , $mol_atom_force ) // записывает в кеш не вызывая handler
геттеро-сеттер и force провоцируют на наличие if-ов в коде, вместо декларативных методов.
Зачастую логика там одинаковая с точностью до передачи параметра. Например:
@ $mol_mem()
users( next? : string[] , force? : $mol_atom_force ) {
return next || this.users_master( next , force )
}
force - техническая деталь реализации атомов, которая просачивается в прикладной код. Параметр публичный, однако используется только для работы с асинхронщиной, когда хэндлер вызывает сам себя с ним, что бы засетить сырое значение в кэш.
Возможность форсировать обновление - вполне себе отдельное прикладное действие. Вот форсирование записи в кеш - прикладному коду как правило не нужно, да. Но иногда необходимо, когда нужно записать данные не в самого себя, а в другое свойство, как в примере обработчика onmessage
.
А если завязываться на Promise или Observable и возвращать только его в случае необходимости актуализации?
Если речь про полноценную поддержку thenable интерфейса "проверять, что возвращённое значение является обещанием и подписываться на его резолв", то тут есть проблема со статической типизацией. Сейчас тип возвращаемого значения выводится автоматически из выражения return, а если возвращать Promise, то $mol_mem() уже не будет прозрачным - декораторы не могут менять тип свойства.
Есть еще неявный destroyed( next? : boolean ), который mol_atom вызывает у хоста свойства
Не у хоста, а у значения свойства, от которого все отписались (socket()
в примере). destroyed есть у всех наследников $mol_object.
А вам это для каких случаев нужно?
Не знаю пока, просто в чем отличие от push тогда, в случае set мерж же есть, когда next предлагаем - set может возвратить не next
Возможность форсировать обновление - вполне себе отдельное прикладное действие
Да, но ради этого мы тащим этот параметр force везде, можно попробовать без него сделать. Ниже пример, в нем можно вот так сбросить: пришел null - передаем Observable, отписывая предыдущий.
data(null).some
проблема со статической типизацией
Можно развивать тему ексепшенов. Не очень понятно, зачем в вашем примере разделять в $my_socket native и data, я набросал такой:
class MySocket {
constructor(private uri: string) {}
@ $mol_mem
data(next?: MyData): MyData {
if (next) return next
throw new Observable((observer) => {
const socket = new WebSocket(this.uri)
socket.onmessage((e) => observer.next(e.data))
socket.onerror((err) => observer.error(err))
return () => { socket.close() }
})
}
}
const socket = new MySocket(uri)
socket.data()
Может что-то упускаю, хотелось бы все случаи рассмотреть, пока не понятно на практике когда какие сочетания next и force использовать с точки зрения бизнес логики, а не технически.
Например, для чего prop( undefined , $mol_atom_force ), зачем нам передавать force вызывая другой хэндлер, как в вызове this.data из this.native/native.onmessage?.
.prop() // приводит к вызову handler( undefined , undefined )
.prop( undefined , $mol_atom_force ) // приводит к вызову handler( undefined , $mol_atom_force )
.prop( next ) // приводит к вызову handler( next , undefined )
.prop( next , $mol_atom_force ) // записывает в кеш не вызывая handler
Не знаю пока, просто в чем отличие от push тогда, в случае set мерж же есть, когда next предлагаем - set может возвратить не next
set вызывает normalize, потом handler, а потом собственно push. push же не вызывает handler.
пришел null - передаем Observable
null - тоже может быть прикладным значением, давать ему дополнительный смысл - ничем не лучше дополнительного параметра.
Можно развивать тему ексепшенов.
Скользкий момент. Я старался сделать $mol_mem() как можно более прозрачным - его можно добавлять и убирать, не ломая логику работы. Если же мы будем кидать стримы, то уже будем обязаны их везде ловить и по особому обрабатывать. И то, и то костыль, но да, код со стримом получается попроще. Однако data() и native() как правило не стоит объединять, так как к native обычно нужно обращаться и по другим поводам. Например, нам нужно послать через сокет сообщение на сервер:
@ $mol_mem()
data( next? : any , force? : $mol_atom_force ) {
const native = this.native() // ждём, пока установится соединение
if( next !== undefined ) native.send( next ) // шлём сообщение, если есть что сказать
// ничего не вернули - значение в кеше не изменится
}
Например, для чего prop( undefined , $mol_atom_force ), зачем нам передавать force вызывая другой хэндлер, как в вызове this.data из this.native/native.onmessage?.
В this.data не undefined передаётся, а новое значение, которое пишется в кеш, не вызывая никаких хендлеров. pull вычисляет новое значение, после чего push-ит его в кеш. Если задача асинхронная, то бросая $mol_atom_wait мы говорим, что не надо ничего пушить в кеш, но когда приходит ответ - push-им его самостоятельно.
set вызывает normalize, потом handler, а потом собственно push. push же не вызывает handler.
handler разве не может делать normalize в случае set?
не ломая логику работы.
А как же throw new $mol_atom_wait( 'Connecting...' ) - по мне так не менее скользкий момент, его же надо обрабатывать, в чем разница с Observable? В любом случае, что б класс без mol_mem заработал, надо реализовать соответствующую логику где-то выше. Суть одна - мы передаем атому сообщения в отдельном потоке, с целью сохранения интерфейсов.
Смысл ОРП мне видится в том, что бы замаскировать потоки под объекты и привычные паттерны работы с ними. Человек, не знакомый с mol_atom, не поймет, что за force, зачем такой геттеро-сеттер, это не похоже на привычный ооп-код и паттерны работы с ним.
Достигается ОРП переносом сложности на среду исполнения (DI) или декораторы, сохраняя понятность и безопасность кода, интерфейсы, ценой введения неких соглашений. Главное что б цена не очень большая была. По мне, destroy, wait, force - бОльшая цена, чем Observable.
Например, нам нужно послать через сокет сообщение на сервер:
И все же, необходимость native - это детали реализации mol_atom, реально нужно читать писать в data, а native - никому не нужен.
Это уже не так красиво, т.к. в es нет стандартна для чтения/записи асинхронных данных.
data(next?: MyData): MyData {
if (next) return next
const socket = new WebSocket(this.uri)
const observable = new Observable((observer) => {
socket.onmessage((e) => observer.next(e.data))
socket.onerror((err) => observer.error(err))
return () => { socket.close() }
})
function sender(data: MyData): Promise<MyData> {
return promisify(socket.send(data))
}
throw {observable, sender}
со стримом получается попроще.
Суть не в том, что проще, а в абстракции, как только в коде встречается mol_ - это завязка на реализацию, а лучше завязываться на спецификацию. Это как чистые компоненты в JSX - это почти чистые функции, могут быть в других похожих фреймворках использованы.
handler разве не может делать normalize в случае set?
normalize вызывается как раз чтобы понять, надо ли вызывать handler. Если в set передано значение эквивалентное тому, что уже есть в кеше - handler вообще не вызывается.
А как же throw new $mol_atom_wait( 'Connecting...' ) - по мне так не менее скользкий момент, его же надо обрабатывать, в чем разница с Observable?
Согласен, можно кидать Promise, вместо $mol_atom_wait, но тогда мы теряем stack trace.
Человек, не знакомый с mol_atom, не поймет, что за force, зачем такой геттеро-сеттер, это не похоже на привычный ооп-код и паттерны работы с ним.
Ну, дополнительный параметр метода для форсирования обновления кеша всё же вполне обыденная штука.
По мне, destroy, wait, force - бОльшая цена, чем Observable.
destroy в любом случае нужен для контроля времени жизни объектов (например, вложенных компонент). При этом объекты должны сами решать что нужно выполнять при создании, а что при уничтожении. К сожалению, в JS нет страндартного деструктора, поэтому приходится "стандартизировать" свой. Стримы тут могут быть альтернативой лишь в одном частном случае, когда собственно объект нам и не нужен и достаточно иметь функцию инициализации и функцию деинициализации.
И все же, необходимость native - это детали реализации mol_atom, реально нужно читать писать в data, а native - никому не нужен.
Ещё может понадобиться реактивный флаг "онлайн/оффлайн", метод "переподключиться" и кто его знает что ещё. Даже банально, когда занимаешься отладкой, удобно, если нативный объект легко доступен через поле обёртки, а не спрятан где-то в замыкании.
Вот, например, обёртка над SpeechAPI имеет кучу ручек: https://github.com/eigenmethod/mol/blob/master/speech/speech.ts
Суть не в том, что проще, а в абстракции, как только в коде встречается mol_ - это завязка на реализацию, а лучше завязываться на спецификацию.
Согласен, тут надо запилить proposal, чтобы это дело стандартизировать... но боюсь такое начинание завернут :-(
destroy в любом случае нужен для контроля времени жизни объектов
Деструкторы в языке с gc, никогда их в js не добавят. Да и что, кроме закрытия ресурсов, контролировать? Атом и так сбрасывает свое состояние по destroy.
Насколько я понял, есть атом и есть контейнер с атомами (mol_object). Приложение - это дерево mol_object, отдающих атомы в свойствах. При этом если нужен destroy - буть добр, отнаследуйся от mol_object в нем определи метод. Имхо, это не очень хороший дизайн, хочется больше свободы.
Стримы тут могут быть альтернативой лишь в одном частном случае, когда собственно объект нам и не нужен и достаточно иметь функцию инициализации и функцию деинициализации.
Можно и без стримов попробовать. Я пытаюсь придумать дизайн с чистыми классами, опираясь только на конструкции самого js, без сторонних библиотек. Если бы в атом передавался некий стрим, который он мог контролировать, как раз можно было бы так сделать. Все же, мне кажется, дело в дизайне и на стримах можно так сделать.
Ещё может понадобиться реактивный флаг "онлайн/оффлайн", метод "переподключиться" и кто его знает что ещё.
Ну тут да, несколько сложнее, можно и на Observable добиться такого, будет более громоздко.
Да и что, кроме закрытия ресурсов, контролировать?
Отписываться от событий, очищать кеши, закрывать соединение, вызывать деструкторы у подчинённых объектов. Вообще говоря, ОПР потенциально позволяет отказаться от GC, так как мы всегда знаем нужен ли кому-то объект или нет.
При этом если нужен destroy - буть добр, отнаследуйся от mol_object в нем определи метод.
Не обязательно наследоваться. Можно реализовать метод destroyed в любом объекте.
Я пытаюсь придумать дизайн с чистыми классами, опираясь только на конструкции самого js, без сторонних библиотек.
Ну так в самом js деструкторов нет. А возить их отдельно от объекта - так себе удовольствие. Сейчас реализовано так, что если объект реализует метод destroyed, то он вызывается, когда чистится кеш.
ОПР потенциально позволяет отказаться от GC
Напоминает счетчик ссылок, но gc делает многие деструкторы необязательными, освобождение памяти например, почему эту идею не распространить и на ОРП. Назад к плюсам что-то не хочется.
Когда возникает потребность очищать кэш, освобождать ресурсы, это в большинстве случаев не бизнес, а технические детали. В исходной постановке задачи нет никаких кэшей, это уже оптимизации.
Деструктор это как shouldComponentUpdate, в реакте он есть, в vue нет, хватает автоматической оптимизации, т.к. лежащая в основе идея - лучше.
Колбэки просты, но примитивны и нестандартны, экосистема js медленно мигрирует в сторону await, promise, observable, rxjs. Например, если бы сокет сразу отдавал Observable, то вопроса с деструктором вообще не возникло бы - не нужно было бы в приложении реализовывать. Аналогично с промисами, бойлерплейта было бы меньше.
Можно реализовать метод destroyed в любом объекте
Меня вот этот код смущает:
if( value instanceof $mol_object ) {
if( ( value.object_owner() === host ) && ( value.object_field() === this.field ) ) {
value.destroyed( true );
}
}
что если объект реализует метод destroyed,
В том то и дело, что это конвеншен, причем mol only. В rxjs, который более распространен и претендует на хреновую, но спецификацию, есть хотя бы disposable, который много где есть еще.
Напоминает счетчик ссылок, но gc делает многие деструкторы необязательными, освобождение памяти например, почему эту идею не распространить и на ОРП.
Потому что оно не работает. GC замедляет программу и ничего при этом не гарантирует. ОРП же даёт двусторонние ссылки между объектами, таким образом каждый объект знает кому он нужен. При этом гарантируется, что все объекты образуют дерево, без циклических зависимостий, которые особенно сложны для GC.
Когда возникает потребность очищать кэш, освобождать ресурсы, это в большинстве случаев не бизнес, а технические детали.
От технических деталей никуда не деться. Вопрос лишь в том, сколько логики нужно написать, чтобы обеспечить технику.
Например, если бы сокет сразу отдавал Observable, то вопроса с деструктором вообще не возникло бы - не нужно было бы в приложении реализовывать.
Очень странно реализовывать фабрику через наблюдателя. Это разные паттерны для разных вещей.
Меня вот этот код смущает:
Действительно, надо будет поправить.
В том то и дело, что это конвеншен, причем mol only. В rxjs, который более распространен и претендует на хреновую, но спецификацию
$mol тоже претендует на хреновую спецификацию :-D
От технических деталей никуда не деться. Вопрос лишь в том, сколько логики нужно написать, чтобы обеспечить технику.
Не только, важно еще как инфраструктурный код отделен от бизнес-кода. Насколько инструмент позволяет изолировать тех детали, что б можно было бизнес код в отдельную библиотеку вытащить и менять движки спокойно. Слабая связанность, интерфейсы.
Очень странно реализовывать фабрику через наблюдателя.
По аналогии, fetch - фабрика для промисов, что тут странного.
$mol тоже претендует на хреновую спецификацию
В гораздо меньшей степени, чем rxjs, который реализован на куче платформ и часть его стандартов перетекла в ECMAScript Observable. Лучше подстраиваться под то, что есть и используется.
По аналогии, fetch - фабрика для промисов, что тут странного.
Странно делать фабрику компонент через промисы - там нет и не должно быть асинхронности.
В гораздо меньшей степени, чем rxjs, который реализован на куче платформ и часть его стандартов перетекла в ECMAScript Observable.
Если вам нравится rx - пожалуйста, используйте его. Зачем его тащить туда, где он как собаке пятая нога? https://github.com/nin-jin/slides/tree/master/orp#Проталкиваем-ФРП
Странно делать фабрику компонент через промисы - там нет и не должно быть асинхронности.
Я имел в виду, что сокет отдает атому асинхронные данные через observable, причем тут компоненты.
Если вам нравится rx - пожалуйста, используйте его. Зачем его тащить туда, где он как собаке пятая нога
Я имел в виду, что есть rx, а в нем есть интерфейс IDisposable, лучше его реализовать, чем придумывать свое название метода.
Я имел в виду, что сокет отдает атому асинхронные данные через observable, причем тут компоненты.
При том, что нет никакой разницы создаёт ли фабрика объект-обёртку над сокетом или объект-вложенный-компонент. И тот и другой нужно будет уничтожить, когда тот будет не нужен.
Я имел в виду, что есть rx, а в нем есть интерфейс IDisposable, лучше его реализовать, чем придумывать свое название метода.
Там только один метод dispose, однако, нужно ещё уметь получать флаг isDisposed. В $mol_object это реализуется через свойство destroyed( next? : boolean )
, через которое можно прочитать значение, либо установить новое. при установке true срабатывают деструкторы.
нет никакой разницы создаёт ли фабрика объект-обёртку над сокетом или объект-вложенный-компонент
Есть разница, в инкапсуляции, destroyed вручную не надо вызывать, а интерфейс публичный. Шансы, что сторонняя либа работает с Observable выше, т.к. es-стандарт, чем с destroyed. Оберток надо меньше клепать или они будут проще.
реализуется через свойство destroyed( next? : boolean )
А кстати, где-то еще используется такой подход с гетерами/сеттерами в одном методе? Может в каком-нибудь языке или фреймворке.
jQuery, d3.js и иже с ними.
а не в js?
Боюсь с ходу не найду.
А не было мысли сделать на get / set?
class Store {
get user(): IUser {
}
set user(user: IUser) {}
}
Тогда станет возможным на этапе написания кода получать ошибку при записи в readonly свойства, скобочки уберутся.
а с форсом решить например так:
store = new Store()
$mol_force(() => store.user = {name: 'test'})
Была, конечно. Но тогда мы потеряем следующие фичи:
Возможность простого переопределения геттера+сеттера.
Возможность параметризировать свойство ключом. Её можно было бы реализовать через Proxy, но у него всё ещё очень плохо с кроссплатформенностью.
$mol_force - совсем уж адский костыль. Основная его проблема в неопределённости области действия:
$mol_force( () => store.user = { name : this.name } )
Кто зафорсится? this.name? store.user? оба? Боюсь тут не получится адекватной предсказуемой логики.
Ну да, $mol_force, тоже костыль. Можно еще так.
setForce(store.user, {name: this.name})
force вообще не частая операция, я бы искал пути привязки к месту вызова конструкции, что б хотя бы часть force автоматизировать.
Возможность простого переопределения геттера+сеттера. Но на практике это всё отлично работает и не доставляет проблем.
Не очень убедительная аргументация. Есть аргументация языками: как сделано в scala, elm, c#, rust, ocaml. Есть аргументация подходами: SOLID, GRASP, классы без наследования (POJO), anemic vs rich model и т.д. Есть аргументация авторитетами: Фаулер, Симан. Есть аргументация аналогиями, смотрите, вот был какой-нить XYZ в adobe flash и общее с ним это Z.
Есть объективные метрики качества кода: связанность, целостность.
У вас ничего этого нет, максимум упомянуты типы, что дескать ts не даст левую сигнатуру записать.
Дело не в сигнатурах, а в месте, где может быть сделано переопределение, больше способов выстрелить в ногу. Например, тот же File из java: мы создаем объект, а потом сеттерами настраиваем его, это плохой стиль.
Переопределение, донастраивание созданной сущности, путем дописывания чего-либо (целостность теряется, разделяется код, создающий и настраивающий сущность), наследование (сильная связанность для масштабирования плохо), возведенное в правило, это вообще не очень хорошо.
Name.hint = ()=> 'Batman'
Почему не сделать это через конструктор?
const hint = $mol_atom('Batman')
const name = new $mol_string({hint})
Можно еще так.
Напишет пользователь так:
setForce(store.user.name, {name: this.name})
И что куда зафорсится?
Почему не сделать это через конструктор?
setForce(store.user.name, {name: this.name})
Это некорректный код. Этого не даст сделать сигнатура setForce:
// @flow
function setForce<V>(v: V, values: $Shape<V>) {}
В любом случае эта конструкция будет эквивалентна store.user.name(name, true)
Я понимаю, форма с форсом универсальна. Но если форс не такая частая операция, может как-то можно для 90% случаев спрятать его и не давать всем подряд делать force.
Сейчас же получается нет никакого разделения: объявили $mol_mem свойство и set и force доступны из паблика всем и всегда?
Все примеры конечно проигрывают по сложности примеру с форсом, но по очевидности и разделению ответственности и безопасности выигрывают.
class Store {
@$mol_reset
resetUserCache() {}
@$mol_set
setUserCache() {}
@$mol_mem
user() {}
}
store.resetUserCache() очевиднее чем store.user(undefined, true)
Ждём: Microsoft/TypeScript#16703
extends Base тоже костыль, реально нужно что-то вроде scala case classes
Это некорректный код.
Суть не в этом, а в том, что setForce нереализуем, ибо аргументы вычисляются до его вызова и никак не догадаться каким свойствам они соответствуют.
можно для 90% случаев спрятать его и не давать всем подряд делать force
Так в 90% случаев его в сигнатурах и нет. В обёртках в основном.
и set и force доступны из паблика всем и всегда?
В этом нет ничего плохого.
store.user( undefined , $mol_atom_force )
Тут мы говорим "Хочу, чтобы свойство user безусловно приняло неопределённое значение", а значит при следующем обращении оно пойдёт "определяться".
store.user( u , $mol_atom_force )
А тут "Хочу, чтобы свойство user безусловно приняло значение u", а значит при следующем обращении возвращено будет именно это значение.
В этом нет ничего плохого.
Это субъективное утверждение. По мне, так объективно хуже - больше ручек управления, когда 2/3 из них не требуется, больше возможностей выстрелить в ногу.
Все, что не используется из вне - должно быть инкапсулированно, хотя б возможность такая должна быть.
Тут мы говорим "Хочу, чтобы свойство user безусловно приняло неопределённое значение",
Не очевидно, это условности mol. Мы это подразумеваем, но не говорим. Если бы мы хотели сказать, то так бы и сказали: store.resetUser() или store.directSetUser(u).
Все, что не используется из вне - должно быть инкапсулированно, хотя б возможность такая должна быть.
Звучит как догмат, не находите? По моему опыту в фанатичном сокрытии больше вреда, чем пользы. Например, когда нужно расширить сторонний компонент, но из-за сокрытия я это сделать не могу без горы копипасты приватного кода.
Не очевидно, это условности mol.
Даже если сходу это не очевидно, это легко понять и далее использовать, не копипастя кучу методов вида reset и directSet.
Кстати, там в таске по тайпскрипту подсказали неплохой воркэраун, так что теперь можно писать так:
const Name = $mol_string.make({
hint : ()=> 'Batman' ,
})
extends Base тоже костыль, реально нужно что-то вроде scala case classes
В чём отличие?
Например, когда нужно расширить сторонний компонент, но из-за сокрытия я это сделать не могу без горы копипасты приватного кода.
Там еще было слово "возможность". Когда я хочу запретить запись в какое-либо поле mol_atom, я это сделать не могу, с горой копипаста или без горы.
Даже если сходу это не очевидно, это легко понять и далее использовать, не копипастя кучу методов вида reset и directSet.
А как же интуитивная понятность кода? Чес. слово, даже прочитав ридми и примеры по $mol, это понять не легко. И я не только за себя говорю.
Есть конечно в ридми mem строчки, но это не очень сравнимо с документацией того же mobx.
/// Force push value to cache
/// Force cache ignoring and pulling fresh value
Например, не очевидно, что store.user.name('test', $mol_atom_force) реально метод не вызовет, а запишет в кэш, а store.user.name(undefinde, $mol_atom_force) метод вызовет и запустит актуализацию.
В чём отличие?
В зависимости от конкретной нестандартной реализации, а не спецификации языка. А если мой класс с моделью в сторонней библиотеке и без Base? Композиция дает больше гибкости, по мне так лучше метод скопипастить:
class User {
copy(user: $Shape<User>): User {
return Object.assign({}, this, user)
}
}
Object.assign можно в хелпер упрятать. Кода почти столько же будет как и с extend.
Когда я хочу запретить
Что за комплекс вахтёра? :-) Поставьте _перила - кто перелезет, берёт ответственность на себя.
Например, не очевидно, что store.user.name('test', $mol_atom_force) реально метод не вызовет, а запишет в кэш, а store.user.name(undefinde, $mol_atom_force) метод вызовет и запустит актуализацию.
Согласен, надо акцентировать на этом внимание.
А если мой класс с моделью в сторонней библиотеке и без Base
Очевидно у него будет другой API. И что?
Что за комплекс вахтёра?
Что за стремление подогнать мир под себя? Наследование, композиция, инкапсуляция, полиморфизм. Мы про ООП говорим же, пусть реактивном. Как в ОРП сделать инкапсуляцию, вот свойства - в них пишите, а из эти только читать можно.
Очевидно у него будет другой API. И что?
Не факт, что другой. Вот есть простой класс, описывающий модель, я могу от него отнаследоваться и доопределить что-то, но если хочу добавить в него функциональность Base, ничего не выйдет, это ж не миксин.
Как в ОРП сделать инкапсуляцию, вот свойства - в них пишите, а из эти только читать можно.
https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование)
но если хочу добавить в него функциональность Base, ничего не выйдет, это ж не миксин.
И что вы предлагаете?
https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование)
И все же, как в вашем подходе разделить ответственность геттер, сеттер, кэш. Спрятать сеттер с кэшем, там где он не понадобится by design, например в вычисляемых значениях.
И что вы предлагаете?
Я выше уже писал.
На мой взгляд не надо ничего прятать. Не будете же вы столовые ножи цепями приковывать к столу, чтобы никто не воспользовался ими за пределами кухни?
Перейти на скалу? Или вы о чём?
Есть flow, ts. Они дают возможности для этого. В разных языках много внимания уделяется безопасности. В ts readonly, во flow ковариантные, контрвариантные типы и т.д.
Почему бы не дать пользоваться этими фичами, продумать несколько форм записи. Задача хорошего подхода - дать выбор, а уж программист сам решит безопаснее ему нужен код или лаконичнее. mol же на ts написан, почему чуть более в полной мере не использовать его возможности.
По поводу Base, я предлагал не делать наследованием, а композицией, либо метод добавить, либо хелпер.
Не, "выбор" приводит к бардаку. Поэтому даже плохой стандарт лучше, его отсутствия. В $mol мы сделали ставку на кастомизацию - можно взять любой сторонний компонент и настроить его под себя как нужно тебе, а не как это предусмотрел автор компонента. Отсюда и требование, что для каждого вложенного компонента должно быть отдельное свойство в компоненте-владельце.
Композиция плохо дружит со статической типизацией. Главным образом это касается сложности написания кода и выводимых сообщений об ошибках.
Я про данные пока, компоненты причем тут. Как-то это все не очевидно, на примерах можно показать?
Композиция плохо дружит со статической типизацией.
А где она плохо дружит, пример можно?
Компоненты - яркий пример, где особенно нужна кастомизация. А так, это те же самые классы, что и в случае с данными.
Например, если вы собираете тип из кусков, то в сообщении об ошибке будет описание структуры типа, а не нормальное человекопонятное имя.
Ладно, это наверное холиворный разговор, я верю, что так и так можно сделать не хуже.
Вопрос по реализации атомов, чем set должен отличаться от push. Я раньше думал дело в normalize, однако и там там он есть, какая-то игра со статусами (зачем их, кстати, столько). set -> obsolete -> slaves.check, push -> slaves.obsolete -> slaves.check
Нет, вся разница в вызове handler. handler вызывается для коммуникации с другими состояниями (затягивание/проталкивание). push - это чисто запись в кеш, без взаимодействия с другими состояниями (кроме уведомления зависимых, конечно).
Инициатором set является потребитель, а push - источник.
Много статусов нужно, чтобы различать ситуации:
А для чего различать статусы checking/obsolete, для отладки?
А нет ли примера, демонстрирующего индикацию загрузки/сохранения и ее кастомизацию? Я не нашел.
Что-то вроде такого
В примерах, что я видел, слишком быстро это происходит, добавить бы setTimeout