Чистая Архитектура для веб-приложений

    Хочу поделиться с вами подходом который я уже много лет использую в разработке приложений, в том числе и веб-приложений. Многим разработчикам настольных, серверных и мобильных приложений этот подход хорошо знаком, т.к. является фундаментальным при построении таких приложений, однако в вебе он представлен очень скудно, хотя желающие использовать такой подход однозначно есть. Кроме того на таком подходе написан редактор VS Code.

    Чистая Архитектура

    В результате применения этого подхода вы отвяжетесь от конкретного фреймворка. Сможете легко переключать библиотеку представления внутри вашего приложения, например React, Preact, Vue, Mithril без переписывания бизнес логики, а в большинстве случаев даже вьюхи. Если у вас есть приложение на Angular 1, вы без проблем сможете перевести его на Angular 2+, React, Svelte, WebComponents или даже свою библиотеку представления. Если у вас есть приложение на Angular 2+, но нету специалистов для него, то вы без проблем сможете перевести приложение на более популярную библиотеку без переписывания бизнес логики. А в итоге вообще забыть про проблему миграции с фремворка на фреймворк. Что же это за магия такая?

    Что такое Чистая Архитектура


    Для того что бы понять это, лучше всего прочитать книгу Мартина Роберта «Чистая Архитектура» (Robert C.Martin «Clean Architecture»). Краткая выдержка из которого приведена в статье по ссылке.

    Основные идеи заложенные в архитектуру:

    1. Независимость от фреймворка. Архитектура не зависит от существования какой-либо библиотеки. Это позволяет использовать фреймворк в качестве инструмента, вместо того, чтобы втискивать свою систему в рамки его ограничений.
    2. Тестируемость. Бизнес-правила могут быть протестированы без пользовательского интерфейса, базы данных, веб-сервера или любого другого внешнего компонента.
    3. Независимоcть от UI. Пользовательский интерфейс можно легко изменить, не изменяя остальную систему. Например, веб-интерфейс может быть заменен на консольный, без изменения бизнес-правил.
    4. Независимоcть от базы данных. Вы можете поменять Oracle или SQL Server на MongoDB, BigTable, CouchDB или что-то еще. Ваши бизнес-правила не связаны с базой данных.
    5. Независимость от какого-либо внешнего сервиса. По факту ваши бизнес правила просто ничего не знают о внешнем мире.

    Идеи описанные в этой книге уже много лет являются основой для построения сложных приложений в самых разных сферах.

    Достигается такая гибкость за счет разделения приложения на слои Service, Repository, Model. Я же добавил к Чистой Архитектуре подход MVC и получил следующие слои:

    • View — выводит данные клиенту, фактически визуализирует состояние логики клиенту.
    • Controller — отвечает за взаимодействие с пользователем посредством IO (ввод-вывод).
    • Service — отвечает за бизнес логику и ее переиспользование между компонентами.
    • Repository — отвечает за получение данных из внешних источников, такие как база данных, api, локальное хранилище и пр.
    • Models — отвечает за перенос данных между слоями и системами, а также за логику обработки этих данных.

    Назначение каждого слоя рассмотрим ниже.

    Кому подойдет Чистая Архитектура


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

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

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

    Где уже применяется?


    Чистая архитектура не привязана к какому то конкретному фреймворку, платформе или языку программирования. Десятилетия ее используют для написания настольных приложений. Его эталонную реализацию можно найти во фреймворках для серверных приложений Asp.Net Core, Java Spring и NestJS. Так же она очень популярна при написании iOs и Android приложений. Но в веб разработке он предстал в крайне неудачном виде во фреймворках Angular.

    Так как я сам не только Typescript, но и C# разработчик, то для примера возьму эталонную реализацию этой архитектуры для Asp.Net Core.

    Вот упрощенный пример приложения:

    Пример приложения на Asp.Net Core
        /**
         * View
         */
    
        @model WebApplication1.Controllers.Profile
    
        <div class="text-center">
            <h1>Добро пожаловать @Model.FirstName</h1>
        </div>
    
        /**
         * Controller
         */
    
        public class IndexController : Controller
        {
            private static int _counter = 0;
            private readonly IUserProfileService _userProfileService;
    
            public IndexController(IUserProfileService userProfileService)
            {
                _userProfileService = userProfileService;
            }
    
            public async Task<IActionResult> Index()
            {
                var profile = await this._userProfileService.GetProfile(_counter);
                return View("Index", profile);
            }
    
            public async Task<IActionResult> AddCounter()
            {
                _counter += 1;
                var profile = await this._userProfileService.GetProfile(_counter);
                return View("Index", profile);
            }
        }
    
        /**
         * Service
         */
    
        public interface IUserProfileService
        {
            Task<Profile> GetProfile(long id);
        }
    
        public class UserProfileService : IUserProfileService
        {
            private readonly IUserProfileRepository _userProfileRepository;
    
            public UserProfileService(IUserProfileRepository userProfileRepository)
            {
                this._userProfileRepository = userProfileRepository;
            }
    
            public async Task<Profile> GetProfile(long id)
            {
                return await this._userProfileRepository.GetProfile(id);
            }
        }
    
        /**
         * Repository
         */
    
        public interface IUserProfileRepository
        {
            Task<Profile> GetProfile(long id);
        }
    
        public class UserProfileRepository : IUserProfileRepository
        {
            private readonly DBContext _dbContext;
            public UserProfileRepository(DBContext dbContext)
            {
                this._dbContext = dbContext;
            }
    
            public async Task<Profile> GetProfile(long id)
            {
                return await this._dbContext
                    .Set<Profile>()
                    .FirstOrDefaultAsync((entity) => entity.Id.Equals(id));
            }
        }
    
        /**
         * Model
         */
    
        public class Profile
        {
            public long Id { get; set; }
            public string FirstName { get; set; }
            public string Birthdate { get; set; }
        }
    


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

    Пример приведен для Asp.Net Core приложения, но для Java Spring, WinForms, Android, React архитектура и код будут такие же, меняется только язык и работа с вьюхой (если она есть).

    Применение в веб-приложениях


    Единственный фреймворк который пытался использовать Чистую архитектуру был Angular. Но получилось это просто ужасно, что в 1, что в 2+.

    И причин для этого много:

    1. Angular монолитный фреймворк. И это его основная проблема. Если тебе в нем что то не нравится, ты вынужден давиться этим ежедневно, и ничего с этим не поделать. Мало того что в нем масса проблемных мест, так это еще и противоречит идеологии чистой архитектуры.
    2. Ужасная адаптация патерна DI. Его просто перенесли как есть, без учета особенностей веб приложений и игнорируя модульную систему импортов современного Javascript.
    3. Ужасный движок представлений. Он очень примитивный и сильно уступает простоте JSX. Данные не типизируются на этапе написания кода, а на этапе компиляции научился отлавливать ошибки только в версии 6, а до этого только в рантайме. А прокинуть в компонент два шаблона и получить контент прокинутого и прокидывающего контроллера из разряда фантастики.
    4. Старый бандлер. В то время как бандлер Rollup позволял собирать ES2015 и делать 2 бандла для старых и новых браузеров уже 4 года, то сборщик angular научился это делать только в версии 9.
    5. И еще много проблем. В целом до ангуляр современные технологии докатываются с задержкой лет в 5 относительно React.

    Но что же другие фреймворки? React, Vue, Preact, Mithril и прочие являются исключительно библиотеками представления и не предоставляют никакой архитектуры… а архитектура у нас уже есть… осталось собрать всё в единое целое!

    Начинаем создавать приложение


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

    Прототип приложения

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

    Паттерн Controller


    Controller — отвечает за взаимодействие пользователя с приложением. Это может быть клик по кнопке на веб странице, настольном приложении, мобильном приложении, или ввод команды в консоли линукса, или сетевой запрос, или любое другое IO событие приходящее в приложение.

    Самый простой контроллер в чистой архитектуре выглядит следующим образом:

    export class SimpleController { // extends React.Component<object, object>
    
        public todos: string[] = []; // состояние контроллера
    
        public addTodo(todo: string): void { // вызывает событие от пользователя
            this.todos.push(todo);
        }
    
        public removeTodo(index: number): void { // вызывает событие от пользователя
            this.todos.splice(index, 1);
        }
    
        // public render(): JSX.Element {...} // view injection
    
    }
    

    Его задача получить от пользователя событие и запустить бизнес процессы. В идеальном случае Controller ничего не знает про View, и тогда его можно переиспользовать между платформами, например Web, React-Native или Electron.

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

    UserPageController. Контроллер с бизнес логикой
    export class UserPageControlle {
    
        public userProfile: any = {};
        public insuranceCases: any[] = [];
        public tariffs: any[] = [];
        public bestTariff: any = {};
    
        constructor() {
            this.activate();
        }
    
        public activate(): void {
            this.requestUserProfile();
            this.requestTariffs();
        }
    
        public async requestUserProfile(): Promise<void> { // получение данных
            try {
                const response = await fetch("./api/user-profile");
                this.userProfile = await response.json();
                this.findBestTariff();
            } catch (e) {
                console.error(e);
            }
        }
    
        public async requestTariffs(): Promise<void> { // получение данных
            try {
                const response = await fetch("./api/tariffs");
                this.tariffs = await response.json();
                this.findBestTariff();
            } catch (e) {
                console.error(e);
            }
        }
    
        public findBestTariff(): void { // метод с бизнес логикой
            if (this.userProfile && this.tariffs) {
                this.bestTariff = this.tariffs.find((tarif: any) => {
                    return tarif.ageFrom <= this.userProfile.age && this.userProfile.age < tarif.ageTo;
                });
            }
        }
    
        /**
         * ... множество других методов, запрос страховых случаев,
         * редактирование профиля, выбор тарифа и прочее
         */
    }
    


    У нас получился обычной контроллер без чистой архитектуры, если отнаследовать его от React.Component то получим рабочий компонент с логикой. Так пишут очень много разработчиков веб-приложений, но у такого подхода есть много существенных недостатков. Главный из которых невозможность переиспользования логики между компонентами. Ведь рекомендуемый тариф может выводиться не только в личном кабинете, но и на лендинге и множестве других мест для привлечения клиента к услуге.

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

    Паттерн Service


    Service — отвечает за всю бизнес логику приложения. Если Controller'у понадобилось получить, обработать, отправить какие то данные — он делает это через Service. Если нескольким контроллерам понадобилась одна и та же логика, они работают с Service. Но сам слой Service ничего не должен знать о слое Controller и View и окружении в котором он работает.

    Давайте вынесем логику из контроллера в сервис и внедрим сервис в контроллер:

    UserPageController. Контроллер без бизнес логики
    import { UserProfilService } from "./UserProfilService";
    import { TariffService } from "./TariffService";
    
    export class UserPageController {
    
        public userProfile: any = {};
        public insuranceCases: any[] = [];
        public tariffs: any[] = [];
        public bestTariff: any = {};
    
        // внедряем сервисы в контроллер
        private readonly userProfilService: UserProfilService = new UserProfilService();
        private readonly tarifService: TariffService = new TariffService();
    
        constructor() {
            this.activate();
        }
    
        public activate(): void {
            this.requestUserProfile();
            this.requestTariffs();
        }
    
        public async requestUserProfile(): Promise<void> {
            try {
                // используем сервисы для получения данных
                this.userProfile = await this.userProfilService.getUserProfile();
                this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            } catch (e) {
                console.error(e);
            }
        }
    
        public async requestTariffs(): Promise<void> {
            try {
                // используем сервис для получения данных
                this.tariffs = await this.tarifService.getTariffs();
            } catch (e) {
                console.error(e);
            }
        }
    
        /**
         * ... множество других методов, запрос страховых случаев,
         * редактирование профиля, выбор тарифа и прочее
         */
    }
    

    UserProfilService. Сервис для работы с профилем пользователя
    export class UserProfilService {
        public async getUserProfile(): Promise<any> { // получение данных
            const response = await fetch("./api/user-profile");
            return await response.json();
        }
        
        /**
         * ... множество других методов для работы с профилем пользователя
         */
    }
    

    TariffService. Сервис для работы с тарифами
    export class TariffService {
        // получение данных
        public async getTariffs(): Promise<any> {
            const response = await fetch("./api/tariffs");
            return await response.json();
        }
    
        // метод с бизнес логикой
        public async findBestTariff(userProfile: any): Promise<any> {
            const tariffs = await this.getTariffs();
            return tariffs.find((tarif: any) => {
                return tarif.ageFrom <= userProfile.age &&
                    userProfile.age < tarif.ageTo;
            });
        }
        
        /**
         * ... множество других методов для работы с тарифами
         */
    }
    


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

    Но что делать если источник данных поменяется, например fetch может смениться на websocket или grps или базу данных, а реальные данные понадобиться заменить тестовыми? И вообще зачем бизнес логике что то знать о источнике данных? Для решениях этих проблем существует слой Repository.

    Паттерн Repository


    Repository — отвечает за общение с хранилищем данных. В качестве хранилища может выступать сервер, база данных, память, localstorage, sessionstorage или любое другое хранилище. Его задача абстрагировать слой Service от конкретной реализации хранилища.

    Давайте вынесем сетевые запросы из сервисов в репозитории, контроллер при этом не меняем:
    UserProfilService. Сервис для работы с профилем пользователя
    import { UserProfilRepository } from "./UserProfilRepository";
    
    export class UserProfilService {
    
        private readonly userProfilRepository: UserProfilRepository =
            new UserProfilRepository();
    
        public async getUserProfile(): Promise<any> {
            return await this.userProfilRepository.getUserProfile();
        }
        
        /**
         * ... множество других методов для работы с профилем пользователя
         */
    }
    

    UserProfilRepository. Сервис для работы с хранилищем профилей пользователя
    export class UserProfilRepository {
        public async getUserProfile(): Promise<any> { // получение данных
            const response = await fetch("./api/user-profile");
            return await response.json();
        }
        
        /**
         * ... множество других методов для работы с профилем пользователя
         */
    }
    

    TariffService. Сервис для работы с тарифами
    import { TariffRepository } from "./TariffRepository";
    
    export class TariffService {
        
        private readonly tarifRepository: TariffRepository = new TariffRepository();
    
        public async getTariffs(): Promise<any> {
            return await this.tarifRepository.getTariffs();
        }
    
        // метод с бизнес логикой
        public async findBestTariff(userProfile: any): Promise<any> {
            // запрашиваем у источника данных
            const tariffs = await this.tarifRepository.getTariffs();
            return tariffs.find((tarif: any) => {
                return tarif.ageFrom <= userProfile.age &&
                    userProfile.age < tarif.ageTo;
            });
        }
        
        /**
         * ... множество других методов для работы с тарифами
         */
    }
    

    TariffRepository. Репозиторий для работы с хранилищем тарифов
    export class TariffRepository {
        // получение данных
        public async getTariffs(): Promise<any> {
            const response = await fetch("./api/tariffs");
            return await response.json();
        }
    
        /**
         * ... множество других методов для работы с хранилищем тарифов
         */
    }
    


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

    В сервисе UserProfilService может показаться что он не нужен и контроллер может напрямую обратиться к репозиторию за данными, но это не так. В любой момент в бизнес слое могут появиться или измениться требования, может потребоваться дополнительный запрос или обогатить данные. Поэтому даже когда в слое сервиса нету логики цепочка Controller — Service — Repository должна сохраняться. Это вклад в ваше завтра.

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

    Модели: DTO, Entities, ViewModels


    Models — отвечает за описание структур с которыми работает приложение. Такое описание очень помогает новым разработчикам проекта понять с чем работает приложение. Кроме того его очень удобно использовать для построения баз данных или проведений валидаций данных хранящихся в модели.

    Модели в зависимости от типа использования делятся на разные паттерны:
    • Entities — отвечают за работу с базой данных и представляют из себя структуру повторяющую таблицу или документ в базе данных.
    • DTO (Data Transfer Object) — служат для переноса данных между разными слоями приложения.
    • ViewModel — содержат заранее подготовленную информацию необходимую для отображении в представлении.


    Добавим в приложение модель профиля пользователя и другие модели, и сообщим остальным слоям что теперь мы работаем не с абстрактным объектом, а с вполне конкретным профилем:
    UserPageController. Вместо типа any используются описанные модели
    import { UserProfilService } from "./UserProfilService";
    import { TariffService } from "./TariffService";
    import { UserProfileDto } from "./UserProfileDto";
    import { TariffDto } from "./TariffDto";
    import { InsuranceCaseDto } from "./InsuranceCasesDto";
    
    export class UserPageController {
    
        /**
         * Используем модель для типа и пустую модель для первой отрисовки
         * как заглушку до тех пока данные из сервиса не придут.
         */
        public userProfile: UserProfileDto = new UserProfileDto();
        public insuranceCases: InsuranceCaseDto[] = [];
        public tariffs: TariffDto[] = [];
        public bestTariff: TariffDto | void = void 0;
    
        private readonly userProfilService: UserProfilService = new UserProfilService();
        private readonly tarifService: TariffService = new TariffService();
    
        constructor() {
            this.activate();
        }
    
        public activate(): void {
            this.requestUserProfile();
            this.requestTariffs();
        }
    
        public async requestUserProfile(): Promise<void> {
            try {
                this.userProfile = await this.userProfilService.getUserProfile();
                this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            } catch (e) {
                console.error(e);
            }
        }
    
        public async requestTariffs(): Promise<void> {
            try {
                this.tariffs = await this.tarifService.getTariffs();
            } catch (e) {
                console.error(e);
            }
        }
    
        /**
         * ... множество других методов, запрос страховых случаев,
         * редактирование профиля, выбор тарифа и прочее
         */
    }
    

    UserProfilService. Вместо any указываем возвращаемую модель
    import { UserProfilRepository } from "./UserProfilRepository";
    import { UserProfileDto } from "./UserProfileDto";
    
    export class UserProfilService {
    
        private readonly userProfilRepository: UserProfilRepository =
            new UserProfilRepository();
    
        public async getUserProfile(): Promise<UserProfileDto> { // возвращаем модель
            return await this.userProfilRepository.getUserProfile();
        }
        
        /**
         * ... множество других методов для работы с профилем пользователя
         */
    }
    

    TariffService. Вместо any указываем возвращаемую модель
    import { TariffRepository } from "./TariffRepository";
    import { TariffDto } from "./TariffDto";
    import { UserProfileDto } from "./UserProfileDto";
    
    export class TariffService {
        
        private readonly tarifRepository: TariffRepository = new TariffRepository();
    
        public async getTariffs(): Promise<TariffDto[]> { // возвращаем модель
            return await this.tarifRepository.requestTariffs();
        }
    
        // возвращаем модель
        public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
            const tariffs = await this.tarifRepository.requestTariffs();
            return tariffs.find((tarif: TariffDto) => {
                // было userProfile.age стало userProfile.getAge()
                const age = userProfile.getAge();
                return age &&
                    tarif.ageFrom <= age &&
                    age < tarif.ageTo;
            });
        }
        
        /**
         * ... множество других методов для работы с тарифами
         */
    }
    

    UserProfilRepository. Вместо any указываем возвращаемую модель
    import { UserProfileDto } from "./UserProfileDto";
    
    export class UserProfilRepository {
        public async getUserProfile(): Promise<UserProfileDto> { // возвращаем модель
            const response = await fetch("./api/user-profile");
            return await response.json();
        }
        
        /**
         * ... множество других методов для рабоыт с профилем пользователя
         */
    }
    

    TariffRepository. Вместо any указываем возвращаемую модель
    import { TariffDto } from "./TariffDto";
    
    export class TariffRepository {
        public async requestTariffs(): Promise<TariffDto[]> { // возвращаем модель
            const response = await fetch("./api/tariffs");
            return await response.json();
        }
    
        /**
         * ... множество других методов для работы с хранилищем тарифов
         */
    }
    

    UserProfileDto. Модель с описанием данных с которыми мы работаем
    export class UserProfileDto { // <-- модель с описанием данных и логикой
        public firstName: string | null = null;
        public lastName: string | null = null;
        public birthdate: Date | null = null;
    
        public getAge(): number | null {
            if (this.birthdate) {
                const ageDifMs = Date.now() - this.birthdate.getTime();
                const ageDate = new Date(ageDifMs);
                return Math.abs(ageDate.getUTCFullYear() - 1970);
            }
            return null;
        }
    
        public getFullname(): string | null {
            return [
                this.firstName ?? "",
                this.lastName ?? ""
            ]
                .join(" ")
                .trim() || null;
        }
    
    }
    

    TariffDto. Модель с описанием данных с которыми мы работаем
    export class TariffDto {
        public ageFrom: number = 0;
        public ageTo: number = 0;
        public price: number = 0;
    }
    


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

    Но есть одна проблема. Если мы попытаемся воспользоваться методами из модели что предоставил текущий репозиторий, то получим исключение. Все дело в том что методы response.json() и JSON.parse() возвращает не нашу модель, а объект JSON, который никак не связан с нашей моделью. Убедиться в этом можно если исполнить команду userProfile instanceof UserProfileDto, получится ложное утверждение. Для того что бы преобразовать данные полученные из внешнего источника к описанной модели существует процесс Десериализации данных.

    Десериализация данных


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

    И самое интересно тут то, что при проектировании ES2015 и добавлении ключевого слово class забыли добавить десериализацию… То что во всех языках присутствует из коробки, в ES2015 просто забыли…

    Для решения этой проблемы мною была написана библиотека для десериализации TS-Serializable, статью о которой можно прочитать по этой ссылке. Цель которой вернуть потерянный функционал.

    Добавляем поддержку десериализации в модели и саму десериализацию в репозиторий:
    TariffRepository. Добавляем процесс десериализации
    import { UserProfileDto } from "./UserProfileDto";
    
    export class UserProfilRepository {
        public async getUserProfile(): Promise<UserProfileDto> {
            const response = await fetch("./api/user-profile");
            const object = await response.json();
            return new UserProfileDto().fromJSON(object); // добавляем десериализацию
        }
        
        /**
         * ... множество других методов для рабоыт с профилем пользователя
         */
    }
    

    TariffRepository. Добавляем процесс десериализации
    import { TariffDto } from "./TariffDto";
    
    export class TariffRepository {
        public async requestTariffs(): Promise<TariffDto[]> { // возвращаем модель
            const response = await fetch("./api/tariffs");
            const objects: object[] = await response.json();
            return objects.map((object: object) => {
                return new TariffDto().fromJSON(object); // добавляем десериализацию
            });
        }
    
        /**
         * ... множество других методов для работы с хранилищем тарифов
         */
    }
    

    ProfileDto . Добавляем поддержку десериализации
    import { Serializable, jsonProperty } from "ts-serializable";
    
    export class UserProfileDto extends Serializable { // <-- наследуемся от базового класса
    
        @jsonProperty(String, null) // <-- вспомогательный декоратор
        public firstName: string | null = null;
    
        @jsonProperty(String, null) // <-- вспомогательный декоратор
        public lastName: string | null = null;
    
        @jsonProperty(Date, null) // <-- вспомогательный декоратор
        public birthdate: Date | null = null;
    
        public getAge(): number | null {
            if (this.birthdate) {
                const ageDifMs = Date.now() - this.birthdate.getTime();
                const ageDate = new Date(ageDifMs);
                return Math.abs(ageDate.getUTCFullYear() - 1970);
            }
            return null;
        }
    
        public getFullname(): string | null {
            return [
                this.firstName ?? "",
                this.lastName ?? ""
            ]
                .join(" ")
                .trim() || null;
        }
    
    }
    

    TariffDto. Добавляем поддержку десериализации
    import { Serializable, jsonProperty } from "ts-serializable";
    
    export class TariffDto extends Serializable { // <-- наследуемся от базового класса
    
        @jsonProperty(Number, null) // <-- вспомогательный декоратор
        public ageFrom: number = 0;
    
        @jsonProperty(Number, null) // <-- вспомогательный декоратор
        public ageTo: number = 0;
    
        @jsonProperty(Number, null) // <-- вспомогательный декоратор
        public price: number = 0;
    
    }
    


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

    Для чего Serializable и jsonProperty ?
    Для работы библиотеки необходимо наследоваться от базового класса Serializable — это необходимо для корректного расширения возможностей базового объекта Ecmascript и последующей с ним работы в Typescript. Также необходимо использовать вспомогательный декоратор jsonProperty для описания тех типов которые мы ожидаем от данных, это связано с тем что у Typescript очень не развитая рефлексия и он генерирует не корректную информацию для uniontypes. Но возможно при развитии рефлексии или трансформеров от них можно будет отказаться.

    Теперь у нас есть почти готовое приложение. Настало время протестировать логику написанную в слоях Controller, Service и Models. Для этого нам необходимо в слое Repository вместо реального запроса на сервер вернуть специально подготовленные тестовые данные. Но как же подменить Repository не трогая того кода который пойдет в продакшен. Для этого существует паттерн Dependency Injection.

    Dependency Injection — внедряем зависимости


    Dependency Injection — внедряет зависимости в слои Contoller, Service, Repository и дает возможность переопределить эти зависимости за пределами этих слоев.

    В программе слой Controller зависит от слоя Service, а он зависит от слоя Repository. В текущем виде слои сами вызывают свои зависимости через создание экземпляра. А для того что бы переопределить зависимость, слою необходимо из вне задать эту зависимость. Для этого есть множество способов, но самый популярным является передача зависимости как параметр в конструкторе.

    Тогда создание программы со всеми зависимости будет выглядеть следующим образом:
    var programm = new IndexPageController(new ProfileService(new ProfileRepository()));
    

    Согласитесь — выглядит ужасно. Даже с учетом того что в программе всего две зависимости, это уже выглядит ужасно. Что уже говорить про программы в которых сотни и тысячи зависимостей.

    Для решения проблемы понадобится специальный инструмент, а для этого его необходимо найти. Если обратиться к опыту других платформ, например Asp.Net Core то там регистрация зависимостей происходит на этапе инициализации программы и выглядит примерно следующим образом:
    DI.register(IProfileService,ProfileService);
    

    а далее фреймворк при создании контроллера уже сам создаст и внедрит эту зависимость.

    Но тут есть три существенные проблемы:
    1. При транспиляции Typescript в Javascript от интерфейсов не остается и следа.
    2. Все что попало в классический DI остается в нем навсегда. Его очень сложно вычистить при рефакторинге. А в веб приложении необходимо экономить каждый байт.
    3. Почти все библиотеки представлений не используют DI и конструкторы контроллеров заняты параметрами.


    В веб приложениях DI используется только в Angular 2+. В Angular 1 при регистрации зависимостей вместо интерфейса использовалась строка, в InversifyJS вместо интрерфейса используется Symbol. И все это реализовано настолько ужасно, что лучше уже много new как в первом примере этого раздела чем эти решения.

    Для решения всех трех проблем был придуман собственный DI, а решение для него мне помог найти фреймворк Java Spring и его декоратор autowired. Описание принципа работы этого DI можно прочитать в статье по ссылке, а репозиторий GitHub.

    Настало время применить получившийся DI в нашем приложении.

    Соединяем все в единое целое


    Для внедрения DI на все слои накинем декоратор reflection, который заставит typescript генерировать дополнительную метаинформацию о типах зависимостей. В контроллере где необходимо вызвать зависимости повесим декоратор autowired. А в том месте где программа инициализируется определим в каком окружении какая зависимость будет реализована.

    Для репозитория UserProfilRepository создадим такой же репозиторий, но с тестовыми данными вместо реального запроса. В итоге получаем следующий код:
    Main.ts. Место инициализации программы
    import { override } from "first-di";
    import { UserProfilRepository } from "./UserProfilRepository";
    import { MockUserProfilRepository } from "./MockUserProfilRepository";
    
    if (process.env.NODE_ENV === "test") {
        // для тестового окружение подменяем реальный запрос на тестовые данные
        override(UserProfilRepository, MockUserProfilRepository);
    }
    

    UserPageController. Внедряем зависимость через декоратор autowired
    import { UserProfilService } from "./UserProfilService";
    import { TariffService } from "./TariffService";
    import { UserProfileDto } from "./UserProfileDto";
    import { TariffDto } from "./TariffDto";
    import { InsuranceCaseDto } from "./InsuranceCasesDto";
    import { autowired } from "first-di";
    
    export class UserPageController {
    
        public userProfile: UserProfileDto = new UserProfileDto();
        public insuranceCases: InsuranceCaseDto[] = [];
        public tariffs: TariffDto[] = [];
        public bestTariff: TariffDto | void = void 0;
    
        @autowired() // внедряет зависимость
        private readonly userProfilService!: UserProfilService;
    
        @autowired() // внедряет зависимость
        private readonly tarifService!: TariffService;
    
        constructor() {
            // конструктор для внедрения не используется, т.к. занят фреймворком
            this.activate();
        }
    
        public activate(): void {
            this.requestUserProfile();
            this.requestTariffs();
        }
    
        public async requestUserProfile(): Promise<void> {
            try {
                this.userProfile = await this.userProfilService.getUserProfile();
                this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            } catch (e) {
                console.error(e);
            }
        }
    
        public async requestTariffs(): Promise<void> {
            try {
                this.tariffs = await this.tarifService.getTariffs();
            } catch (e) {
                console.error(e);
            }
        }
    
        /**
         * ... множество других методов, запрос страховых случаев,
         * редактирование профиля, выбор тарифа и прочее
         */
    }
    

    UserProfilService. Внедряем генерацию рефлексии и зависимости
    import { UserProfilRepository } from "./UserProfilRepository";
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection // заставляет typescript генерировать рефлексию
    export class UserProfilService {
    
        private readonly userProfilRepository: UserProfilRepository;
    
        constructor(userProfilRepository: UserProfilRepository) {
            // внедряем зависимость через конструктор
            this.userProfilRepository = userProfilRepository;
        }
    
        public async getUserProfile(): Promise<UserProfileDto> {
            return await this.userProfilRepository.getUserProfile();
        }
    
        /**
         * ... множество других методов для работы с профилем пользователя
         */
    }
    

    TariffService. Внедряем генерацию рефлексии и зависимости
    import { TariffRepository } from "./TariffRepository";
    import { TariffDto } from "./TariffDto";
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection // заставляет typescript генерировать рефлексию
    export class TariffService {
    
        private readonly tarifRepository: TariffRepository;
    
        constructor(tarifRepository: TariffRepository) {
            // внедряем зависимость через конструктор
            this.tarifRepository = tarifRepository;
        }
    
        public async getTariffs(): Promise<TariffDto[]> {
            return await this.tarifRepository.requestTariffs();
        }
    
        public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
            const tariffs = await this.tarifRepository.requestTariffs();
            return tariffs.find((tarif: TariffDto) => {
                const age = userProfile.getAge();
                return age &&
                    tarif.ageFrom <= age &&
                    age < tarif.ageTo;
            });
        }
    
        /**
         * ... множество других методов для работы с тарифами
         */
    }
    
    

    UserProfilRepository. Внедряем генерацию рефлексии
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection // заставляет typescript генерировать рефлексию
    export class UserProfilRepository {
        public async getUserProfile(): Promise<UserProfileDto> {
            const response = await fetch("./api/user-profile");
            const object = await response.json();
            return new UserProfileDto().fromJSON(object);
        }
    
        /**
         * ... множество других методов для работы с профилем пользователя
         */
    }
    

    MockUserProfilRepository. Новый репозиторий для тестирования
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection // заставляет typescript генерировать рефлексию
    export class MockUserProfilRepository { // репозиторий для тестов
        public async getUserProfile(): Promise<UserProfileDto> {
            const profile = new UserProfileDto();
            profile.firstName = "Констанция";
            profile.lastName = "Константинопольская";
            profile.birthdate = new Date(Date.now() - 1.5e12);
            return Promise.resolve(profile); // возвращаем тестовые данные
        }
    
        /**
         * ... множество других методов для рабоыт с профилем пользователя
         */
    }
    

    TariffRepository. Внедряем генерацию рефлексии
    import { TariffDto } from "./TariffDto";
    import { reflection } from "first-di";
    
    @reflection // заставляет typescript генерировать рефлексию
    export class TariffRepository {
        public async requestTariffs(): Promise<TariffDto[]> {
            const response = await fetch("./api/tariffs");
            const objects: object[] = await response.json();
            return objects.map((object: object) => {
                return new TariffDto().fromJSON(object);
            });
        }
    
        /**
         * ... множество других методов для работы с хранилищем тарифов
         */
    }
    


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

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

    Настало время увидеть результат работы программы. Для этого существует слой View.

    Внедряем View


    Слой View отвечает за представление данных которые содержатся в слое Controller пользователю. Я в примере буду использовать для этого React, но на его месте может быть любой другой, например Preact, Svelte, Vue, Mithril, WebComponent или любой другой.

    Для этого просто отнаследуем наш контроллер от React.Component, и добавим ему метод render с отображением представления:

    Main.ts. Запускает отрисовку React компонента
    import { override } from "first-di";
    import { UserProfilRepository } from "./UserProfilRepository";
    import { MockUserProfilRepository } from "./MockUserProfilRepository";
    import { UserPageController } from "./UserPageController";
    import React from "react";
    import { render } from "react-dom";
    
    if (process.env.NODE_ENV === "test") {
        // для тестового окружение подменяем реальный запрос на тестовые данные
        override(UserProfilRepository, MockUserProfilRepository);
    }
    
    render(React.createElement(UserPageController), document.body);
    

    UserPageController. Наследует от React.Component и добавляет метод render
    import { UserProfilService } from "./UserProfilService";
    import { TariffService } from "./TariffService";
    import { UserProfileDto } from "./UserProfileDto";
    import { TariffDto } from "./TariffDto";
    import { InsuranceCaseDto } from "./InsuranceCasesDto";
    import { autowired } from "first-di";
    import React from "react";
    
    // наследуем контроллер от React.Component
    export class UserPageController extends React.Component<object, object> {
    
        public userProfile: UserProfileDto = new UserProfileDto();
        public insuranceCases: InsuranceCaseDto[] = [];
        public tariffs: TariffDto[] = [];
        public bestTariff: TariffDto | void = void 0;
    
        @autowired()
        private readonly userProfilService!: UserProfilService;
    
        @autowired()
        private readonly tarifService!: TariffService;
    
        // реакт занял конструктор
        constructor(props: object, context: object) {
            super(props, context);
        }
    
        // после создания компонента запускаем запросы
        public componentDidMount(): void {
            this.activate();
        }
    
        public activate(): void {
            this.requestUserProfile();
            this.requestTariffs();
        }
    
        public async requestUserProfile(): Promise<void> {
            try {
                this.userProfile = await this.userProfilService.getUserProfile();
                this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
                this.forceUpdate(); // обновляем view после получения данных
            } catch (e) {
                console.error(e);
            }
        }
    
        public async requestTariffs(): Promise<void> {
            try {
                this.tariffs = await this.tarifService.getTariffs();
                this.forceUpdate(); // обновляем view после получения данных
            } catch (e) {
                console.error(e);
            }
        }
    
        // внедрение слоя view
        public render(): JSX.Element {
            return (
                <>
                    <div className="user">
                        <div className="user-name">
                            Имя пользователя: {this.userProfile.getFullname()}
                        </div>
                        <div className="user-age">
                            Возраст: {this.userProfile.getAge()}
                        </div>
                    </div>
                    <div className="tarifаs">
                        {/* остальная часть вьюхи */}
                    </div>
                </>
            );
        }
    
        /**
         * ... множество других методов, запрос страховых случаев,
         * редактирование профиля, выбор тарифа и прочее
         */
    }
    


    Добавив всего две строчки и шаблон представления и наш контроллер превратился в компонент реакта с рабочей логикой.

    Почему вызывается forceUpdate вместо setState?
    В коде можно заметить что forceUpdate вызывается напрямую, вместо использования setState. Дело в том что за любым методом перерисовки компонента setState, Redux, mobX или др., в любом фреймворке на любой платформе стоит вызов перерисовки представления. В React и Preact это forceUpdate, в Ангуляре ChangeDetectorRef.detectChanges(), в Mithril сразу redraw в остальных аналогично. Поэтому можно использовать ручное управление перерисовкой или создать Observable обертку над компонентом, как это делает MobX или Angular, для получение реактивности. Это дело вкуса и в данной статье не рассматривается.

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

    Разделение Controller и View


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

    Получаем следующие изменения:
    UserPageView. Вынесли представление в отдельный файл
    import { UserPageOptions, UserPageController } from "./UserPageController";
    import React from "react";
    
    export const userPageView = <P extends UserPageOptions, S>(
        ctrl: UserPageController<P, S>,
        props: P
    ): JSX.Element => (
        <>
            <div className="user">
                <div className="user-name">
                    Имя пользователя: {ctrl.userProfile.getFullname()}
                </div>
                <div className="user-age">
                    Возраст: {ctrl.userProfile.getAge()}
                </div>
            </div>
            <div className="tarifаs">
                {/* остальная часть вьюхи */}
            </div>
        </>
    );
    

    UserPageOptions. Вынесли view и React в отдельный файл
    import { UserProfilService } from "./UserProfilService";
    import { TariffService } from "./TariffService";
    import { UserProfileDto } from "./UserProfileDto";
    import { TariffDto } from "./TariffDto";
    import { InsuranceCaseDto } from "./InsuranceCasesDto";
    import { autowired } from "first-di";
    import { BaseComponent } from "./BaseComponent";
    import { userPageView } from "./UserPageview";
    
    export interface UserPageOptions {
        param1?: number;
        param2?: string;
    }
    
    // наследуем контроллер от BaseComponent
    export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {
    
        public userProfile: UserProfileDto = new UserProfileDto();
        public insuranceCases: InsuranceCaseDto[] = [];
        public tariffs: TariffDto[] = [];
        public bestTariff: TariffDto | void = void 0;
    
        // иньекция представления
        public readonly view = userPageView;
    
        @autowired()
        private readonly userProfilService!: UserProfilService;
    
        @autowired()
        private readonly tarifService!: TariffService;
    
        // типизированные пропсы
        constructor(props: P, context: S) {
            super(props, context);
        }
    
        // запустится при componentDidMount, см. BaseComponent
        public activate(): void {
            this.requestUserProfile();
            this.requestTariffs();
        }
    
        public async requestUserProfile(): Promise<void> {
            try {
                this.userProfile = await this.userProfilService.getUserProfile();
                this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
                this.forceUpdate();
            } catch (e) {
                console.error(e);
            }
        }
    
        public async requestTariffs(): Promise<void> {
            try {
                this.tariffs = await this.tarifService.getTariffs();
                this.forceUpdate();
            } catch (e) {
                console.error(e);
            }
        }
    
        /**
         * ... множество других методов, запрос страховых случаев,
         * редактирование профиля, выбор тарифа и прочее
         */
    }
    

    BaseComponent. Компонент который абстрагирует нас от конкретного фреймворка
    import React from "react";
    
    export class BaseComponent<P, S> extends React.Component<P, S> {
    
        // внедряем view
        public view?: (ctrl: this, props: P) => JSX.Element;
    
        constructor(props: P, context: S) {
            super(props, context);
            // можно дописать свою логику
        }
    
        // абстрагируем от жизненного цикла конкретного фреймворка
        public componentDidMount(): void {
            this.activate && this.activate();
        }
    
        // абстрагируем от жизненного цикла конкретного фреймворка
        public shouldComponentUpdate(
            nextProps: Readonly<P>,
            nextState: Readonly<S>,
            nextContext: any
        ): boolean {
            return this.update(nextProps, nextState, nextContext);
        }
    
        public componentWillUnmount(): void {
            this.dispose();
        }
    
        public activate(): void {
            // метод для переопределения
        }
    
        public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
            // метод для переопределения
            return false;
        }
    
        public dispose(): void {
            // метод для переопределения
        }
    
        // внедрение слоя view
        public render(): React.ReactElement<object> {
            if (this.view) {
                return this.view(this, this.props);
            } else {
                return React.createElement("div", {}, "Представление не определено");
            }
        }
    
    }
    


    Теперь наше представление находится в отдельном файле, а контроллер о нем ничего не знает, кроме того что оно есть. Возможно не стоит инжектировать представление через свойство контроллера, а делать это через декоратор как это делает Angular, но это тема для отдельных размышлений.

    В базовом компоненте также находится абстракция от жизненного цикла фреймворка. Во всех фреймворках они разные, но они есть во всех фреймворках. Angular это ngOnInit, ngOnChanges, ngOnDestroy. В React и Preact это componentDidMount, shouldComponentUpdate, componentWillUnmount. В Vue это created, updated, destroyed. В Mithril это oncreate, onupdate, onremove. В WebComponents это connectedCallback, attributeChangedCallback, disconnectedCallback. И так в каждой библиотеке. У большинства даже одинаковый или похожий интерфейс.

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

    Смотрим результат


    Осталось лишь оценить что же получилось. Вся программа имеет следующий финальный вид:
    Main.ts. Файл с которого запускается программа
    import { override } from "first-di";
    import { UserProfilRepository } from "./UserProfilRepository";
    import { MockUserProfilRepository } from "./MockUserProfilRepository";
    import { UserPageController } from "./UserPageController";
    import React from "react";
    import { render } from "react-dom";
    
    if (process.env.NODE_ENV === "test") {
        override(UserProfilRepository, MockUserProfilRepository);
    }
    
    render(React.createElement(UserPageController), document.body);
    

    UserPageView. Представление одного из компонентов программы.
    import { UserPageOptions, UserPageController } from "./UserPageController";
    import React from "react";
    
    export const userPageView = <P extends UserPageOptions, S>(
        ctrl: UserPageController<P, S>,
        props: P
    ): JSX.Element => (
        <>
            <div className="user">
                <div className="user-name">
                    Имя пользователя: {ctrl.userProfile.getFullname()}
                </div>
                <div className="user-age">
                    Возраст: {ctrl.userProfile.getAge()}
                </div>
            </div>
            <div className="tarifаs">
                {/* остальная часть вьюхи */}
            </div>
        </>
    );
    

    UserPageController. Логика одного из компонентов для взаимодействия с пользователем
    import { UserProfilService } from "./UserProfilService";
    import { TariffService } from "./TariffService";
    import { UserProfileDto } from "./UserProfileDto";
    import { TariffDto } from "./TariffDto";
    import { InsuranceCaseDto } from "./InsuranceCasesDto";
    import { autowired } from "first-di";
    import { BaseComponent } from "./BaseComponent";
    import { userPageView } from "./UserPageview";
    
    export interface UserPageOptions {
        param1?: number;
        param2?: string;
    }
    
    export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {
    
        public userProfile: UserProfileDto = new UserProfileDto();
        public insuranceCases: InsuranceCaseDto[] = [];
        public tariffs: TariffDto[] = [];
        public bestTariff: TariffDto | void = void 0;
    
        public readonly view = userPageView;
    
        @autowired()
        private readonly userProfilService!: UserProfilService;
    
        @autowired()
        private readonly tarifService!: TariffService;
    
        // запустится при componentDidMount, см. BaseComponent
        public activate(): void {
            this.requestUserProfile();
            this.requestTariffs();
        }
    
        public async requestUserProfile(): Promise<void> {
            try {
                this.userProfile = await this.userProfilService.getUserProfile();
                this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
                this.forceUpdate();
            } catch (e) {
                console.error(e);
            }
        }
    
        public async requestTariffs(): Promise<void> {
            try {
                this.tariffs = await this.tarifService.getTariffs();
                this.forceUpdate();
            } catch (e) {
                console.error(e);
            }
        }
    
        /**
         * ... множество других методов, запрос страховых случаев,
         * редактирование профиля, выбор тарифа и прочее
         */
    }
    

    BaseComponent. Базовый класс для всех компонентов программы
    import React from "react";
    
    export class BaseComponent<P, S> extends React.Component<P, S> {
    
        public view?: (ctrl: this, props: P) => JSX.Element;
    
        constructor(props: P, context: S) {
            super(props, context);
        }
    
        public componentDidMount(): void {
            this.activate && this.activate();
        }
    
        public shouldComponentUpdate(
            nextProps: Readonly<P>,
            nextState: Readonly<S>,
            nextContext: any
        ): boolean {
            return this.update(nextProps, nextState, nextContext);
        }
    
        public componentWillUnmount(): void {
            this.dispose();
        }
    
        public activate(): void {
            // метод для переопределения
        }
    
        public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
            // метод для переопределения
            return false;
        }
    
        public dispose(): void {
            // метод для переопределения
        }
    
        public render(): React.ReactElement<object> {
            if (this.view) {
                return this.view(this, this.props);
            } else {
                return React.createElement("div", {}, "Представление не определено");
            }
        }
    
    }
    

    UserProfilService. Сервис для переиспользования логики между компонентами для работы с профилем пользователя
    import { UserProfilRepository } from "./UserProfilRepository";
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection
    export class UserProfilService {
    
        private readonly userProfilRepository: UserProfilRepository;
    
        constructor(userProfilRepository: UserProfilRepository) {
            this.userProfilRepository = userProfilRepository;
        }
    
        public async getUserProfile(): Promise<UserProfileDto> {
            return await this.userProfilRepository.getUserProfile();
        }
    
        /**
         * ... множество других методов для работы с профилем пользователя
         */
    }
    

    TariffService. Сервис для переиспользования логики между компонентами для работы с тарифами
    import { TariffRepository } from "./TariffRepository";
    import { TariffDto } from "./TariffDto";
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection
    export class TariffService {
    
        private readonly tarifRepository: TariffRepository;
    
        constructor(tarifRepository: TariffRepository) {
            this.tarifRepository = tarifRepository;
        }
    
        public async getTariffs(): Promise<TariffDto[]> {
            return await this.tarifRepository.requestTariffs();
        }
    
        public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
            const tariffs = await this.tarifRepository.requestTariffs();
            return tariffs.find((tarif: TariffDto) => {
                const age = userProfile.getAge();
                return age &&
                    tarif.ageFrom <= age &&
                    age < tarif.ageTo;
            });
        }
    
        /**
         * ... множество других методов для работы с тарифами
         */
    }
    

    UserProfilRepository. Репозиторий для получение профиля с сервера, его проверки и валидации
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection
    export class UserProfilRepository {
        public async getUserProfile(): Promise<UserProfileDto> {
            const response = await fetch("./api/user-profile");
            const object = await response.json();
            return new UserProfileDto().fromJSON(object);
        }
    
        /**
         * ... множество других методов для рабоыт с профилем пользователя
         */
    }
    

    MockUserProfilRepository. Репозиторий для тестирования логики и верстки на тестовых данных
    import { UserProfileDto } from "./UserProfileDto";
    import { reflection } from "first-di";
    
    @reflection // заставляет typescript генерировать рефлексию
    export class MockUserProfilRepository { // репозиторий для тестов
        public async getUserProfile(): Promise<UserProfileDto> {
            const profile = new UserProfileDto();
            profile.firstName = "Констанция";
            profile.lastName = "Константинопольская";
            profile.birthdate = new Date(Date.now() - 1.5e12);
            return Promise.resolve(profile); // возвращаем тестовые данные
        }
    
        /**
         * ... множество других методов для рабоыт с профилем пользователя
         */
    }
    

    TariffRepository. Репозиторий для получение тарифов с сервера, их проверки и валидации
    import { TariffDto } from "./TariffDto";
    import { reflection } from "first-di";
    
    @reflection // заставляет typescript генерировать рефлексию
    export class TariffRepository {
        public async requestTariffs(): Promise<TariffDto[]> {
            const response = await fetch("./api/tariffs");
            const objects: object[] = await response.json();
            return objects.map((object: object) => {
                return new TariffDto().fromJSON(object);
            });
        }
    
        /**
         * ... множество других методов для работы с хранилищем тарифов
         */
    }
    

    UserProfileDto. Модель с логикой и описанием данных для переноса данных между слоями
    import { Serializable, jsonProperty } from "ts-serializable";
    
    export class UserProfileDto extends Serializable {
    
        @jsonProperty(String, null)
        public firstName: string | null = null;
    
        @jsonProperty(String, null)
        public lastName: string | null = null;
    
        @jsonProperty(Date, null)
        public birthdate: Date | null = null;
    
        public getAge(): number | null {
            if (this.birthdate) {
                const ageDifMs = Date.now() - this.birthdate.getTime();
                const ageDate = new Date(ageDifMs);
                return Math.abs(ageDate.getUTCFullYear() - 1970);
            }
            return null;
        }
    
        public getFullname(): string | null {
            return [
                this.firstName ?? "",
                this.lastName ?? ""
            ]
                .join(" ")
                .trim() || null;
        }
    
    }
    

    TariffDto. Модель с логикой и описанием данных для переноса данных между слоями
    import { Serializable, jsonProperty } from "ts-serializable";
    
    export class TariffDto extends Serializable {
    
        @jsonProperty(Number, null)
        public ageFrom: number = 0;
    
        @jsonProperty(Number, null)
        public ageTo: number = 0;
    
        @jsonProperty(Number, null)
        public price: number = 0;
    
    }
    


    В итоге получили модульное масштабируемое приложение с очень маленьким количеством бойлерплейта (1 базовый компонент, 3 строчки на внедрение зависимостей на класс) и очень низкими накладными расходами (фактически только на внедрение зависимостей, все остальное логика). Так же мы не завязаны на какую либо библиотеку представления. Когда умер Angular 1 многие начали переписывать приложения на React. Когда кончились разработчики для Angular 2, многие компании стали страдать из-за скорости разработки. Когда умрет React в очередной раз придется переписывать решения завязанные на его фреймворк и экосистему. Но с Читой Архитектурой про завязыванием на фреймворк можно забыть.

    В чем преимущество относительно Redux?


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

    Как видно из схемы с ростом Redux приложения масштабируется вертикально, Store и количество Reducers также увеличивается и превращается в бутылочное горлышко. А количество накладных расходов на пересоздание Store и поиск нужного Reducer начинает превышать полезную нагрузку.

    Проверить соотношение накладных расходов к полезной нагрузке на приложении среднего размера можно простым тестом.
    let create = new Function([
    "return {",
    ...new Array(100).fill(1).map((val, ind) => `param${ind}:${ind},`),
    "}"
    ].join(""));
    
    let obj1 = create();
    
    console.time("store recreation time");
    let obj2 = {
        ...obj1,
        param100: 100 ** 2
    }
    console.timeEnd("store recreation time");
    
    console.time("clear logic");
    let val = 100 ** 2;
    console.timeEnd("clear logic");
    
    console.log(obj2, val);
    
    // store recreation time: 0.041015625ms
    // clear logic: 0.0048828125ms
    

    На пересоздания Store в 100 свойств ушло в 8 раз больше времени чем на саму логику. При 1000 элементов это уже 50 раз больше. Кроме того одно действие пользователя может породить целую цепочку экшенов, вызов которых тяжело отлавливать и отлаживать. Можно конечно возразить что 0,04 мс на пересоздание Store это очень мало и тормозить не будет. Но 0,04 мс это на процессоре Core i7 и на один экшен. С учетом более слабых мобильных процессоров и тем что одно действие пользователя может породить десятки экшенов, все это приводит к тому что расчеты не вписываются в 16 мс и создается ощущение что приложение тормозит.

    Давайте сравним с тем как растет приложение на Чистой Архитектуре:
    Чистая Архитектура

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

    Бонус 1: Расширение функционала компонентов фреймворка


    Бонусом получили возможность дополнять или изменять поведения компонентов библиотеки представления. Например реакт при ошибке в представлении не отрисовывает все приложение, если сделать небольшую доработку:
    export class BaseComponent<P, S> extends React.Component<P, S> {
    
        ...
    
        public render(): React.ReactElement<object> {
            try {
                if (this.view) {
                    return this.view(this, this.props);
                } else {
                    return React.createElement("div", {}, "Представление не определено");
                }
            } catch (e) {
                return React.createElement(
                    "div",
                    { style: { color: "red" } },
                    `В этом компоненте произошла ошибка: ${e}`
                );
            }
        }
    
    }
    

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

    Бонус 2: Валидация данных


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

    Бонус 3: Создание сущностей


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

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


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

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

    Какой подход вы использовали до прочтения этой статьи

    • 55,8%Flux, Redux, Vuex или другой стейт менеджер63
    • 22,1%MobX, RxJs или другой observable без ЧА25
    • 24,8%Чистая Архитектура28
    • 8,8%Другой, поясню в комментариях…10

    Понравилась ли вам Чистая Архитектура, планируете ли использовать такой подход?

    • 30,3%Да44
    • 28,3%Возможно41
    • 22,1%Нет32
    • 17,2%Я размещаю логику на бэкенде25
    • 2,1%Я верстаю, у меня нету логики3
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 80

      +7
      Service — отвечает за всю бизнес логику приложения.

      Entities — отвечают за работу с базой данных и представляют из себя структуру повторяющую таблицу или документ в базе данных.

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

        0
        Анемичные модели считаются антипатерном.

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

            Координация — смотря а каком смысле.


            Если это связанные модели в одном контексте (например, пользователь в контексте фотоальбома и его фотографии), то все должно делаться через Aggregate Root.
            Если это разные контексты (например, наличие фото в фотоальбоме позволяет использовать сервис знакомств), или разные слои (после добавления фото отправляется емейл) — события.
            Если нужно построить PDF с отчётом, сколько пользователей, добавивших фото, воспользовались сервисом знакомств, это уже вполне себе сервис.

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

              Простите, а вам надо, чтобы авторитетный дядька сказал "НаркотикиАнемичные модели — это плохо, мммкей"? Аргументы против приводят и Фаулер, и Эванс, и все подряд, а уж конечный вывод — дело читателя.


              Основная же ошибка — это привязывать модель к таблице в базе данных. Из этого непонятно когда случившегося перехода от "модели сущности" к "модели таблицы в РСУБД" все проблемы. Если мыслить сущностями, сразу очевидно нарушение инкапсуляции.

            +1

            Проголосовал за "другое...' Рассказываю: воодушевлен CA, но без фанатизма. Приложение готово в любой момент к переходу к CA, но, например, entity жёстко связаны с MobX и поробрасываются во вью (React) как есть, без DTO.

              +2
              Этот подход на самом деле самый популярный на mobx. Я бы назвал его ленивый CA =)
              +9
              не в обиду будет сказано, но стоит ли призрачная возможность переезда на другую библиотеку/фреймворк такого количества лишних сущностей?
                +2
                Я ее использую не столько что бы абстрагироваться от фреймворка сколько абстрагироваться от платформы. Поскольку я пишу не только веб, но и бек, и мобилки, и настольные приложения на C#. И такой подход позволяет мне свободно переключаться между платформами и гибко реагировать на любые повороты в бизнес требованиях.
                +5
                В книжке Р. Мартина вы пропустили главу Screaming Architecture. Там он прямо говорит, что все вариации MVC не стоит использовать. В связи с этим вся статья становится сомнительной.
                Ну и «независимость от UI» для веб-приложения выглядит странно.
                Да и независимость от фреймворка для JS — утопия.
                  0
                  Возможно вы ошиблись главой, но не могу найти такого в книге. Книгу я читал очень давно, но на сколько я помню слой представления в ней вообще не рассматривается.
                    +4
                    Не, не ошибся. Именно так и называлась. Смысл в том, что части системы должны явно говорить об их назначении и в этом ключе разделение на модели и контроллеры Мартин считает неверным подходом.
                    И вообще: чистая архитектура всё таки подаётся в привязке к серверной части системы, реализованной на ООП-языке.
                    Натянуть, к примеру, Go-приложение на эту концепцию ещё можно. Но вот пытаться разрабатывать веб-приложение по принципам чистой архитектуры, на мой взгляд, полнейшая глупость.
                  0
                  Удалено
                    0
                    Удалено
                      +1
                      Если вы скинете ссылку на его текст, то ситуация проясниться. Пока что совсем не понятно о чем вы говорите.
                      По поводу веба к счастью разработчики VS Code не посчитали это глупостью и написали на CA приложение на электроне которое не тормозит. Кроме того такой подход из коробки идет у Android и iOs, в iOs даже название Controller сохранено.
                        +3
                        По поводу веба к счастью разработчики VS Code не посчитали это глупостью и написали на CA приложение

                        У меня есть странный вопрос: а где найти подтверждение тому, что разработчики VS Code писали на "Clean Architecture"?


                        Аналогично:


                        Чистая архитектура не привязана к какому то конкретному фреймворку, платформе или языку программирования. Десятилетия ее используют для написания настольных приложений. Его эталонную реализацию можно найти во фреймворках для серверных приложений Asp.Net Core

                        Что такое "эталонная реализация чистой архитектуры в asp.net core"?

                          0
                          Открыть ссылку выше которая приведет прямо на исходники.
                            +1

                            Я поискал и не нашел в этих исходниках ни одного упоминания "Clean Architecture".

                              0
                              Clean Architecture в коде никогда не упоминается. Суть чистой архитектуры в организации кода по слоям View, Controller, Service, Repository, Model, ViewModel. Все они у вас перед глазами сразу по ссылке.
                                +6
                                Суть чистой архитектуры в организации кода по слоям View, Controller, Service, Repository, Model, ViewModel

                                Ненене, подождите. Давайте не путать. Когда какая-то команда что-то написала "на Clean Architecture" — это значит, что они взяли книгу и следовали ее правилам. Если же вы просто видите в коде разделение на какие-то области, которые вам кажутся похожими на приведенные в "Clean Architecture" — это значит только то, что вы их там видите. Слоистая архитектура, если что, была задолго до книги Мартина.

                                  +1
                                  Да, Мартин не является автором такого подхода, так же как и Рихтер не является автором CLR. Но Мартин написал популярную книгу рассказывающую о таком подходе и дал название такому подходу, которое стало общепринятым. И Чистая Архитектура дополняет слоистую архитектуру.
                                    +1
                                    Но Мартин написал популярную книгу рассказывающую о таком подходе и дал название такому подходу, которое стало общепринятым.

                                    Неа, не стало. С чего вы это взяли?


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

                                      0
                                      Согласен что эталонной нет. В том же Asp.Net Core и JavaSpring между ними есть разница. Но тут главное не дословное следование книжке, а сама суть процесса по разделению кода.
                                        +1
                                        а сама суть процесса по разделению кода.

                                        Знаете, любая архитектура — это процесс по разделению кода. Почему вы считаете, что VS Code — это доказательство успешности именно Clean Architecture, а не любой другой?

                                          0
                                          Хорошо, допустим в VS Code это не ЧА. Тогда давайте найдем что же это за архитектура там применяется.
                                            +1

                                            VSCode архитектура там применяется :) Clean Architecture для меня это принципы по которым можно спроектировать архитектуру и/или по соблюдению которых можно отнести уже существующую к чистой или нечистой. На первый взгляд код VSCode к чистой отнести нельзя.

                                              0
                                              Хорошо. Если не нравится сравнение с ЧА, давайте по другому. Каким общепринятым названием называется архитектура в которой логика делиться на слои view, controller, model, service, viewModel, repository, entity и так далее...?

                                              Подход который использует VSCode я уже десять лет наблюдаю в ASP.Net. Так что вариант «своя архитектура» не учитывается.
                                                +2
                                                Каким общепринятым названием называется архитектура в которой логика делиться на слои

                                                Layered architecture. PoEAA, первая часть, первая глава. Страница 17 в издании Addison-Wesley 2010 года.

                                              +2

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

                                    +4

                                    Более того. Открываем книгу, глава 21, "SCREAMING ARCHITECTURE" (дадада, вам ее уже поминали"):


                                    So what does the architecture of your application scream? When you look at the top-level directory structure, and the source files in the highest-level package, do they scream “Health Care System,” or “Accounting System,” or “Inventory Management System”? Or do they scream “Rails,” or “Spring/Hibernate,” or “ASP”?
                                    [...]
                                    Just as the plans for a house or a library scream about the use cases of those buildings, so should the architecture of a software application scream about the use cases of the application.

                                    Открываем репозиторий по ссылке: команды, контроллеры, ядро, модели, режимы, сервисы, представления.


                                    Явно противоречит написанному в книге.

                                      0
                                      Это скорее касается организации кода, нежели архитектуры. У Мартина есть свое мнение, у меня свое. Я стараюсь придерживаться организации кода как по ссылке. У такого подхода тоже свои глубокие корни.
                                        +2
                                        Это скорее касается организации кода, нежели архитектуры

                                        А архитектура — она разве не про организацию кода (в широком смысле)?


                                        У Мартина есть свое мнение, у меня свое. Я стараюсь придерживаться организации кода как по ссылке.

                                        Гм. А я стараюсь придерживаться своего собственного подхода. А что, у Мартина есть свое мнение, у вас свое, у меня — свое. Но у меня все равно "Чистая архитектура" по книжке. Да?

                                          0
                                          Про организацию в том числе. Но SCREAMING ARCHITECTURE нужно рассматривать отдельно от Clean Architecture. И я считаю ее личным мнением автора книги.

                                          И самый главный момент, книга написана по уже имеющийся архитектуре, а не код организовали по книге.
                                            +4
                                            Но SCREAMING ARCHITECTURE нужно рассматривать отдельно от Clean Architecture.

                                            То есть вы из книги выбираете только удобные вам места?


                                            И я считаю ее личным мнением автора книги.

                                            А Clean Architecture — не считаете?


                                            И самый главный момент, книга написана по уже имеющийся архитектуре

                                            Прекрасно. По какой? Где взять тот конкретный пример (или набор) примеров, по которому написана книга?

                                      +1
                                      Суть чистой архитектуры в организации кода по слоям View, Controller, Service, Repository, Model, ViewModel

                                      Это ваше понимание сути CA? Как мне помнится, в CA четыре слоя и ни один из них так не называется. А даже если провести аналогии, то View и ViewModel — это один слой UI или нет?

                                0

                                В упор не вижу разделения на четыре канонических слоя. Да, может быть можно все каталоги верхнего уровня однозначно определить к одному из слоёв CA, но отсутствие явного подтверждения этому означает, по-моему, что в лучшем случае разработчики VSCode не желают афишировать использование CA. А более вероятно, что однозначно отнести просто не получится. Например, в services смешаны сервисы из разных слоёв и хорошо если каждый отдельный можно отнести к конкретному слою, а не, например, в одном классе сочетаются и элементы usecases, и элементы controllers

                                  0

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

                                    0
                                    именно
                                +1
                                Services по сути антикорапшн лаер который изолирует вас от того кто вызвает код ваших Entity а Repository это антикорапшн лаер который изолирует вас от того откуда вы получаете данные для ваших Entity. По хорошему если у вас чистая архитектура с богатой моделью то вся логика в Entity если делаете с анемичной моделью то вся логика в DomainService. Не надо путать с тем Service который изолирует ваше приложение. Это ApplicationService. DomainService что-то вычисляет и возвращает результат. Он не использует Repository. Просто принимает входные значения и возвращает результат. Например Validator, AmountCalculator да просто обычный
                                class Calculator
                                {
                                 void Add(int rhs, int lhs) => rhs + lhs;
                                }
                                

                                Типичный DomainSerivice. Хотя лично я против анемичной модели и против DomainServices. Чуть подробнее можно тут почитать. habr.com/ru/post/493426
                                  0
                                  Я работаю в команде. И обычному фронту вообще тяжело понять что такое Service и почему нельзя на jquery скрипт написать. Поэтому я стараюсь не усложнять такими подробностями. Ведь в любом случае это Service =)
                                    0
                                    В синей книге (DDD) Сервис определен как любой класс без состояния. Ну и разделение такое нужно чтобы отделить те классы без состояния которые служат слоем изоляции от тех классов без состояния которые что-то вычисляют.
                                      –1
                                      Ну и главное вообще что я хотел сказать и что комментатор выше до меня говорил. Я вообще люблю фронтэндшиков. Бывшая девушка фронтом занималась с которой спокойно растилась (добра тебе Маша если это читаешь :) да и сам пишу иногда для себя что-то на фронте на том же Angular и Blazor просто мое ИМХО что для фронта это все излишнее усложнение. Не надо это все там. Типичный фронт это провалидировал и отправил на сервер или получил с сервера и показал что пришло. Вот всякие мобильные или андроид приложение (которые тоже фронт) еще более менее этим всем заморачиваются если у них своя логика есть, а в большинстве случаев — наворачивать там такие слои это просто трата времени.
                                        +2
                                        Смотря что и как писать. Если просто провалидировал и отправил на сервер или получил с сервера и показал что пришло, то да, излишне. А если писать Приложение с большой буквы, то очень даже помогает =).
                                          +1
                                          Хм, я в во всяком тырпрайсе большом и кровавом (вообще Java/C# как раз из этой сферы) и тут логика на фронте может привести к очень веселым последствиям (в финансовом плане). Обычно стараемся делать минимум на фронте и максимум на беке.
                                            0
                                            На самом деле это стереотип. На беке достаточно иметь защищенное АПИ, для любых клиентов и интеграций. А выдает ваш бек html или json на безопасность никак не влияет.
                                              +1
                                              На беке достаточно иметь защищенное АПИ

                                              … внутри которого валидировать всю бизнес-логику, которую вы уже проделали на клиенте.

                                              +1

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


                                              С точки зрения UX во многих случаях лучше было бы максимально проделать подготовительную работу на фронте, а на бэке лишь проверить на допустимость и отдать флаг успешности. То, что называется offline first.

                                          +2
                                          И обычному фронту вообще тяжело понять что такое Service

                                          Объяснять не пробовали?

                                            0
                                            Объясняю потихоньку, вот статью даже написал. Но я не бесконечный.
                                        +2

                                        Внезапный вопрос: а почему вдруг приложение, целиком и полностью написанное на asp.net — не веб-приложение?

                                          0
                                          Полусайт-полуприложение. К тому же весьма дорогое по стоимости поддержке серверов. Отдать клиенту статику и отрендерить на клиенте приложение в сотни раз дешевле.
                                            +2
                                            Полусайт-полуприложение.

                                            Почему? Открываем вики:


                                            a web application or web app is a client–server computer program that the client (including the user interface and client-side logic) runs in a web browser

                                            Приложение на asp.net под это определение попадает.

                                          0

                                          Есть у меня ощущение, что как только ваши View стали работать отлично от идеоматичного React (принимать контроллер первым параметром, вместо props), то вcякий тулинг Реакта с ними не особенно будет работать.


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

                                            0
                                            Тулинг работает исправно.
                                            Все данные для переиспользования распологаются в сервисе.
                                              0
                                              Все данные для переиспользования распологаются в сервисе.

                                              А, почитал еще немного и понял. У вас же DI через property injection, а не через constructor. Тогда да, прокидывать вручную ничего не нужно.

                                            +2
                                            Уважаемый автор, искренне не понимаю вашу ненависть в Angular 2+ )
                                            Да, это не чистая CA, но это один из немногих фрэймворков, который диктует свою архитектуру из коробки, а не порождает зоопарки, извините, говнокода.
                                            Вы написали замечательную статью, но на практике ей вдохновятся единицы, а большая часть сообщество React/Vue так и продолжит порождать в каждом новом проекте новую архитектуру. Раздувать одну бибилиотеку кучей других, потому что из коробки функционала не хватает для большинства real-world проектов.
                                            Как пример — из коробки в React нет TS. Половина будет юзать старый добрый JS, половина перейдет на TS. Дальше хуже — Redux, MobX, просто на хуках. Инструментов великое множество, и каждый такой инструмент зачастую приводит к разнице в архитектуре.

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

                                            По поводу Angular 1.x (AngularJS) вообще стоит забыть как о страшном сне, это была ошибка, но она привела к созданию Angular 2 =)
                                              +2
                                              Поступок с Angular 1, когда фреймворк просто бросили и написали другой, нанес мне психологическую травму. С тех пор я не доверяю никакому Angular =).

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

                                              Я знаю что 60% разработки на реакте это редакс. А так я предложил успешную концепцию которая справляется с любой задачей по гибкости и производительности. И если хотя бы часть сообщества не побоится использовать такой подход, то это уже хороший шаг к наведению порядка на реакте.
                                                +2
                                                Чем хорош React — он настолько минималистичен, что на его базе вы можете построить проект с хорошей архитектурой.
                                                Чем хорош Angular — это уже сделали за вас, за вашего друга и за вот того разраба, вместо собирания своего холиварного конструкта вы берете и работаете.
                                                  –1
                                                  Чем хорошо не быть разрабом — вместо собирания своего холиварного конструкта вы берете и не работаете разрабом, круто же никаких холиваров, архитектур и прочего «мусора».
                                              0
                                              Мне кажется скоро статьи про «замечательную» чистую архитектуру которая «создана» для Front-end приложений будут раздражать сильнее, чем очередная статья от «вирусолога» про корону. Какая эта уже по счету за последнее время?
                                                0

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

                                                  +2
                                                  Размеры веб приложений растут. А решений для написания больших приложений нет. Поэтому люди обращаются к практикам из других систем. Поэтому таких статей будет только больше.
                                                    –7
                                                    Размеры веб приложений растут. А решений для написания больших приложений нет. Поэтому люди обращаются к практикам из других систем.

                                                    Да что вы говорите, правда что ли? Спасибо спаситель, что снизошли с небес чтобы поведать нам «правильное» решение для написания больших приложений, а то мы глупенькие совсем и не знаем что и как писать, чем и как пользоваться, выжимать из инструментов все соки на полную катушку для решения наших задач. А ещё мы настолько ущербные, что пользуемся фреймворками, а не пишем «фреймворконезависимый» и «правильный» код, который спасет нас от всего, а ещё этот код очень «легко» поддерживать, одно «удовольствие» вообще что либо имплементить в рамках такой архитектуры.
                                                  +3

                                                  (вынесу в корень, многобукав)


                                                  Но SCREAMING ARCHITECTURE нужно рассматривать отдельно от Clean Architecture. [...] И самый главный момент, книга написана по уже имеющийся архитектуре, а не код организовали по книге.

                                                  Хорошо, давайте посмотрим на только Clean Architecture, это глава 22 соответствующей книги.


                                                  Пост:


                                                  для примера возьму эталонную реализацию этой архитектуры для Asp.Net Core.

                                                  Вот упрощенный пример приложения:

                                                  Из кода я оставлю только самое (для меня) важное:


                                                  public IndexController(IUserProfileService userProfileService)
                                                  public UserProfileService(IUserProfileService userProfileService)
                                                  //упс, циклическая зависимость
                                                  //наверное, вот это имелось в виду?
                                                  public UserProfileService(IUserProfileRepository userProfileRepository)
                                                  public UserProfileRepository(DBContext dbContext)

                                                  Рисуем цепочку зависимостей: контроллер -> сервис -> репозиторий -> БД. Проверяем себя — в статье явно написано то же самое (это, кстати, важно, потому что одно небольшое изменение радикально изменит архитектуру):


                                                  В программе слой Controller зависит от слоя Service, а он зависит от слоя Repository.

                                                  Поскольку сервис зависит от репозитория, слой репозитория является внутренним по отношению к слою сервисов. Аналогично, слой БД является внутренним по отношению к слою репозитория. Это соответствует главному правилу из Clean Architecture:


                                                  Source code dependencies must point only inward...

                                                  … точнее, только первой его части. Потому что вот вторая:


                                                  ...toward higher-level policies.

                                                  Но репозиторий и БД — это не higher-level policy, а деталь имплементации. Проверяем себя (выделение мое):


                                                  The software in the interface adapters layer is a set of adapters that convert data from the format most convenient for the use cases and entities, to the format most convenient for some external agency such as the database or the web. [...] No code inward of this circle should know anything at all about the database.

                                                  Напомню, что согласно диаграмме в той же главе, слой interface adapters — это слой gateways, второй снаружи. Слой сервисов (бизнес-логики) — третий, и он не может зависеть от репозиториев, размещенных во втором, согласно основному правилу. Следовательно, описанное в статье


                                                  он [слой Service] зависит от слоя Repository.

                                                  противоречит CA, как она описана в главе 22. Ровно то же самое применимо и к зависимости репозиторий-фреймворк БД (второй и первый слои соответственно).


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


                                                  PS Бонус-пойнт:


                                                  Достигается такая гибкость за счет разделения приложения на слои Service, Repository, Model.

                                                  Вот только нет таких слоев у Мартина. У него есть четыре слоя:


                                                  1. Frameworks & drivers
                                                  2. Interface Adapters
                                                  3. Application Business Rules
                                                  4. Enterprise Business Rules

                                                  Из которых два внутренних еще называются Use Cases и Entities.


                                                  Слово Model как название слоя в главе 22 не встречается. Слово Repository там не встречается вообще.


                                                  Еще про репозитории (хотя это, конечно, глава 34 уже):


                                                  The keen-eyed reader will notice that the OrdersRepository from previous diagrams has been renamed to simply be Orders. [...] To put that another way, we talk about “orders” when we’re having a discussion about the domain, not the “orders repository.”
                                                    0
                                                    Боюсь если соблюдать всё что вы написали, то это отпугнет вообще всех фронтенд разработчиков. Можно конечно и интерфейс писать на каждый класс и разделение слоев делать более «эталонным» и вообще сделать все «энтерпрайзненько», но это не решит никаких проблем проекта, только раздует техдолг до огромных масштабов.

                                                    Поэтому не надо никому доказывать что вы лучше поняли книгу, тем более что автор книги не автор этой архитектуры. И не надо так радикально подходить к соблюдению всего что написано в книге. Достаточно взять только то что решает твои проблемы и не создает проблем с поддержкой и уже получится хороший продукт.
                                                      +1
                                                      Боюсь если соблюдать всё что вы написали, то это отпугнет вообще всех фронтенд разработчиков.

                                                      А если не соблюдать, то получится архитектура, которая не соответствует соответствующей главе соответствующей книги. Вот и все.


                                                      разделение слоев делать более «эталонным»

                                                      Что значит "более эталонным", вы говорили, что вы уже привели эталонную архитектуру?


                                                      только раздует техдолг до огромных масштабов.

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


                                                      тем более что автор книги не автор этой архитектуры

                                                      А кто автор этой архитектуры?


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

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

                                                        0
                                                        Тогда может быть напишите свою статью? Как вы представляете себе ЧА в веб-приложении на любой библиотеке? Вдруг ваш подход и вправду окажется лучше. По обрывкам предложений не понятно.
                                                          +2
                                                          Как вы представляете себе ЧА в веб-приложении на любой библиотеке?

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


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


                                                          Вдруг ваш подход и вправду окажется лучше.

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

                                                      +1
                                                      > Вот только нет таких слоев у Мартина. У него есть четыре слоя

                                                      В книге он пишет, что слоев может быть любое количество. Главное чтобы зависимости были направлены в сторону внутренних кругов.
                                                        0

                                                        Важную часть забыли: "в сторону внутренних кругов и повышения абстракции" ('toward higher-level policies"). Теперь давайте задумаемся, как же расположить слои "Service, Repository, Model" в порядке повышения абстракции?

                                                      +3

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


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


                                                      P.S. реальная история – мигрировали библиотеку виджетов с Mithril на Preact. Библиотека была написана грамотно, весь mithril-специфичный код был собран в специальный адаптер, все компоненты работали через него. Но в процессе миграции выяснилось, что API Mithril и Preact отличается, и полностью реализовать тот же самый интерфейс адаптера не получится, надо рефакторить адаптер, а значит и все компоненты целиком.

                                                        +2

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

                                                          0
                                                          Да вы правы. Логика которая относится к модели нужно размещать в методах модели. В примерах кода так и было сделано. В сервисах помимо склеивание, конкретно у меня есть еще логика по обработке не связанных моделей или массива моделей. Например из массива моделей выбрать подходящие под какие либо условия. Или на основание двух разных моделей получить третью. А также случаи логики не связаны с моделями совсем. Если у вас по другому, расскажите пожалуйста как реализовано у вас.

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

                                                            Давно уже придумано, Application Service vs Domain Service

                                                              +1
                                                              Спасибо за подсказку. Теперь буду делить сервисы на Application Services, Infrastructure Services, Domain Services. Так же выделил еще одну группу View Services с логикой для вьюхи, не связанную никак с бизнесом, не зашиваемую в компонент, не относится к паттерну helper. Например генерирует css анимации для разных вьюх на основе состояния бизнес логики. Да да, у меня и такое встречается =)
                                                                –1
                                                                Выделите ещё штук 15 сервисов, будет ещё лучше ведь. И ещё примените заодно десяток другой паттернов и вообще всё по красоте будет. Что мелочиться то.
                                                            +3

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


                                                            mamento


                                                            будем дублировать между сервисами?

                                                            Зачем дублировать? Сервисы можно (и нужно) инжектить друг в друга. Так что если что-то реализовано в одном сервисе — всегда можно использовать эту реализацию и в другом.

                                                              0
                                                              Не прибит, в статье про это не рассказал, но менять можно. На один вью может приходиться несколько контроллеров, на один контроллер может приходиться несколько вью. Главное что бы интерфейс сохранялся.
                                                              –1

                                                              Druu так давно никто не делает, кроме вас. View надо инжектить в медиатор, или говоря современным языком в ноду.

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

                                                              Самое читаемое