Здравствуйте.

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

Вы, возможно, узнаете в данном подходе Flux или MVI, но я считаю, что это что-то другое. Почему - вы узнаете дальше.

На самом деле не важно как это всё называется, придумал ли я что-то новое или нет.

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

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

Так же весь код считайте псевдокодом.

План

  1. Предисловие

  2. Архитектура MVA

  3. Отличия от других архитектур

  4. Плюсы / Минусы

  5. Инструменты

  6. Вариант реализации

  7. Ссылки на примеры

  8. Итог

  9. Дополнение по обратной связи

Предисловие

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

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

  • Какие-то инструменты много весят, но дают большой функционал.

  • Какие-то дают вам удобство, но забирают производительность.

  • Какие-то ограничивают вас, но делают проект легко изменяемым.

  • Какие-то забирают больше времени, но делают так что проект не умрет через день.

  • итд.

И каждый инструмент нужен тогда когда нужен именно такой инструмент. Как бы это очевидно не звучало.

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

Сделать сложно - очень просто, но сделать просто - очень сложно.

Архитектура MVA

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

  1. Model - хранилище данных с самообновлением

  2. View - представление данных с самообновлением

  3. Action - самодостаточное действие

Визуальное представление архитектуры MVA
Визуальное представление архитектуры MVA

Что такое Effect - узнаем чуть дальше.

Сущности

Разберем каждую сущность по отдельности и определим их задачи.

Model

Вся задача Model сводится к тому, чтобы подписаться на действия и самообновляться.

То есть никто кроме самой Model не может обновить её или вызвать какое-то обновление.

Например:

// При вызове действия getPosts - состояние само обновится на true, а при его окончании на false
const $postsIsPending = store<boolean>(false)    
  .on(getPosts, 'onBefore', () => true)    
  .on(getPosts, 'onFinally', () => false);

View

Задача View сводится к рендеру данных и вызову действий.

Для удобства так же разделяем View на два типа компонентов одни из которых (shared, entity) ни от чего не зависят, а вторые (feature, widget) уже и подписываются на Model и вызывают Effect.

Пример виджета:

import { FC, memo } from 'react';


export const Posts: FC = memo(function Posts () {
    const postsIsPending = useStore($postsIsPending);
    const postsList      = useStore($postsList);

    return (
        <section>
            <h2>Posts</h2>
            {
                postsIsPending ? <Loader/> : <PostsList list={ postsList }/>
            }
        </section>
    );
});

Action

Что такое действия? Действия могут быть любой функцией. Это может быть:

  • Функция входа в аккаунт signIn (login: string, password: string): Promise<AuthData>

  • Функция загрузки постов getPosts (userId: string, options: SearchOptions): Promise<Array<Post>>

  • Функция, чтобы оставить комментарий sendCommentary (postId: string, commentary: string): Promise<Commentary>

  • итд.

Например:

const getPosts = function (userId: string): Promise<Array<Post>> {
    return fetch(`${ __API__ }/v1/posts/user/${ userId }`, { method: 'GET' })
        .then((response) => response.json());
};

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

Связи

Теперь разберем как связаны эти сущности друг с другом.

View -> Model

View подписывается на Model, отслеживает изменения и самообновляется.

В примере выше это было сделано через хук useStore, а самообновление происходит самим условным React

Model -> Effect

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

Допустим у нас есть Action getUser (userId: string): Promise<User>. Это действие мы можем хотеть использовать во многих местах нашего приложения. Например, для получения данных для рендера главной страницы пользователя или же для получения данных для показа их в выпадающем окне при наведении итд. И для этих задач у нас будут созданы две разные модели, одна для главной страницы, одна для выпадающего окна. Но обе они будут подписаны на одно и тоже. Получается, что при вызове этого действия - обновляться будут обе модели, а мы, скорее всего, этого не хотим.

По этому мы создаем Effect-ы (обертку над Action)

const getUserPageData    = effect(getUser);
const getUserPopOverData = effect(getUser);

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

Как раз таки на этот Effect мы и подписывались в примере в Model выше.

// При вызове действия getPosts - состояние само обновится на true, а при его окончании на false
const $postsIsPending = store<boolean>(false)
    .on(getPosts, 'onBefore', () => true)
    .on(getPosts, 'onFinally', () => false);

Effect -> Action

Это собственно обертка над Action которая служит для того о чем мы говорили до.

const getUserPageData    = effect(getUser);
const getUserPopOverData = effect(getUser);

Отличия от других архитектур

Теперь разберем отличия от Flux, MVI и допустим MVC.

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

Flux

С Flux вообще интересно, потому что они вроде бы похожи, но вообще разные. И там и там однонаправленный поток данных (в чем похожи), но реализация архитектуры - разная. Основное отличие от Flux это отсутствие Dispatcher.

Тут, для меня, действительно сложно, потому что в зависимости от того как посмотреть - она может быть то похожа, то совсем другой. Но главным аргументом для меня в пользу того, что они разные - является то, что если попробовать перенести то как делаю я в MVA на Flux - получается всё гораздо сложнее и не понятно зачем.

Тут нужно попробовать применить этот подход и самому почувствовать разницу.

MVI

С MVI достаточно просто. Там я нашел 2 варианта трактовки и оба они отличаются от MVA.

  1. Действия (Intent) в MVI сами обновляют модель. Модель не подписывается на действия.

  2. Действия (Intent) в MVI сами отправляют данные в модель которые та уже сама как-то обновляет. Модель не подписывается на действия.

В MVA только модель может себя обновить и обновление вызывается по вызову какого-то Effect.

MVC

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

В MVC всем рулит Controller. Он является связью между View и Model. Опять же, можно всё трактовать по-разному. Где-то вы прочтете, что Controller контролирует только Model, а View на его подписывается, кто-то допускает связь между View и Model на прямую итд.

В MVA никто никем не рулит.

  • View напрямую подписывается на Model и самообновляется.

  • Model подписывается на Effect и самообновляется.

  • Action просто существуют.

Плюсы / Минусы

Ps. Тут приведет не весь список, а только важные моменты.

Плюсы

Основной плюс подхода - его простота.

  1. Благодаря простой структуре можно легко понимать устройство всего приложения целиком вне зависимости от его размера.

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

  3. Вы почти не пишите лишнего кода, кроме создания эффектов, подписок, но это зависит от реализации. (effect, on)

  4. Разделение кода позволяет разрабатывать и тестировать части приложения отдельно друг от друга. (кроме того момента, что для подписок Model нужно знать сигнатуры Action, но их можно определить заранее).

Минусы

Буквально не обнаружено ни одного.

Инструменты

Я не просто так писал в предисловии как отношусь к инструментам и что всё есть инструмент.

У нас есть задача реализовать данную архитектуру, что нам нужно?

  1. View должен самообновляться по подписке на Model

  2. Model должен самообновляться по подписке на Effect

  3. У нас должна быть возможность создавать Effect на основе Action

А теперь начинаем искать готовые решения. (это разумеется не все возможные, а просто несколько примеров).

Model

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

  1. Redux + Thunk-и

  2. Effector

Redux + Thunk

Да, мы создаем Thunk-и.

Например:

export const getPostsThunk = createAsyncThunk<Array<Post>, string, ThunkApi>(
    'posts/getPosts',
    async (userId, thunkApi) => {
        try {
            return await getPosts(userId);
        } catch (e: unknown) {
            return thunkAPI.rejectWithValue(e);
        }
    },
);

А дальше подписываемся через extraReducers в слайсах.

const initialState: PostsSchema = {
    isPending: false,
    error    : null,
    list     : [],
};

export const postsSlice = createSlice({
    name         : 'posts',
    initialState : initialState,
    reducers     : {},
    extraReducers: (builder) => {
        builder.addCase(getPostsThunk.fulfilled, (state, action) => {
            state.isPending = false;
            state.error     = null;
            state.list      = action.payload;
        });
        builder.addCase(getPostsThunk.pending, (state) => {
            state.isPending = true;
            state.error     = null;
        });
        builder.addCase(getPostsThunk.rejected, (state, action) => {
            state.isPending = false;
            state.error     = action.payload ?? {
                code   : 500,
                message: 'Unknown error',
            };
            state.list      = [];
        });
    },
});

Мы получили то что и хотели? Получили.

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

Effector

Тут всё просто. В Effector есть всё что нам нужно.

Создаем Model, создаем Effect и готово

const getPostsEffect = createEffect(getPosts);

const $postsIsPending = createStore(false)
    .on(getPostsEffect, () => true)
    .on(getPostsEffect.finally, () => false);

const $postsError = createStore<Error>(null)
    .on(getPostsEffect.fail, ({ error }) => error);

const $postsList = createStore<Array<Post>>([])
    .on(getPostsEffect.done, ({ result }) => result);

Да, классно. Effector добавляет всего 4 кб (в gzip) к бандлу. Но и как в случае в Redux, он не нужен нам целиком, мы не будем использовать всё что в нём есть. Не подходит.


Какие еще есть варианты? Да просто пишем свой стор с тремя функциями (store, effect и combine) и всё. На сколько будет "умной" реализация вы уже сами для себя решаете. И вы добавите туда столько сколько именно вам для вашей задачи нужно. Я написал свою реализацию и на ней написал "социальную сеть". Всё отлично работает и всего ~150 строчек вместе с типизацией.

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

Это всё равно что добавить axios в проект и просто делать им запросы как fetch-ем.

View

Давайте так же поговорим и про то что уже есть готовое для View. Возьмем несколько вариантов:

  1. React

  2. SolidJS

  3. Svelte

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

Например, React добавляет к бандлу 40 кб (в gzip), а SolidJS всего 6 кб (в gzip), а Svelte так еще меньше. Тогда почему React?

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


Допустим мы выбрали:

  • React (40 kb gzip)

  • Redux (15 kb gzip)

  • Axios (15 kb gzip)

Мы еще не написали ни строчки кода, приложение уже весит 70 кб в gzip.

А допустим мы выбрали:

  • SolidJS (6 kb gzip)

  • Свою реализацию стора (500 b)

  • Свою реализацию апишки для запросов (500 b)

В итоге мы имеем 7 кб в gzip. В 10 раз меньше. Я об этом.

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

Я ничего не рекламирую и не агитирую за какое-то решение, я просто делюсь размышлениями.

Вариант реализации

Тут я просто опишу то как я бы сейчас реализовал MVA.

Структура папок:

/src
   /action
      /[принадлежность]
         /[имя]
            name.action.ts
   /model
      /[принадлежность]
         /[имя]
            name.model.ts
   /ui
      /[тип компонента (shared, entity, feature, widget, page, layout, etc)]
         /[принадлежность]
            /[Имя]
               /[ui]
                  Component.module.css
                  Component.tsx(.svelte, .etc)

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

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

Ссылки на примеры

Примерно 2 месяца назад я открыл для себя такой подход и написал о нем статью. Тогда еще не было чего-то большого написано полностью с таким подходом, теперь есть.

Все ссылки ведут на github.

  1. Переписанная полностью на MVA "Социальная сеть"

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

Вы можете посмотреть сами как это можно реализовать (как это реализовал я) и что это такое.

  • Модели лежат в /src/app/model

  • Вьюшки в /src/[type]

  • Действия в /src/app/action

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

  1. Частично переписанный этот же проект на Svelte и Effector, сразу с использованием MVA

  2. Тудушка (React, своя реализация стора)

  3. Тудушка (Svelte, Effector)

  4. Тудушка (React, Effector)

  5. Тудушка (SolidJS, своя реализация стора)

Итог

Я искренне считаю, что это очень крутая архитектура. Очень простая.

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

Вам никто и ничто не мешает использовать этот подход вместе с другими.

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

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

Дополнение

К статье я оставил голосование и в нем несколько человек указало "непонятно зачем это". Постараюсь объяснить еще пару моментов.

1. Независимые ни от чего Action. То есть вы можете использовать Action где угодно и как угодно, потому что всё что они делают - ни на что само по себе не влияет. Например: подгружают посты, комментарии, вход в аккаунт, начало звонка итд. Благодаря этому вы получаете дополнительную гибкость.

2. Все Model изменяются по подпискам на Effect, все View изменяются по подпискам на Model. В итоге вы просто "говорите" приложению "загрузи посты для пользователя 123" и все Model подписанные на этот Effect сами обновятся так как вы указали. Вам не нужно думать о том, чтобы обновлять какие-то состояния вручную или вызывать множество методов.

Тут вы можете мне предложить другую архитектуру, где мы, например, создаем класс, в классе у нас будут свойства на которые мы можем подписаться из View и методы для работы, которые мы будем дергать. Казалось бы, вместо, условного вызова getPostsForUser(123) мы будем писать posts.getPostsForUser(123), и всё так же будет обновляться само, где-то там. Да, всё так. Но, а теперь мы захотели очистить список постов при logout. Тогда нам из logout дергать методы posts? Или через .then дергать методы posts? Или создавать отдельную функцию куда добавлять все методы всех классов которые нужно дергать при вызове logout? И таких случаев может быть много. Потом мы захотим добавить loader который будет показывать состояние любых запросов (такая полоска вверху сайта). И как быть тут?

В MVA вы просто в каждой модели, которую нужно сбросить, подпишите на logout Effect и будете очищать её. Всё. Вызвали logout - все кто нужно было - очистились. Если нужно создать loader - пишите модель, подписываете её на все Effect которые нужно отслеживать, всё работает. Те Effect-ы остаются без изменения, они ничего не заметили. Action-ы вы не меняете. Всё осталось как прежде, но теперь от них зависит еще и loader.

Из-за чего можно сделать еще один вывод, что добавление новых моделей не будут заставлять вас изменять старый код. Например: добавив еще одну модель для "звонков", вы можете подписать её на logout так же и всё. Вам не нужно будет лезть в старый код и добавлять туда новый метод для очищения calls.


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

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


Если у вас есть замечания / предложения / критика / вопросы и что угодно другое - то пишите в комментариях или в телегу @VanyaMate

Спасибо за внимание)

Комментарии (8)


  1. luckoff
    27.08.2024 14:42

    Интересный подход, но как будто бы он повторяет основной принцип MVVM (Model-View-ViewModel). Поправьте если это не так)

    Но задумка в целом интересная, но я не могу ее представить в других фреймворках, том же Vue или Angular. Архитектура в этом плане узковатая, хотелось бы еще увидеть примеры не только в React


    1. VanyaMate Автор
      27.08.2024 14:42
      +2

      Здравствуйте.

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

      Реализация на MVVM будет похоже на реализацию через MobX, где мы создаем класс, подписываемся на его изменения из View, даем ему методы которые эта же View дергает.

      Да, мы так же можем вынести Action отдельно, да, в каком-то смысле мы так же вызываем только действия, а дальше оно всё само происходит, но из-за отсутствия эффектов нам придется вручную вызывать некоторые Action из других Action, потому что MVVM не предполагает, как я понимаю, подписок на них. Хотя мы конечно можем создать что-то похоже.. опять же как посмотреть.. но.. это прям гораздо сложнее..

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

      В итоге из любого участка приложения будь то UI, уведомления, какое-то броадкаст сообщение, что угодно, мы вызываем действие getPosts - наша Model сама по подписке ставит isLoading = true, View рендерит <Loader/>. Действие закончилось успешно - наша Model сама по подписке ставит isLoading = false, posts = result, View рендерит список постов.

      А всё что мы сделали - просто вызвали какой-то Effect.

      Хороший пример можно привести с logoutEffect из "Социальной сети".

      logoutEffect это обертка над logout запросом на сервер и очисткой localStorage.

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

      я просто всеми ими подписываютсь на этот logoutEffect и они все очищаются.

      Вот вам один пример

      ----

      Теперь про Vue и Angular.

      Во Vue это точно можно сделать. К сожалению я не знаю Vue, но вы точно так же как и во всех примерах создаете Action и Model. Их можно буквально просто скопировать в проект и всё будет работать так же. А дальше, тут я уже не знаю как, потому что это особенности Vue, подписываетесь на изменения стора и точно так же из любого участка кода вызываете Effect-ы. У Effector есть пакет effector-vue . Вот как-то так. Но, думаю, как и в React мы можем просто создать такой же хук для работы и с самописным стором.

      С Angular всё сложно, потому что сделать так же просто как в React, Svelte, Solid, Vue - не выйдет. Там другая вообще архитектура приложений. Опять же, Angular я не знаю на таком уровне, чтобы прям точно дать ответ как это там можно сделать, но думаю как-то можно, но не факт, что это будет удобнее чем какие-то другие варианты которые используются и реализуются в Angular. Всё таки MVA это тоже инструмент и он не всегда подходит.


  1. markelov69
    27.08.2024 14:42
    +2

    Я искренне считаю, что это очень крутая архитектура. Очень простая.

    Использовать Effector и Redux в 2024 году. С такими инструментами она априори не может быть крутой и простой. Открою вам страшную тайну, в связке с React'ом можно и нужно использовать только MobX, тут без альтернативно, разве что только самописный аналог.

    Вот это я понимаю реально супер просто и круто:
    https://stackblitz.com/edit/vitejs-vite-y2qj7g?file=src%2FApp.tsx&terminal=dev


    1. VanyaMate Автор
      27.08.2024 14:42

      Здравствуйте.

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

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

      Казалось бы, мы просто создаем классы с данными, методы для работы и всё круто. Никаких хуков для работы с этим не нужно, просто экспортируем в компоненты и радуемся жизни.

      makeAutoObservable, observer и всё работает.

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

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

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

      Создаете отдельно Action. Например получение списка постов. Это просто запрос на сервер, валидация данных их возвращение.

      Создаете парочку Model. postsIsPending и postsList, например.

      Создаете Effect обертку над Action, подписываетесь на неё моделями и всё.

      Дальше просто рендерите эти данные.

      ----

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

      Я просто делюсь своими идеями.

      Я да, я так же как и с Redux, Effector, считаю что и MobX не нужен) Просто на нем вроде как нельзя реализовать "просто" то о чем писал я, по этому я о нем и не писал.


      1. markelov69
        27.08.2024 14:42

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

        Создаете отдельно Action. Например получение списка постов. Это просто запрос на сервер, валидация данных их возвращение.

        Создаете парочку Model. postsIsPending и postsList, например.

        Создаете Effect обертку над Action, подписываетесь на неё моделями и всё.

        Дальше просто рендерите эти данные.

        Вот вариант с постами
        https://stackblitz.com/edit/vitejs-vite-3ajbxs?file=src%2Fpages%2Fmain%2Findex.tsx,src%2FglobalState%2Fposts%2Findex.ts&terminal=dev
        Там всё реально ультра просто и очевидно:

        Нажать сюда
        Нажать сюда

        Минимум лишних файлов, папок и кода, и главное всё предельно наглядно и очевидно.


  1. Ivan_I
    27.08.2024 14:42

    Я так понимаю эта архитектура тяготеет к функциональному подходу? Совместима ли она с SOLID принципами? Или SOLID здесь не уместен?


    1. VanyaMate Автор
      27.08.2024 14:42

      Здравствуйте.

      Архитектура не диктует то с помощью каких инструментов вам нужно реализовать её.

      Архитектура закладывает принципы, структуру, но как вы это реализуете - это уже решаете вы.

      Если вы хотите использовать ООП, применять принципы SOLID для реализации, пожалуйста, вы это можете сделать.


      1. Ivan_I
        27.08.2024 14:42

        Хорошо. Понятно.