hyoo-ru / mam_mol

$mol - fastest reactive micro-modular compact flexible lazy ui web framework.
http://mol.hyoo.ru
MIT License
676 stars 58 forks source link

Пример с индикацией загрузки/сохранения #226

Closed zerkalica closed 6 years ago

zerkalica commented 7 years ago

А нет ли примера, демонстрирующего индикацию загрузки/сохранения и ее кастомизацию? Я не нашел.

Что-то вроде такого

  1. Есть строковое поле ввода и кнопка сохранить
  2. Поле уже с дефолтным значением и в состоянии disabled
  3. Данные в поле ввода медленно загружаются с сервера (setTimeout)
  4. В это время рядом с полем выводится слово "Загрузка..."
  5. После загрузки слово пропадает и disabled с поля снимается, состояние актуализируется
  6. После редактирования и нажатия кнопки происходит медленное сохранение на сервер
  7. Рядом с кнопкой выводится слово: "Сохранение..."

В примерах, что я видел, слишком быстро это происходит, добавить бы setTimeout

nin-jin commented 7 years ago

В Chrome Developer Tools вы можете включить HTTP Throttling на вкладке Network. Если выбрать там GPRS, то вся коммуникация будет идти крайне медленно.

nin-jin commented 7 years ago

У нас есть небольшое демо-приложение, как раз показывающее работу с индикаторами ожидания. Код: https://github.com/eigenmethod/mol/tree/master/app/users Онлайн: http://mol.js.org/app/users/

zerkalica commented 7 years ago

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

Насколько я понял, есть глобальный mol_atom_wait, о котором знают все кнопки и компоненты. Когда любой атом в pending, mol рисует вместо компонента какой-то дефолтный загрузчик и кнопке этот класс устанавливает.

Вопрос был как с этим явно работать и как разделять wait для каждого компонента на загрузку и на сохранение.

nin-jin commented 7 years ago

В том-то и дело, что явная работа с ними как правило не нужна.

Явная работа со статусами есть в компоненте, выводящем сообщение об ошибке: 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#Реактивный-рендеринг

zerkalica commented 7 years ago

Спасибо, более менее понятно.

Как только мы обращаемся к свойству - происходит актуализация, бросается exception. Однако через try/catch можно показать либо ошибку, либо wait, если приходит специальный Error, который бросается в недрах http.

nin-jin commented 7 years ago

Именно так. Бросается он тут: https://github.com/eigenmethod/mol/blob/master/http/http.ts#L76

zerkalica commented 7 years ago

А с observable такую штуку можно подружить?

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

Как встроиться в жизненный цикл pull/put/reap?

nin-jin commented 7 years ago

Примерно так...

Делаем обёртку над сокетом, которая инкапсулирует в себе всю асинхронность:

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 // синхронно запрашиваем данные
}
zerkalica commented 7 years ago

А какая цель этого куска кода?

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/ нуждается в рефакторинге.

nin-jin commented 7 years ago

А какая цель этого куска кода?

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

Почему, кстати $mol_atom_force, а не true?

Чтобы код был самодокументируемым. Абстрактный true не понятно что делает.

В первой версии атомов было очевидное апи с merge, тут не очень понятна разница межу set и push.

Честно сказать, сейчас апи $mol_atom мне не очень нравится (надо бы навести порядок там), поэтому лучше использовать $mol_mem. Вместо merge сейчас есть метод normalize.

тут не очень понятна разница межу set и push.

Она осталась прежней: push безусловно записывает значение в кеш, а set - предлагает новое значение, а пользовательская функция уже решает, что с этим значением делать.

Мне кажется, та бородатая статья https://habrahabr.ru/post/235121/ нуждается в рефакторинге.

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

zerkalica commented 7 years ago

Вместо 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.

nin-jin commented 7 years ago

В отличие от 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.

zerkalica commented 7 years ago

А вам это для каких случаев нужно?

Не знаю пока, просто в чем отличие от 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()
  1. destroyed не нужен, через throw мы отдаем Observable фреймворку, а дальше он управляет его временем жизни, делает push, unsubscribe
  2. Мы не завязываемся больше на реализацию mol_object, mol_atom_wait, mol_atom_force, только на стандартный Observable, интерфейсы проще.

Может что-то упускаю, хотелось бы все случаи рассмотреть, пока не понятно на практике когда какие сочетания 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
nin-jin commented 7 years ago

Не знаю пока, просто в чем отличие от 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-им его самостоятельно.

zerkalica commented 7 years ago

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 - это почти чистые функции, могут быть в других похожих фреймворках использованы.

nin-jin commented 7 years ago

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, чтобы это дело стандартизировать... но боюсь такое начинание завернут :-(

zerkalica commented 7 years ago

destroy в любом случае нужен для контроля времени жизни объектов

Деструкторы в языке с gc, никогда их в js не добавят. Да и что, кроме закрытия ресурсов, контролировать? Атом и так сбрасывает свое состояние по destroy.

Насколько я понял, есть атом и есть контейнер с атомами (mol_object). Приложение - это дерево mol_object, отдающих атомы в свойствах. При этом если нужен destroy - буть добр, отнаследуйся от mol_object в нем определи метод. Имхо, это не очень хороший дизайн, хочется больше свободы.

Стримы тут могут быть альтернативой лишь в одном частном случае, когда собственно объект нам и не нужен и достаточно иметь функцию инициализации и функцию деинициализации.

Можно и без стримов попробовать. Я пытаюсь придумать дизайн с чистыми классами, опираясь только на конструкции самого js, без сторонних библиотек. Если бы в атом передавался некий стрим, который он мог контролировать, как раз можно было бы так сделать. Все же, мне кажется, дело в дизайне и на стримах можно так сделать.

Ещё может понадобиться реактивный флаг "онлайн/оффлайн", метод "переподключиться" и кто его знает что ещё.

Ну тут да, несколько сложнее, можно и на Observable добиться такого, будет более громоздко.

nin-jin commented 7 years ago

Да и что, кроме закрытия ресурсов, контролировать?

Отписываться от событий, очищать кеши, закрывать соединение, вызывать деструкторы у подчинённых объектов. Вообще говоря, ОПР потенциально позволяет отказаться от GC, так как мы всегда знаем нужен ли кому-то объект или нет.

При этом если нужен destroy - буть добр, отнаследуйся от mol_object в нем определи метод.

Не обязательно наследоваться. Можно реализовать метод destroyed в любом объекте.

Я пытаюсь придумать дизайн с чистыми классами, опираясь только на конструкции самого js, без сторонних библиотек.

Ну так в самом js деструкторов нет. А возить их отдельно от объекта - так себе удовольствие. Сейчас реализовано так, что если объект реализует метод destroyed, то он вызывается, когда чистится кеш.

zerkalica commented 7 years ago

ОПР потенциально позволяет отказаться от 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, который много где есть еще.

nin-jin commented 7 years ago

Напоминает счетчик ссылок, но gc делает многие деструкторы необязательными, освобождение памяти например, почему эту идею не распространить и на ОРП.

Потому что оно не работает. GC замедляет программу и ничего при этом не гарантирует. ОРП же даёт двусторонние ссылки между объектами, таким образом каждый объект знает кому он нужен. При этом гарантируется, что все объекты образуют дерево, без циклических зависимостий, которые особенно сложны для GC.

Когда возникает потребность очищать кэш, освобождать ресурсы, это в большинстве случаев не бизнес, а технические детали.

От технических деталей никуда не деться. Вопрос лишь в том, сколько логики нужно написать, чтобы обеспечить технику.

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

Очень странно реализовывать фабрику через наблюдателя. Это разные паттерны для разных вещей.

Меня вот этот код смущает:

Действительно, надо будет поправить.

В том то и дело, что это конвеншен, причем mol only. В rxjs, который более распространен и претендует на хреновую, но спецификацию

$mol тоже претендует на хреновую спецификацию :-D

zerkalica commented 7 years ago

От технических деталей никуда не деться. Вопрос лишь в том, сколько логики нужно написать, чтобы обеспечить технику.

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

Очень странно реализовывать фабрику через наблюдателя.

По аналогии, fetch - фабрика для промисов, что тут странного.

$mol тоже претендует на хреновую спецификацию

В гораздо меньшей степени, чем rxjs, который реализован на куче платформ и часть его стандартов перетекла в ECMAScript Observable. Лучше подстраиваться под то, что есть и используется.

nin-jin commented 7 years ago

По аналогии, fetch - фабрика для промисов, что тут странного.

Странно делать фабрику компонент через промисы - там нет и не должно быть асинхронности.

В гораздо меньшей степени, чем rxjs, который реализован на куче платформ и часть его стандартов перетекла в ECMAScript Observable.

Если вам нравится rx - пожалуйста, используйте его. Зачем его тащить туда, где он как собаке пятая нога? https://github.com/nin-jin/slides/tree/master/orp#Проталкиваем-ФРП

zerkalica commented 7 years ago

Странно делать фабрику компонент через промисы - там нет и не должно быть асинхронности.

Я имел в виду, что сокет отдает атому асинхронные данные через observable, причем тут компоненты.

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

Я имел в виду, что есть rx, а в нем есть интерфейс IDisposable, лучше его реализовать, чем придумывать свое название метода.

nin-jin commented 7 years ago

Я имел в виду, что сокет отдает атому асинхронные данные через observable, причем тут компоненты.

При том, что нет никакой разницы создаёт ли фабрика объект-обёртку над сокетом или объект-вложенный-компонент. И тот и другой нужно будет уничтожить, когда тот будет не нужен.

Я имел в виду, что есть rx, а в нем есть интерфейс IDisposable, лучше его реализовать, чем придумывать свое название метода.

Там только один метод dispose, однако, нужно ещё уметь получать флаг isDisposed. В $mol_object это реализуется через свойство destroyed( next? : boolean ), через которое можно прочитать значение, либо установить новое. при установке true срабатывают деструкторы.

zerkalica commented 7 years ago

нет никакой разницы создаёт ли фабрика объект-обёртку над сокетом или объект-вложенный-компонент

Есть разница, в инкапсуляции, destroyed вручную не надо вызывать, а интерфейс публичный. Шансы, что сторонняя либа работает с Observable выше, т.к. es-стандарт, чем с destroyed. Оберток надо меньше клепать или они будут проще.

реализуется через свойство destroyed( next? : boolean )

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

nin-jin commented 7 years ago

jQuery, d3.js и иже с ними.

zerkalica commented 7 years ago

а не в js?

nin-jin commented 7 years ago

Боюсь с ходу не найду.

zerkalica commented 7 years ago

А не было мысли сделать на get / set?

class Store {
  get user(): IUser {
  }
  set user(user: IUser)  {}
}

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

а с форсом решить например так:

store = new Store()

$mol_force(() => store.user = {name: 'test'})
nin-jin commented 7 years ago

Была, конечно. Но тогда мы потеряем следующие фичи:

  1. Возможность простого переопределения геттера+сеттера.

  2. Возможность параметризировать свойство ключом. Её можно было бы реализовать через Proxy, но у него всё ещё очень плохо с кроссплатформенностью.

$mol_force - совсем уж адский костыль. Основная его проблема в неопределённости области действия:

$mol_force( () => store.user = { name : this.name } )

Кто зафорсится? this.name? store.user? оба? Боюсь тут не получится адекватной предсказуемой логики.

zerkalica commented 7 years ago

Ну да, $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})
nin-jin commented 7 years ago

Можно еще так.

Напишет пользователь так:

setForce(store.user.name, {name: this.name})

И что куда зафорсится?

Почему не сделать это через конструктор?

Ждём: https://github.com/Microsoft/TypeScript/issues/16703

zerkalica commented 7 years ago

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

nin-jin commented 7 years ago

Это некорректный код.

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

можно для 90% случаев спрятать его и не давать всем подряд делать force

Так в 90% случаев его в сигнатурах и нет. В обёртках в основном.

и set и force доступны из паблика всем и всегда?

В этом нет ничего плохого.

store.user( undefined , $mol_atom_force )

Тут мы говорим "Хочу, чтобы свойство user безусловно приняло неопределённое значение", а значит при следующем обращении оно пойдёт "определяться".

store.user( u , $mol_atom_force )

А тут "Хочу, чтобы свойство user безусловно приняло значение u", а значит при следующем обращении возвращено будет именно это значение.

zerkalica commented 7 years ago

В этом нет ничего плохого.

Это субъективное утверждение. По мне, так объективно хуже - больше ручек управления, когда 2/3 из них не требуется, больше возможностей выстрелить в ногу.

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

Тут мы говорим "Хочу, чтобы свойство user безусловно приняло неопределённое значение",

Не очевидно, это условности mol. Мы это подразумеваем, но не говорим. Если бы мы хотели сказать, то так бы и сказали: store.resetUser() или store.directSetUser(u).

nin-jin commented 7 years ago

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

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

Не очевидно, это условности mol.

Даже если сходу это не очевидно, это легко понять и далее использовать, не копипастя кучу методов вида reset и directSet.

Кстати, там в таске по тайпскрипту подсказали неплохой воркэраун, так что теперь можно писать так:

const Name = $mol_string.make({
    hint : ()=> 'Batman' ,
})

extends Base тоже костыль, реально нужно что-то вроде scala case classes

В чём отличие?

zerkalica commented 7 years ago

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

Там еще было слово "возможность". Когда я хочу запретить запись в какое-либо поле 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.

nin-jin commented 7 years ago

Когда я хочу запретить

Что за комплекс вахтёра? :-) Поставьте _перила - кто перелезет, берёт ответственность на себя.

Например, не очевидно, что store.user.name('test', $mol_atom_force) реально метод не вызовет, а запишет в кэш, а store.user.name(undefinde, $mol_atom_force) метод вызовет и запустит актуализацию.

Согласен, надо акцентировать на этом внимание.

А если мой класс с моделью в сторонней библиотеке и без Base

Очевидно у него будет другой API. И что?

zerkalica commented 7 years ago

Что за комплекс вахтёра?

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

Очевидно у него будет другой API. И что?

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

nin-jin commented 7 years ago

Как в ОРП сделать инкапсуляцию, вот свойства - в них пишите, а из эти только читать можно.

https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование)

но если хочу добавить в него функциональность Base, ничего не выйдет, это ж не миксин.

И что вы предлагаете?

zerkalica commented 7 years ago

https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование)

И все же, как в вашем подходе разделить ответственность геттер, сеттер, кэш. Спрятать сеттер с кэшем, там где он не понадобится by design, например в вычисляемых значениях.

И что вы предлагаете?

Я выше уже писал.

nin-jin commented 7 years ago

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

Перейти на скалу? Или вы о чём?

zerkalica commented 7 years ago

Есть flow, ts. Они дают возможности для этого. В разных языках много внимания уделяется безопасности. В ts readonly, во flow ковариантные, контрвариантные типы и т.д.

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

По поводу Base, я предлагал не делать наследованием, а композицией, либо метод добавить, либо хелпер.

nin-jin commented 7 years ago

Не, "выбор" приводит к бардаку. Поэтому даже плохой стандарт лучше, его отсутствия. В $mol мы сделали ставку на кастомизацию - можно взять любой сторонний компонент и настроить его под себя как нужно тебе, а не как это предусмотрел автор компонента. Отсюда и требование, что для каждого вложенного компонента должно быть отдельное свойство в компоненте-владельце.

Композиция плохо дружит со статической типизацией. Главным образом это касается сложности написания кода и выводимых сообщений об ошибках.

zerkalica commented 7 years ago

Я про данные пока, компоненты причем тут. Как-то это все не очевидно, на примерах можно показать?

Композиция плохо дружит со статической типизацией.

А где она плохо дружит, пример можно?

nin-jin commented 7 years ago

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

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

zerkalica commented 7 years ago

Ладно, это наверное холиворный разговор, я верю, что так и так можно сделать не хуже.

Вопрос по реализации атомов, чем set должен отличаться от push. Я раньше думал дело в normalize, однако и там там он есть, какая-то игра со статусами (зачем их, кстати, столько). set -> obsolete -> slaves.check, push -> slaves.obsolete -> slaves.check

nin-jin commented 7 years ago

Нет, вся разница в вызове handler. handler вызывается для коммуникации с другими состояниями (затягивание/проталкивание). push - это чисто запись в кеш, без взаимодействия с другими состояниями (кроме уведомления зависимых, конечно).

Инициатором set является потребитель, а push - источник.

nin-jin commented 7 years ago

Много статусов нужно, чтобы различать ситуации:

  1. Состояние актуально (actual).
  2. Состояние точно не актуально (obsolete) - ставится, если непосредственная зависимость изменила значение.
  3. Состояние возможно не актуально (checking) - ставится, когда в дереве зависимостей есть устаревшие состояния (obsolete).
zerkalica commented 7 years ago

А для чего различать статусы checking/obsolete, для отладки?