Pull to refresh

Comments 52

https://github.com/LabEG/reca - тот же mobx, со встроенным DI и удобным коннектором к вьюхе. Уже больше года работает в энтерпрайзных проектах без нареканий.

Замечательно, что такая библиотека существует. Однако, её разработчики преследуют несколько иные цели, нежели я. MVVM предназначен именно для разделения логики и отображения. И напрямую от DI никак не зависит. Я просто предоставил возможность подключения DI. Причем не какой-то определенной DI библиотеки, а любой

Я думаю холивар можно прекратить, если разделить крупные проекты на большие или сложные.

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

А если у вас крупный проект это веб-редактор видео или ещё какая кастомная штука - тогда редакс идеально подходит и вся его многословность помогает держать в узде сложную логику.

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

В этом и смысл. MobX также позволяет "держать в узде сложную логику", предоставляя при этом более простой инструментарий

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

А если у вас крупный проект это веб-редактор видео или ещё какая кастомная штука - тогда редакс идеально подходит и вся его многословность помогает держать в узде сложную логику.

Ахахах, боже вот это бред

Нет. Redux просто не нужен. Особенно в больших приложениях. Он их убивает. Большие приложения пишутся толпой с разным уровнем квалификации. Redux при этом располагает их всех к плохим подходам. Например:


  • Модуль А читает данные модуля Б, хотя они формально не связаны. Почему? Ну просто потому что удобно, вот жеж оно лежит. Паразитные хаотические связи. Это потом ооочень тяжело чинить. Изменения в одном месте ломают неожиданные места в другом. Если ещё и без Typescript-а то это игра в сапёра.
  • Код разбросан по всей кодовой базе. Тут у нас action type-ы, тут сами action-creator-ы, тут reducer-ы, тут их саги, тут ещё бог знает что. В то время как логично логику держать цельно. В итоге опытне редаксоводы героически сражаются с лишними абстракциями придумывая сотни разных велосипедов. Сам такой был.
  • По-умолчанию у нас любая часть любого reducer-а может реагировать на любой action. Это приводит к сильно-запутанному коду. Мы снова в ситуации когда модуль А реагирует на action из модуля B, потому что "удобно" и дедлайн. Да и все ж говорят что pub sub "это круто".
  • Производительность вызывает вопросы. Все эти useSelector-ы и connect-ы вызываются вообще всегда на любое изменение вообще все. А дальше песни и пляски с stale props, zombie childrens.
  • Чем меньше мусорного кода тем проще код в поддержке. Redux это тонны бойлерплейта.
  • Весёлые приключения с переиспользованием redux-спаггети когда какой-то модуль понадобился на странице сразу в нескольких экземплярах (каждому свой state). Всё решается, конечно, но выглядит как лютый изврат и ещё больше бойлерплейта.
  • Веселье со всякими race condition-ми и state-ом от ранее умершего компонента, который достался по наследству. Добавляет сложности в те места, где без глобалки (а redux это по сути одна глобальная переменная) таких проблем никогда и не было бы.

Короче я устал. Резюмируя: redux это распиаренная пачка антипаттернов. И в первую очередь он противопоказан большим проектам. Они с ним реально гниют.


Единственные плюсы redux-а:


  • Можно весь state упаковать в JSON и переиспользовать. Например восстановить сессию с того же места. Или синхронизировать разные устройства\окружения.
  • Debugging это скорее сильная сторона любого immutable подхода. В том числе и redux-а.

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

Я понял Вашу терминологию, но хочу отметить два момента.

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

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

А если у вас крупный проект это веб-редактор видео или ещё какая кастомная штука — тогда редакс идеально подходит и вся его многословность помогает держать в узде сложную логику.

^ это ваши слова. А мой тезис — redux не нужен вообще. Для крупных проектов типа "видео-редактор" или "кастомных штук", и для малых проектов. Для всех проектов.

Это всё хорошо, но зачем MobX, когда уже есть $mol_wire, который минимум в 2 раза лучше по любым параметрам, плюс умеет сам освобождать ресурсы, просовывать IoC контекст, давать всем объектам уникальные человекопонятные имена, абстрагировать от асинхронности и прочее? Ваш пример из начала статьи выглядел бы как-то так:

import { $mol_wire_solo as mem } from 'mol_wire_lib'

class CounterModel extends Object {
  
	@mem count( next = 0 ) { return next }

	increase() {
		this.count( this.count() + 1 )
	}
	
}

class CounterView extends Component< CounterView > {
	
	@mem counter() { return new CounterModel }
	
	@mem render() {
		return (
			<div>
				<span>Counter: { this.counter().count() }</span>
				<button onClick={ () => this.couter().increate() }>increase</button>
			</div>
		)
	}
	
}

А потом мы такие, хренак, хотим хранить данные в локальном хранилище:

class LocalCounterView extends CounterView {
	
	@mem localStore() {
		return new LocalStore
	}
	
	@mem counter() {
		const model = new CounterModel
		model.count = next => this.localStore().value( 'counter', next ) ?? 0
		return model
	}
	
}

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

MobX скачивают в 1000 раз чаще, чем данную библиотеку. И MobX образовало вокруг себя довольно большое сообщество. Поэтому заинтересованных в небольшой доработке уже существующего популярного решения будет в разы больше, чем в доработке для малоизвестной новой библиотеки. А я своей статьей хотел именно заинтересовать других разработчиков

Опять эта апелляция к популярности вместо технических аргументов. Мы тут менеджеры или программисты?

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

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

Кстати, если же вы против популярного, и за все быстрое, по такой логике можно начать писать какой-нибудь свой WASM движок на C вместо JS. Или даже свой браузер, где JS не будет и в помине.

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

развитое сообщество мне в этом не помогает

В этом помогает отзывчивый мейнтейнер. Сообщество с более-менее сложной проблемой может только посочувствовать.

Погоня за быстродействием - это всегда риск

Обоснуйте.

вы против популярного, и за все быстрое

Не выдумывайте. Я за популяризацию лучших технических решений.

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

Не даёт. Но повышает число найденных ошибок. А чиниться они могут годами.

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

Каждая вьюмодель должна быть привязана ко вью и, следовательно, при удалении вью из разметки вьюмодель должна "умирать";

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

Каждая вьюмодель может (и зачастую будет) являться стором;

максимум состояние лоадинга

Этот стор может использоваться не только в компоненте, который создал вьюмодель (т.е. внутри вью), но и во всех дочерних компонентах вью;

можно воспользоваться mobx-react-lite и через обсерв ререндерить компоненты. Концепция в принципе должна предполагать, что если вам завтра захочется переписать на Vue, то вы просто перерисуете компоненты

Вью и вьюмодели знают о существовании друг друга

а зачем ? точнее не так, зачем VM знать о view ?


К несчастью, я являюсь человеком живущим с этим в проме (CRM система), если в двух словах, то реализация должна выглядеть следующим образом:

Репозитории, ЮзКейсы, Сущности (или модель сущности), и Вьюмодель могут быть извлечены из DI контейнера
Репозитории, ЮзКейсы, Сущности (или модель сущности), и Вьюмодель могут быть извлечены из DI контейнера

В данном архитектурном решении разрабатывает N команд и вот основные минусы этого подхода:

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

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

  • Боль и страдания уготованы вам так же, если вы изменяете данные в наблюдаемых объектах mobx через создание нового объекта (к примеру через спред оператор). Как говориться вызывайте спасателей, мы ищем где потекла память.

  • tsyringe - более-менее ведет себя в монолите, но чуть больше (например модульность) - пиши пропало. Связывание контейнеров очень увлекательная игра в которой Вы - не выиграете :) Более-менее ведет себя inversify.

  • Вернемся к mobx, допустим у Вас есть большая структура бизнес данных, которую необходимо обсервить. И тут будет ждать не ожиданость, Что-то изменили на N уровне структуры - а компонент не ререндериться, вы берете бубен и идете плясать. toJS в геттере конечно помогает, но батенька это надо еще понять, почему оно не ререндериться.

И это только верхушка айсберга. Можно наверное и жить в виде View->ViewModel->Model->Entity, но в сложных решениях необходимо более мелкое разделение что бы была возможность переиспользования.

Есть конечно и плюсы, но боли тоже много. Советую почитать о чистой архитектуре, раз уж вы пожаловали к нам на дно %)

ViewModel шикарная вещь на любых проектах, любого размера и сложности. Максимальная простота, минимум кода, все максимально очевидно, ибо элементарно заходишь в любой обработчик какого ни будь клика и там сверху вниз все написано, что происходит, откуда что берется и куда записывается. Вот прямо сразу все это видно и лежит на ладони. Не надо лазать в 10ки файлов, и раскапывать дебри всевозможный абстракций.
То что ты советуете в виде View->ViewModel->Model->Entity и ещё более мелких разделений это ничто иное как лютейшая дичь, усложнение элементарных вещей на ровном месте, иными словами вы просто берете лопату и копаете под собой, вопрос зачем? Ведь все элементарно. Без шуток, реально элементарно.
А если руки не из нужного места и вы не умеете писать хороший код и выстраивать архитектуру, то вам ничего не поможет, не чистая архитектура, ни 100500 слоев и т.п.

Это стол - за ним едят, это вилка - ей едят, это стул - за ним сидят. VM конечно поставляет методы для работы через которые ui взаимодействует с моделью, но если VM будет и хранить в себе данные и заниматься вообще всем (в том числе и натягиванием совы на глобус), то тогда зачем выносить что-то в VM? храните все в компоненте на 2000-3000 строк.

Архитектурные решения для того и создаются, что бы иметь консистентный подход при разработке фичей и разработчик четко понимал, на каком уровне что происходит. Для примера: если мне потребуется в разных частях приложения делать какие-либо действия (например пускай это будет запись и обновление в IndexedDB каких либо данных, или вызов эндпоинта), то в моем случае я из VM просто вызову соответствующий useCase (а в более крупном представлении метод из модели), в вашем же случае, я надеюсь не стоит пояснять, сколько раз вы задублируете код и какие проблемы это потянет если вы захотите изменить логику того или иного флоу.

То что я советую в виде View->ViewModel->Model - это MVVM как странно бы это не звучало, и эти четыре буковочки значатся в заголовке статьи и лейтмотивом бегут через весь текст. Прошу заметить, что в минусах, описанных мной, не значатся архитектурные проблемы, так как данный подход используется много где в отличии от веба, и он обкатан годами, а только лишь конкретные проблемы в используемых решениях.

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

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

Для примера: если мне потребуется в разных частях приложения делать какие-либо действия (например пускай это будет запись и обновление в IndexedDB каких либо данных, или вызов эндпоинта)


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

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

Чуть ниже я описал пример реализации. Вы можете с ним ознакомиться, а так же предложить решение для предложенного примера в рамках вашего понимания элементарности ☺️. Код не обязательно, просто опишите, как должно работать.

Утро. опечатался. конечно же "это стул - на нем сидят"

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

Наверное, я соглашусь, то, что я показал в статье - это не совсем реализация MVVM. И не только из-за того, что там нет модели. Однако, это подход концептуально близок к этому паттерну, поэтому я так его и назвал.

В указанном мной подходе необходимости в абстракции модели зачастую нет. Да и в целом, модель из MV-паттернов является абстракцией скорее необходимой для бэкенд разработки. Вы можете, конечно, вместо вьюмодели хранить observable поля в специальном объекте, который бы удалялся при размонтировании вью. Но реально все что изменится в таком подходе - это то, как вы будете получать данные. Не viewModel.field, а viewModel.model.field.

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

Теперь об остальных замечаниях.

Я специально разделил понятие DI и MVVM. В моем представлении те данные, которые нужны только одному компоненту логично хранить именно во вьюмодели, а то, что нужно в разрозненных частях, - в DI контейнере. Уже такое деление помогает разработчику, взглянув на данные, понять как и где они используются. Поэтому не делать MVVM стором я бы не стал.

Учитывая, что я описал свою точку зрения по необходимости модели, ответ на то, нужно ли удалять вьюмодель при размонтировании вью становится очевиден, ведь MVVM хранит его данные. Но даже, если бы она не хранила данные, а содержала бы только логику, я бы все равно советовал её удалять. Во-первых, если ваши вьюмодели являются синглтонами, то вы не сможете использовать несколько вью с одинаковыми вьюмоделями. Во-вторых, вьюмодели должны содержать логику - как минимум функции по обработке состояния. И если не уничтожать вьюмодели, то память приложения будет постепенно засоряться теми данными, которыми пользователь, может и не воспользоваться больше.

Вашу мысль про mobx-react-lite я не особо понял, если честно. Вы бы не могли её расписать?

Зачем вью и вьюмодель знают о существовании друг друга? Ответ на этот вопрос таится в самой статье. Вьюмодель способна обрабатывать логику на разных жизненных этапах вью. Таким образом можно полноценно разделять логику и отображение, и даже от хуков во вью отказаться. К тому же, вьюмодель знает о пропах вью. Таким образом она может навешивать реакции на их изменение, а дети вью могут без проп дриллинга получать нужный проп. Что влияет как на производительность, так и на простоту анализа кода.

Понятие чистой архитектуры - это всегда условная вещь. 

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

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

Я как раз подсветил как раз этот момент в минусах, но как только человек разбирается в базовых принципах, процесс работы над фичей происходит гораздо быстрее и гибче. Разработчики используют один и тот же подход на уровне всего приложения и если посадить разработчика на фичу, которую делал другой разраб - у него не возникает "Да #@$@, кто это писал" и ему нет необходимости тратить время что бы понять как оно работает, а так же у него не возникает вопросов "А как мне это написать". Он четко знает, что маппинг - тут, бизнес данные храним здесь, если мне что-то нужно оттуда - я возьму это оттуда.

Я ни в коем случае не пытаюсь сказать что вот подход - использовать его можно так и никак иначе, но скажем есть автомобиль, есть тип автомобиля - седан, вы же не называете все автомобили седанами, а только те, которые соответствуют каким-то критериям. Вы же не называете кабриолет джипом, лишь потому что они схожи концептуально, тут тоже самое, так как тем, кто читает статью не приходиться додумывать "А что хотел сказать автор?" или например если статью будет читать тот, кто не разбирается в вопросе, ведь тем самым он может использовать это за референс и абсолютно наивно полагать, что MVVM строится именно таким образом. Ни в коем случае не критика в Ваш адрес, а просто небольшое замечание, вы в любом случае большой молодец, что ресерчите это направление.

Не viewModel.field, а viewModel.model.field

касаемо этого:
Посредством DI вы внедряете зависимости в VM. К примеру:

// Model.ts
@injectable()
export class SomeModel implements ISomeModel {
  id = '';
  serviceDesc = '';
  serviceName = '';
  systemName = '';

  constructor() {
    makeAutoObservable(this);
  }

  fillModel(): void {
    this.id = '1';
    this.serviceDesc = 'Some description';
    this.serviceName = 'someService';
    this.systemName = 'someSystem';
  }

  dispose(): void {
   this.id = '';
   this.serviceDesc = '';
   this.serviceName = '';
   this.systemName = '';
  }
}



//ViewModel.ts
@injectable()
export class SomeVM implements ISomeVM  {
  loading = false;
  
  get item(): ISomeModel {
    return this.someModel;
  }

  constructor(
    @inject('someModel')
    protected someModel: ISomeModel
  ) {
    makeObservable<ISomeVM>(this, {
      loading: observable,
      item: observable,
      init: action.bound,
      dispose: action.bound
    });
  }

  init(): void {
    this.loading = true;

    try {
      this.someModel.fillModel();
    } finally {
      this.loading = false;
    }
  }
  
  dispose(): void {
    this.loading = false;
    this.someModel.dispose();
  }
}



//ui.tsx
const SomePage: FC = () => {

  // кастомный хук который вытягивает нужную VM из контейнера
  const myVM = useViewModel<ISomeVM>('SomeVM');
  
  if(myVM.loading) {
    return (<>Loading...</>)
  }
  
  return (
    <>
      <div>{myVM.item.id}</div>
      <div>{myVM.item.serviceDesc}</div>
      <div>{myVM.item.serviceName}</div>
      <div>{myVM.item.systemName}</div>

      <button onClick={myVM.init} > Загрузить </button>
      <button onClick={myVM.dispose} > Очистить </button>
    </>
  );
};

export default observer(SomePage);
// observer - функция из mobx-react-lite которая обсервит состояние и ререндерит компонент.

Маленький пример внедрения зависимостей, эту же модель вы можете внедрить и в какой-либо другой VM в которой она вам понадобиться. А так же, эту VM вы можете использовать в любом компоненте, в котором она понадобиться. Что означает переиспользование. Мы не кладем все яйца в корзину, а разделяем так, что бы можно было переиспользовать.
Бонусом ко всему - без проблемное тестирование любого участка кода.
Касаемо хранения VM и моделей в памяти, это копейки, в основном память кушают бизнес данные, которые мы выгружаем методами dispose. Можно очищать к примеру при выходе с роута, а при входе - дергать init метод из VM (Да, описываем это в конфиге роута, желательно использовать агностик, к примеру router5).

Думаю пример выше ответит на большинство вопросов, но если что-то не ясно, я с радостью объясню.

Каждый занимается своим делом

Я думаю, каждый останется при своем мнении. Я считаю, что такие абстракции могут быть далеко не всегда обоснованы. Ваш код может и будет более гибким, но так ли часто нужна эта гибкость? На моей практике гибкость нужна была исключительно редко, и там только где она действительно нужна, я прибегал к подобному делению. В остальных же случаях "яйца в одной корзине" мне только помогали. Потому что проще носить одну корзину с 10 яйцами, чем десять корзин с 1 яйцом

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


Для примера приведу пример ?: Мы хотим логировать каждое действие пользователя (клики, вызов эндпоинтов, переход по роутингу) складывать это в какое-либо хранилище и отсылать на сервер когда накопилось N количество данных.
Решение:
во всех VM мы внедряем модель "LogCollectorModel" и просто на каждом методе VM мы вызываем метод this.logCollectorModel.addToLog(someLog). В методе модели addToLog, мы проверяем, скопилось ли достаточное количество данных и если да, то отправляем, если нет, то добавляем в хранилище.

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

А как бы вы решили данную задачу при описанном подходе?

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

А ещё непонятно, почему в вашем примере модель имеет какую-то логику. Если вы хотите использовать "Модель", то она должна только хранить данные.

Ролевая модель и аутентификация у меня были, и я делал их отдельными DI синглотн контейнерами. Они не были сущностями из паттерна MVVM. В вашем же примере, я бы создал вообще отдельную сущность, как некий middleware, который бы использовал в `configure({ vmFactory })`

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

Модель (англ. Model) (так же, как в классической MVC) представляет собой логику работы с данными и описание фундаментальных данных, необходимых для работы приложения.


Бизнес требования в сложных системах могут быть очень разными и это наиболее простое из того что может быть. Это по сути дела, обычный, самый стандартный логгер. Мы не логируем вызов любого метода VM, мы логируем действия пользователя. Решение через middleware конечно имеет право на жизнь, но я не спроста указал что в логи мы можем писать что угодно в зависимости от ситуации (someLog). И если это не сущности MVVM, то как мы их классифицируем и куда положим? А будет ли это универсальным решением? как нам это использовать например в роутинге что бы залогировать переход?

Мы снова возвращаемся к проблеме гибкости и расширяемости. Без всех вводных хорошего решения не придумать. А продумывать заранее все возможные ситуации - сильно усложнять себе жизнь и замедлять процесс разработки.

Вот именно по этому мы и используем готовые архитектурные решения (даже если они нам кажутся избыточными, урезать их не стоит, так как дорабатывать потом - себе дороже) + если в решении MVVM используется модель - мы используем модель. И нам легче общаться между собой, так как используя этот термин, мы подразумеваем четкую концепцию. Мы абсолютно точно знаем, что ВМ в только поставляет данные и никак иначе, потому что так написано. Внедряя зависимости, мы получаем гибкость, консистентность решений и хорошую документированность. Потому как нам не надо описывать что в таком случае мы делаем так, в таком так. У нас одно решение на все случаи.

Мы хотим логировать каждое действие пользователя (клики, вызов эндпоинтов, переход по роутингу) складывать это в какое-либо хранилище и отсылать на сервер когда накопилось N количество данных.

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

Ну да ладно, предположим что каким-то чудом внезапно на завершающей стадии проекта нам это понадобилось. Что в реальности 1 из 1000000000. Ровно так же как любой "аргумент" который начинает с "А вот если мы захотим ....".

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

Когда обрастете реализованными проектами и опытом(я имею ввиду реальным, т.е. хотя бы не меньше 15 проектов с нуля реализуете сами), то вы придете к "KISS (Keep it simple, stupid), YAGNI (You aren't gonna need it), Чем меньше, тем лучше (Less is more)" во главе всего. То есть даже сами того не подозревая, т.к. эти принципы станут само собой разумеющимися.

И так к реализации:


клики
- window.addEventListner('click', handler);

вызов эндпоинтов - Разумеется любой уважающий себя разработчик не дергает энпонты напрямую fetch, axios и т.п. а делает это через специальную обертку, аля apiRequest(method, url, data). Так вот, просто внутри этой обертки логируем. На крайняк что-то такое https://gist.github.com/benjamingr/0433b52559ad61f6746be786525e97e8

переход по роутингу - либо средствами роутера ловим, либо если роутер самописный, то вообще элементарно, либо window.addEventListener('popstate', handler);


P.S. Я бы рад отвечать быстрее, но из-за всяких недалеких, обиженных жизнью, которые заминусовали карму из-за того, что мое мнение не сходится с их мнением, приходится писать не чаще 1 раза в час. Понятное дело что серой массе комфортно находится именно в обществе серой массы, но что тогда делать тем, кто мыслит глубже и дальше?

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

1) в задании предложено реализовать самый обычный логгер (стандартный до не могу). И кто вам сказал про завершающую стадию? Рассматривайте это как бизнес-требование на этапе разработки архитектуры.

2) Проблемы в вашем решении:
- не консистентность, а именно в одном случае мы пишем хендлер, в другом мы делаем обертку. если какое либо дейсвие еще понадобиться залогировать, к примеру ошибки на разных уровнях приложения - наверное создадим еще обертку? или 20 оберток.
- Судя по решению, вы предлагаете в хендлере создать большую лапшу из того что надо записать в лог относительно клика? и каждый раз обрабатывая click - мы столкнемся с тем что эту портянку из ифов надо обработать - производительное ли это решение?
- popstate - срабатывает только на хистори, но в приложениях не все переходы пишутся в хистори
- вы не реализовали в вашем описании отправку логов по достижению N элементов.

Вот ваше решение:

Решение:
во всех VM мы внедряем модель "LogCollectorModel" и просто на каждом методе VM мы вызываем метод this.logCollectorModel.addToLog(someLog). В методе модели addToLog, мы проверяем, скопилось ли достаточное количество данных и если да, то отправляем, если нет, то добавляем в хранилище.


Чем оно отличается от вызова функции collecor.log(soleLog); во всех местах где это нужно? Во всех обработчиках событий, в функции которая перехватывает роутинг, в функции которая перехватывает вызовы к АПИ, или же просто рядом с каждым конкретном вызовом вызывать данную функцию.

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

И в чем проблема? Это в итоге вызывает collecor.log(soleLog);

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

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

Какая формулировка "задачи" (никакая) такое и решение. Я не знаю как у вас задумка обработки кликов, вообще всех подряд(в любом месте как в яндекс.метрика на пример) или только конкретных. Это разумеется вы не сказали. Если конкретных, то опять же все элементарно, в обработчике клика вы вызываете функцию логгера с конкретным набором данных присущих конкретной кнопке.

- popstate - срабатывает только на хистори, но в приложениях не все переходы пишутся в хистори

В ваших может и не все, в моих все. И опять же это банальнейший пример, который работает и выполняет свою задачу. Перехватывать переходы по роутингу можно в разных местах. popstate это пример одного из N мест.

- вы не реализовали в вашем описании отправку логов по достижению N элементов.

Зачем? Это максимально простая и тривиальная задача. Вы бы с таким же успехом предложили решить задачу, чтобы при нажатии каждый 10ый раз на кнопку в консоли выводилось сообщение, ура мы это сделали.


Всё это к чему, ваш пример задачи с логгером ну реально элементарный. особенно если о нем заранее известно на этапе проектирования. Я не понимаю почему вы из него пытаетесь сделать архитектурную загадку или феномен. Мое "решение" было конечно же шуточным, ибо задача сформулирована так "поди туда не знаю куда, принеси то, не знаю что". Но главный посыл, что она решается элементарно и каких-то трудностей не может вызвать от слова совсем. Возможно в вашей архитектуре из 40 слоев и тонн бойлерплейта да, она может вызвать проблему. Но т.к. я таким не балуюсь и у меня во главе всего KISS, YAGNI, Less is more, такого рода задачи вообще не могут априори никакой проблемы и сложности вызвать. Тем более если о них известно заранее, просто закладываешь это сразу в архитектуру и дело с концом.

Чем оно отличается от вызова функции collecor.log(soleLog); во всех местах где это нужно? Во всех обработчиках событий, в функции которая перехватывает роутинг, в функции которая перехватывает вызовы к АПИ, или же просто рядом с каждым конкретном вызовом вызывать данную функцию.

Тем , что решение универсальное (вызывается откуда угодно, как угодно и логирует что угодно). Полностью соответствует вашему KISS, так как для описания его мне потребовалось всего пара предложений, куда уж проще и к тому же он лежит в обсуждаемой концепции MVVM. Попробуйте гипотетически придумать ситуацию, в которой оно не сработает или будет не простым.

Действительно, пример с логером элементарный, он один и он логирует там где это только необходимо.
В случае с eventListener не все так однозначно, он тупо логирует абсолютно все клики и в том числе на тексте - везде. В данном случае в handler нам придется сделать switch-case и разделять контент отправляемый в логгер относительно event.target. Замечу, что данный switch-case будет срабатывать при каждом клике. (я надеюсь не стоит разбирать ситуацию, в которой мы в надежде избавиться от switch-case создаем N eventListener которые крутятся в реалтайме и вызывают проблемы с производительностью).

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

Какая формулировка "задачи" (никакая) такое и решение. Я не знаю как у вас задумка обработки кликов, вообще всех подряд(в любом месте как в яндекс.метрика на пример) или только конкретных. Это разумеется вы не сказали. Если конкретных, то опять же все элементарно, в обработчике клика вы вызываете функцию логгера с конкретным набором данных присущих конкретной кнопке.

При неопределенности бизнес требований обычно два варианта, вы либо разрабатываете универсальное решение, либо уточняете требования. Аналитики и бизнес - не разработчики, они не видят всех потенциальных возможных проблем, а мы видим - это наша работа. Logger is just a logger, nothing more. в данном случае.
Разработчики React не пишут по бизнес-требованиям, а разрабатывают универсальное решение решающее конкретные задачи в конкретном контексе.

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

И модульные окна с оверлеями? Вы там в работе с хистори роутера не умерли? ?

шуточное решение или нет, масштабируемость этого решения крайне низкая.
В данном контексте мы рассматриваем только MVVM, там нет 40 слоев, а всего лишь 3 - Model.View.ViewModel.
Если говорить о решении в рамках большого энтерпрайз приложения которое разрабатывает большое количество разработчиков, то это другая тема, не обсуждаемая здесь.

P.S.: На всякий случай - это не я Вам минусы ставлю.

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

Любое, даже самое банальное и простое приложение можно легко превратить в "энтерпрайз" и называть его так, достаточно только разбить в нем все на много слоев абстракций, попытаться предусмотреть все возможные варианты развития событий и заложиться под все подряд, в обязательном порядке строго следовать всем принципам solid, заодно в качестве view заюзать Angular + RxJS, так сказать чтобы жизнь мёдом не казалась и вуаля. Ваше типичное простое приложение тем "энтерпрайз". Просто тупо из-за того, что его усложнили донельзя на ровном месте.

Вообще у меня всегда улыбка на лице когда люди говорят "энтерпрайз". Потому что если откинуть все предрассудки и быть откровенными самим с собой, то на самом деле, ничего сложного в этих приложениях нет в 99.9% случаях объективно, все там просто и тривиально. Но, за счет того, что с точки зрения подхода к написанию кода это приложение решили усложнить на 2 порядка, оно стало входить в категорию "знтерпрайз" (которая кстати ни грамма уважения в моих глазах не вызывает).

И модульные окна с оверлеями? Вы там в работе с хистори роутера не умерли?

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

Тем , что решение универсальное (вызывается откуда угодно, как угодно и логирует что угодно)

Так collecor.log(soleLog) точно так же вызывается откуда угодно, как угодно и логирует что угодно)

Так collecor.log(soleLog) точно так же вызывается откуда угодно, как угодно и логирует что угодно)

Тогда залогируйте мне пожалуйста следующее:

<a href="#" onClick={vm.someHandler}> link0</a>
<a href="#" onClick={vm2.someHandler1}> link1</a>
<a href="#" onClick={vm3.someHandler2}> link2</a> // Хочу писать в лог 'Пользователь кликнул на ссылку 1'
<a href="#" onClick={vm4.someHandler3}> link3</a> // Хочу писать в лог 'Пользователь кликнул на ссылку 2'
<a href="#" onClick={vm5.someHandler4}> link4</a>

Вот только эти два варианта.
Записи должны где-то сохраняться естественно до передачи.

import { collecor } from 'helpers/collector';

class Vm3 {
  someHandler2() {
    collecor.log("Пользователь кликнул на ссылку 1");
    // ... Весь остальной код
  }
}

class Vm4 {
  someHandler3() {
    collecor.log("Пользователь кликнул на ссылку 2");
    // ... Весь остальной код
  }
}


А куда делись eventListener? :)

Вы уверены что данное решение будет работать и хранить в себе данные ?

А куда делись eventListener? :)

Зачем?) Если в задаче явно указано, логировать не все подряд клики, а конкретные. Если все подряд(например как в яндекс.метрика), то eventListener конечно)

Я же сказал, решение шуточное, ибо формулировка 'задачи" была вообще без каких либо уточнений)

во всех VM мы внедряем модель "LogCollectorModel" и просто на каждом методе VM мы вызываем метод this.logCollectorModel.addToLog(someLog)

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

Мы рассматриваем только относительно концепции используемой в статье и не погружаемся вглубь, иначе дискуссия растянется на несколько дней или месяцев :) конечно есть куча паттернов, при помощи которых это можно автоматизировать на разных слоях(не только в рендере)

Можно разрабатывать и через жопу, конечно, но зачем?

Я немного упростил ваш пример:

export class SomeModel extends Model {
    
    id() {
        return this.data().id ?? 0
    }
    
    serviceDesc() {
        return this.data().serviceDesc ?? ''
    }
    
    serviceName() {
        return this.data().serviceName ?? 'Unnamed Service'
    }
    
    systemName() {
        return this.data().systemName ?? 'Unnamed System'
    }

    // Cached data will be disposed automatically
    @mem data() {
        sleep( 1000 ) // emulate long loading
        return {
            id: '1',
            serviceDesc: 'Some description',
            serviceName: 'someService',
            systemName: 'someSystem',
        }
    }

}

export class SomePage extends View<SomePage> {

    // Используем класс модели из IoC сонтекста
    @mem model() {
        return new this.$.SomeModel
    }

    // Индикатор ожидания появляется автоматически, пока данные грузятся
    @mem render() {
        return <>
            <div>{ this.model().id() }</div>
            <div>{ this.model().serviceDesc() }</div>
            <div>{ this.model().serviceName() }</div>
            <div>{ this.model().systemName() }</div>
        </>
    }
    
}

Не благодарите.

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

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

В качестве поиграться, наверное -да, поиграемся. использование в проме - пока не понятно, решению 10 месяцев, комьюнити нет, куда ехать с ишьюсами - не ясно. и лично меня $ - бесят (но это чертовы вьетнамские флэшбеки) :)

решению 10 месяцев

Молу лет 7 уже. За это время система реактивности 4 раза переписывалась без существенного изменения апи. Последний раз - год назад, да.

комьюнити нет

Я ему передам.

куда ехать с ишьюсами - не ясно

Как обычно - в багтрекер.

Учитывая, что ViewModel знает все о пропсах компонента, кажется, на деле означает, что в итоге связка ViewModel + реактовский компонент чисто для рендера это с технической точки зрения практически то же самое, что компоненты на классах (ViewModel это как классовый компонент без функции render()) :)

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

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

А в подходе MVVM берется все лучшее из двух миров. Компоненты являются функциями, а логика хранится в классе.

Выглядит здорово! Напоминает пакет Elementary для приложений на Flutter.
Я так понимаю, доступа к React Context из VM нет?

Нет, к Context API доступа нет. Однако, вы можете сделать что-то типа

viewModel.someContextValue = useContext(SomeContext)

Sign up to leave a comment.

Articles