Нужно помнить, что "программа - это набор автоматизированных процессов по обработке информации". Но объекты скрывают информацию/данные, потому что объект - это набор методов. Получается, чтобы эффективно использовать объекты - нужно найти такие варианты использования, при которых мы можем абстрагироваться от данных. Хотя бы на время жизни объекта.
Реализация машины состояний (State) через объекты помогает абстрагироваться от текущего состояния машины + её данных, и использовать общие для каждого состояния методы. Например мы составляем заказ. Он может быть в работе (корзина наполняется), а может быть оплачен. Пользователь инициировал useCase "Добавить товар в заказ". Мы вытащили из базы объект заказа и вызвали метод "добавить товар". Объект заказа знает о своём состоянии. Если он в состоянии наполнения - то он добавит товар и пересчитает сумму. Если он уже оплачен, то он выбросит исключение. Клиентский код не знает о состоянии объекта, и работает с ним как с целой машиной. Это разделяет "Что/Как" и "Что/Почему". Объекты дают возможность удобно реализовать такое поведение в коде.
На счёт сторонних сервисов. Представьте, что ваше приложение использует банковский сервис. Вы можете отправлять туда платежи и получать список платежей со статусами. Для доступа к этому сервису по сети нужно отправлять API-ключи + знать, на какой url стучать. Вы можете создать объект, который будет представлять в вашем приложении банковский сервис. Сделать в нём приватные поля с ключами и url. И написать туда 2 метода: "Отправить платёж" и "Получить платежи". Клиентскому коду не нужно знать о вещах, связанных с HTTP. Только методы. Это разделяет "Что/Как", а так же позволяет тестировать клиентский код без отправки сообщений на реальный банковский сервис, используя test doubles.
DDD-Агрегаты - это набор объектов из предметной области с единой границей транзакции. Тот же пример с заказом. Когда вы добавляете в заказ новый товар - должно произойти 2 действия: создаётся OrderItem с ссылкой на товар, и пересчитывается общая сумма заказа. Нельзя создать OrderItem, и не пересчитать сумму заказа. Это одна транзакция. Объекты позволяют объединить всё, что связано с заказом, в одно место и работать с несколькими структурами данных как с одной сущностью.
В общем случае можно сказать, что мы используем объекты, когда хотим работать с данными, не зная их внутреннего устройства. Это полезно на границах системы (клиентский код не хочет знать "как" отправлять в сторонние сервисы информацию, только "что" отправлять и "кому"), при реализации машин состояний (клиентский код не хочет знать текущее состояние машины, он хочет работать с ней как с единым целым) и при определении границ транзакций (работа с несколькими связанными структурами данных, которые должны изменяться одновременно).
Объяснение ООП через собак, кошек и машины не имеет смысла
Программа - это набор автоматизированных процессов по обработке данных. Все парадигмы - это инструменты для реализации
В enterprice программах для разных задач одновременно используются все популярные парадигмы (Struct, FP, OOP)
Используйте объекты как границы транзакций (DDD-Агрегаты), инкапсуляцию машин состояний (паттерн State из GoF) или абстрагирование над устойчивыми понятиями (Инкапсуляция доступа к сторонним сервисам / Классы по типу Collection)
Да, вы правы. По моему определению (принадлежащему Алану Кею XD) эти языки не являются ООП. Только SmallTalk и немного Objective-C. Это не значит, что они плохие. Просто нельзя из-за их проблем хоронить отличную парадигму.
Я не привёл код на SmallTalk, потому что я сам плохо его знаю отличия в синтаксисе там не существенны, но могут запутать.
На SmallTalk это выглядело бы так:
result
ifTrue: [useCase execute]
ifFalse: [response unprocessible].
Если я правильно понял, то вас смутила внешняя схожесть двух конструкций. Но уверяю вас, что разница есть.
В случае if result == true это вы контролируете поток выполнения. Вы знаете, что result == true и что useCase->execute() точно вызовется. При чем вызовется только он.
В случае result->ifTrue(useCase->execute()) вы не контролируете поток выполнения. Вы сказали объекту result отправить объекту useCase сообщение execute. Что он с ним сделает? Пошлёт ли он его вообще? Сделает ли он что-нибудь до или после отправки сообщения? Вы не знаете, потому что потеряли контроль над потоком выполнения.
В функциональной парадигме для той же ситуации работает pattern matching:
return match result with
| true -> executeUseCase,
| false -> createUnprocessibleResponse
Здесь вы знаете все возможные варианты result и обязаны обработать каждый из них. Когда в структурной парадигме я могу не прописывать else, а в объектной не вызывать ifElse.
Это простой пример, и разница кажется несущественной. Я привёл его, как способ быстро проверить, является ли код объектным или структурным. Но идеи, стоящие за каждой парадигмой, заставляют нас по разному думать о программе. И чем сложнее программа, тем сильнее будут проявляться эти различия.
Я думаю, что ваш комментарий поможет применить идеи из статьи при работе с Vue и случайно не разругаться с остальными разработчиками на проекте. Мой подход к написанию кода действительно отличается от принятых практик, потому что я привязываюсь к концепциям, а не к принятым в фреймворке реализациям. Что позволяет мне переключаться между фреймворками не замечая разницы. Это не всем подходит, так что спасибо за дополнение :)
Объясню некоторые выборы в статье, возможно вы взгляните на них под другим углом.
На счёт composable. Вы создали функцию, которая возвращает объект, в котором есть поля и процедуры, работающие с этими полями. То есть - вы создали класс (шаблон для создания объектов). Тут уже дело договорённостей. Если на проекте всю кастомную логику держат в composable, во Vue так принято и разработчикам так привычнее - конечно делайте через них. Суть от этого не изменится - это всё еще класс. Я же люблю для создания объектов использовать механизм, который для них предназначается. А функциям и процедурам отводить свою отдельную роль.
На счёт представления не очень понял. Внутри компонента ProgressBar.vue всё равно придётся указать тип данных пропса (в моём случае Progress). И он должен быть без методов (ProgressModel не подходит), потому что это, как вы правильно сказали, dumb component. И на что он будет ссылаться, если нет разделения на данные и модель, мне не понятно. Но у вас компонент ProgressBar контролируется через модель - а это главное. Остальное - детали реализации, так что для меня это не принципиально.
На счёт контроллеров. В статье я дал им определение: "Это класс объектов, которые подписываются на события от устройств ввода". В моей реализации компонент-родитель подписывается на события, так что относительно MVC для Progress - он контроллер. Да, он же является и mediator (в ваших терминах "smart component"). Но контроллером, относительно MVC для progress, он от этого быть не перестаёт.
Ещё раз спасибо за проявленный к статье интерес и полезное прикладное дополнение для разработчиков на Vue
Будьте уверены, что увидев эту структуру у себя на проекте, вы подумаете, что это шутка по типу FizzBuzzEnterprise.
Однако обратите внимание. Концептуально это действительно единственный способ добиться той низкой связности, которую ставит своей задачей SOLID. При таком дизайне все компоненты защищены от изменений по "частоте", "важности компонентов" и "стоимости".
Но в ваших проектах, скорее всего, и такая сильная защита не нужна. А значит вы можете не делать его 100% SOLID. Об этом утверждении и была моя статья.
SOLID влияет на менеджмент зависимостей. DDD влияет на читабельность. Смысл статьи как раз и был в том, чтобы в небольших проектах сначала думать о читабельности, а уже потом о менеджменте зависимостей. А советы из DDD - это первое, о чем я думаю, говоря про читабельность (clean code уже по умолчанию)
Да, можно использовать DI. Мой ответ из другого комментария:
Чтобы и контроллеры и UseCase'ы соответствовали принципам OCP и DIP действительно нужно столько дополнительных классов. Можно заменить Factories для интерактора используя инъекцию зависимостей и передавать UseCaseActivator как параметр в Controller. Но сильно нашу схему это не упростит, и мой посыл остаётся актуален: "SOLID улучшает coupling, но ухудшает читабельность"
В данном примере предполагается, что Factory будет отдавать нам не только UseCaseActivator , но и RequestBuilder , так как их реализации используются одновременно. На схеме у автора это не показано (и у меня как следствие), что косяк. Но в видео-уроке это проговаривается в контексте.
Так что Factory можно заменить на инъекцию UseCaseActivator и RequestBuilder . И все ключевые зависимости останутся без изменений
Обратите внимание, что в вашем примере функция handleRequest ссылается на createRequestDTO , а та в свою очередь ссылается на RequestDTO . Но RequestDTO используется функцией UseCase . А значит единственный способ упаковки, который не вызовет цикличные зависимости будет такой, как на этой схеме:
В таком случае любое изменение внутренней структуры RequestDTO потребует изменение как минимум createRequestDTO , что означает перекомпиляцию и перевыпуск компонента контроллеров (того, что слева от двойных линий). А именно этого мы пытались избежать, добавляя абстракции, связанные с интерфейсом-маркером (красный блок на второй картинке в статье).
Поэтому если вас не волнуют вопросы перекомпиляции и перевыпуска компонентов - лучше вообще не плодить абстракции и остановиться на первой схеме из статьи. И не важно, в какой парадигме вы пишете. Если же вам важно разделить перекомпиляцию и перевыпуск компонентов контроллера и useCase'а, то вариант с абстракциями, как на второй схеме - это единственный способ.
А на счет ООП и ФП. Если в проекте есть полиморфизм, то для меня это уже ООП. Я не считаю, что классы == ООП. В конце-концов функция с замыканием нескольких переменных - это уже объект с одним методом. Если код организован на основе отправки сообщений, где отправитель не знает о внутреннем устройстве получателя - это ООП. Если код организован как конвейер по преобразованию данных - это ФП. Я выбираю парадигму исходя из задачи. Поэтому не могу поддержать вас в недолюбливании ООП, хотя и согласен, что с некоторыми задачами эта парадигма не справляется (как и шуруповерт не справляется с гвоздями)
Дополню ваш комментарий цитатой Роберта Мартина, на счет того, как не довести своё желание следовать принципам "до абсурда":
Теперь, когда вы знаете принципы SOLID, как вы будете их использовать? Это законы? Незыблемые правила? Или они скорее как... рекомендации? Каждый принцип решает конкретную проблему. Каждый принцип даёт вам контекст, чтобы вы смогли принять лучшее решение. И ваше решение не всегда будет в пользу принципов. Часто вы будете нарушать их. Но я хочу, чтобы вы знали, какой именно принцип вы нарушаете. И чтобы ваше решение нарушить этот принцип было осознанным.
По моему мнению, лучшие способ не доводить идею до абсурда, это понимать что лежит в основе этой идеи, и какую проблему она решает. И оставаться приземленным, используя идею как полезный инструмент, а не как истину в последней инстанции. В принципе, об этом "приземлении" отчасти и была эта статья.
Если под "бессмысленными абстракциями" вы подразумеваете структуру кода "после применения SOLID", то эти абстракции не мои. Я взял их из курса Роберта Мартина по SOLID. Вот кадр из урока (кусочек из статьи слева-сверху):
Чтобы и контроллеры и UseCase'ы соответствовали принципам OCP и DIP действительно нужно столько дополнительных классов. Можно заменить Factories для интерактора используя инъекцию зависимостей и передавать UseCaseActivator как параметр в Controller. Но сильно нашу схему это не упростит, и мой посыл остаётся актуален: "SOLID улучшает coupling, но ухудшает читабельность"
Вы абсолютно правы. Как говорил Эдсгера Дейкстра: "Цель абстракции состоит не в том, чтобы добавить неопределенности, а в том, чтобы создать новый семантический уровень, используя который мы можем быть абсолютно точны".
SOLID не работает с абстракциями, и не добавляет "новый семантический уровень". Как описывает их Роберт Мартин: "SOLID - это принципы менеджмента зависимостей".
Посыл статьи как раз в этом. В большинстве проектов хорошие абстракции, имена и транзакционная целостность (а все вместе - читабельность) важнее, чем менеджмент зависимостей.
Конечно.
Нужно помнить, что "программа - это набор автоматизированных процессов по обработке информации". Но объекты скрывают информацию/данные, потому что объект - это набор методов. Получается, чтобы эффективно использовать объекты - нужно найти такие варианты использования, при которых мы можем абстрагироваться от данных. Хотя бы на время жизни объекта.
Реализация машины состояний (State) через объекты помогает абстрагироваться от текущего состояния машины + её данных, и использовать общие для каждого состояния методы. Например мы составляем заказ. Он может быть в работе (корзина наполняется), а может быть оплачен. Пользователь инициировал useCase "Добавить товар в заказ". Мы вытащили из базы объект заказа и вызвали метод "добавить товар". Объект заказа знает о своём состоянии. Если он в состоянии наполнения - то он добавит товар и пересчитает сумму. Если он уже оплачен, то он выбросит исключение. Клиентский код не знает о состоянии объекта, и работает с ним как с целой машиной. Это разделяет "Что/Как" и "Что/Почему". Объекты дают возможность удобно реализовать такое поведение в коде.
На счёт сторонних сервисов. Представьте, что ваше приложение использует банковский сервис. Вы можете отправлять туда платежи и получать список платежей со статусами. Для доступа к этому сервису по сети нужно отправлять API-ключи + знать, на какой url стучать. Вы можете создать объект, который будет представлять в вашем приложении банковский сервис. Сделать в нём приватные поля с ключами и url. И написать туда 2 метода: "Отправить платёж" и "Получить платежи". Клиентскому коду не нужно знать о вещах, связанных с HTTP. Только методы. Это разделяет "Что/Как", а так же позволяет тестировать клиентский код без отправки сообщений на реальный банковский сервис, используя test doubles.
DDD-Агрегаты - это набор объектов из предметной области с единой границей транзакции. Тот же пример с заказом. Когда вы добавляете в заказ новый товар - должно произойти 2 действия: создаётся OrderItem с ссылкой на товар, и пересчитывается общая сумма заказа. Нельзя создать OrderItem, и не пересчитать сумму заказа. Это одна транзакция. Объекты позволяют объединить всё, что связано с заказом, в одно место и работать с несколькими структурами данных как с одной сущностью.
В общем случае можно сказать, что мы используем объекты, когда хотим работать с данными, не зная их внутреннего устройства. Это полезно на границах системы (клиентский код не хочет знать "как" отправлять в сторонние сервисы информацию, только "что" отправлять и "кому"), при реализации машин состояний (клиентский код не хочет знать текущее состояние машины, он хочет работать с ней как с единым целым) и при определении границ транзакций (работа с несколькими связанными структурами данных, которые должны изменяться одновременно).
Вижу пост про ООП - копирую стандартную портянку:
Единственный ООП язык - это SmallTalk
Объяснение ООП через собак, кошек и машины не имеет смысла
Программа - это набор автоматизированных процессов по обработке данных. Все парадигмы - это инструменты для реализации
В enterprice программах для разных задач одновременно используются все популярные парадигмы (Struct, FP, OOP)
Используйте объекты как границы транзакций (DDD-Агрегаты), инкапсуляцию машин состояний (паттерн State из GoF) или абстрагирование над устойчивыми понятиями (Инкапсуляция доступа к сторонним сервисам / Классы по типу Collection)
Справедливо. Тогда, согласно определению, будем считать Objective-C НЕ объектно-ориентированным 🤝
Поэтому и "немного" XD
Да, вы правы. По моему определению (принадлежащему Алану Кею XD) эти языки не являются ООП. Только SmallTalk и немного Objective-C. Это не значит, что они плохие. Просто нельзя из-за их проблем хоронить отличную парадигму.
Я не привёл код на SmallTalk, потому что
я сам плохо его знаюотличия в синтаксисе там не существенны, но могут запутать.На SmallTalk это выглядело бы так:
Если я правильно понял, то вас смутила внешняя схожесть двух конструкций. Но уверяю вас, что разница есть.
В случае
if result == true
это вы контролируете поток выполнения. Вы знаете, чтоresult == true
и чтоuseCase->execute()
точно вызовется. При чем вызовется только он.В случае
result->ifTrue(useCase->execute())
вы не контролируете поток выполнения. Вы сказали объектуresult
отправить объектуuseCase
сообщениеexecute
. Что он с ним сделает? Пошлёт ли он его вообще? Сделает ли он что-нибудь до или после отправки сообщения? Вы не знаете, потому что потеряли контроль над потоком выполнения.В функциональной парадигме для той же ситуации работает
pattern matching
:Здесь вы знаете все возможные варианты
result
и обязаны обработать каждый из них. Когда в структурной парадигме я могу не прописыватьelse
, а в объектной не вызыватьifElse
.Это простой пример, и разница кажется несущественной. Я привёл его, как способ быстро проверить, является ли код объектным или структурным. Но идеи, стоящие за каждой парадигмой, заставляют нас по разному думать о программе. И чем сложнее программа, тем сильнее будут проявляться эти различия.
Я думаю, что ваш комментарий поможет применить идеи из статьи при работе с Vue и случайно не разругаться с остальными разработчиками на проекте. Мой подход к написанию кода действительно отличается от принятых практик, потому что я привязываюсь к концепциям, а не к принятым в фреймворке реализациям. Что позволяет мне переключаться между фреймворками не замечая разницы. Это не всем подходит, так что спасибо за дополнение :)
Объясню некоторые выборы в статье, возможно вы взгляните на них под другим углом.
На счёт composable. Вы создали функцию, которая возвращает объект, в котором есть поля и процедуры, работающие с этими полями. То есть - вы создали класс (шаблон для создания объектов). Тут уже дело договорённостей. Если на проекте всю кастомную логику держат в composable, во Vue так принято и разработчикам так привычнее - конечно делайте через них. Суть от этого не изменится - это всё еще класс. Я же люблю для создания объектов использовать механизм, который для них предназначается. А функциям и процедурам отводить свою отдельную роль.
На счёт представления не очень понял. Внутри компонента
ProgressBar.vue
всё равно придётся указать тип данных пропса (в моём случаеProgress
). И он должен быть без методов (ProgressModel
не подходит), потому что это, как вы правильно сказали, dumb component. И на что он будет ссылаться, если нет разделения на данные и модель, мне не понятно. Но у вас компонентProgressBar
контролируется через модель - а это главное. Остальное - детали реализации, так что для меня это не принципиально.На счёт контроллеров. В статье я дал им определение: "Это класс объектов, которые подписываются на события от устройств ввода". В моей реализации компонент-родитель подписывается на события, так что относительно MVC для
Progress
- он контроллер. Да, он же является и mediator (в ваших терминах "smart component"). Но контроллером, относительно MVC дляprogress
, он от этого быть не перестаёт.Ещё раз спасибо за проявленный к статье интерес и полезное прикладное дополнение для разработчиков на Vue
Как я и написал в статье:
Однако обратите внимание. Концептуально это действительно единственный способ добиться той низкой связности, которую ставит своей задачей SOLID. При таком дизайне все компоненты защищены от изменений по "частоте", "важности компонентов" и "стоимости".
Но в ваших проектах, скорее всего, и такая сильная защита не нужна. А значит вы можете не делать его 100% SOLID. Об этом утверждении и была моя статья.
SOLID влияет на менеджмент зависимостей. DDD влияет на читабельность. Смысл статьи как раз и был в том, чтобы в небольших проектах сначала думать о читабельности, а уже потом о менеджменте зависимостей. А советы из DDD - это первое, о чем я думаю, говоря про читабельность (clean code уже по умолчанию)
Да, можно использовать DI. Мой ответ из другого комментария:
В данном примере предполагается, что
Factory
будет отдавать нам не толькоUseCaseActivator
, но иRequestBuilder
, так как их реализации используются одновременно. На схеме у автора это не показано (и у меня как следствие), что косяк. Но в видео-уроке это проговаривается в контексте.Так что
Factory
можно заменить на инъекциюUseCaseActivator
иRequestBuilder
. И все ключевые зависимости останутся без измененийОбратите внимание, что в вашем примере функция
handleRequest
ссылается наcreateRequestDTO
, а та в свою очередь ссылается наRequestDTO
. НоRequestDTO
используется функциейUseCase
. А значит единственный способ упаковки, который не вызовет цикличные зависимости будет такой, как на этой схеме:В таком случае любое изменение внутренней структуры
RequestDTO
потребует изменение как минимумcreateRequestDTO
, что означает перекомпиляцию и перевыпуск компонента контроллеров (того, что слева от двойных линий). А именно этого мы пытались избежать, добавляя абстракции, связанные с интерфейсом-маркером (красный блок на второй картинке в статье).Поэтому если вас не волнуют вопросы перекомпиляции и перевыпуска компонентов - лучше вообще не плодить абстракции и остановиться на первой схеме из статьи. И не важно, в какой парадигме вы пишете. Если же вам важно разделить перекомпиляцию и перевыпуск компонентов контроллера и useCase'а, то вариант с абстракциями, как на второй схеме - это единственный способ.
А на счет ООП и ФП. Если в проекте есть полиморфизм, то для меня это уже ООП. Я не считаю, что классы == ООП. В конце-концов функция с замыканием нескольких переменных - это уже объект с одним методом. Если код организован на основе отправки сообщений, где отправитель не знает о внутреннем устройстве получателя - это ООП. Если код организован как конвейер по преобразованию данных - это ФП. Я выбираю парадигму исходя из задачи. Поэтому не могу поддержать вас в недолюбливании ООП, хотя и согласен, что с некоторыми задачами эта парадигма не справляется (как и шуруповерт не справляется с гвоздями)
Дополню ваш комментарий цитатой Роберта Мартина, на счет того, как не довести своё желание следовать принципам "до абсурда":
По моему мнению, лучшие способ не доводить идею до абсурда, это понимать что лежит в основе этой идеи, и какую проблему она решает. И оставаться приземленным, используя идею как полезный инструмент, а не как истину в последней инстанции. В принципе, об этом "приземлении" отчасти и была эта статья.
Если под "бессмысленными абстракциями" вы подразумеваете структуру кода "после применения SOLID", то эти абстракции не мои. Я взял их из курса Роберта Мартина по SOLID. Вот кадр из урока (кусочек из статьи слева-сверху):
Чтобы и контроллеры и UseCase'ы соответствовали принципам OCP и DIP действительно нужно столько дополнительных классов. Можно заменить
Factories
для интерактора используя инъекцию зависимостей и передаватьUseCaseActivator
как параметр вController
. Но сильно нашу схему это не упростит, и мой посыл остаётся актуален: "SOLID улучшает coupling, но ухудшает читабельность"Вы абсолютно правы. Как говорил Эдсгера Дейкстра: "Цель абстракции состоит не в том, чтобы добавить неопределенности, а в том, чтобы создать новый семантический уровень, используя который мы можем быть абсолютно точны".
SOLID не работает с абстракциями, и не добавляет "новый семантический уровень". Как описывает их Роберт Мартин: "SOLID - это принципы менеджмента зависимостей".
Посыл статьи как раз в этом. В большинстве проектов хорошие абстракции, имена и транзакционная целостность (а все вместе - читабельность) важнее, чем менеджмент зависимостей.