Как ваш код домена взаимодействует с вашим интерфейсным приложением
(Это продолжение статьи о доменно-ориентированной архитектуре. Чтобы узнать, что это такое, почему это может быть важно и как реализовать это в коде внешнего интерфейса, ознакомьтесь с первой частью .)
К настоящему времени у вас есть класс предметной области, его типы и репозиторий. Вы, вероятно, задаетесь вопросом, как это будет работать с кодом вашего приложения и вашей средой (Vue, React, Angular, Svelte и т. д.) и как вы собираетесь использовать это в своих компонентах. Мне жаль разочаровывать, но мы еще не совсем готовы прикоснуться к коду фреймворка, но мы приближаемся к этому.
Начальный
Причина, по которой мы пока не можем говорить о вашем коде фреймворка, заключается в том, что ваши объекты домена не могут напрямую входить в ваши компоненты. Как сказано в предыдущей статье, ваш домен должен оставаться защищенным.
Разрешение объектов предметной области в компонентах оставляет их открытыми для любых манипуляций во всех возможных местах. Вот так сложность становится неуправляемой.
Поскольку мы не предоставляем свободный доступ к объектам предметной области, архитектура предлагает вместо этого предоставить доступ к объектам View.
Просмотр объектов
Давайте углубимся в код (тут соус):
import type { Recipe } from "@/domain/recipe/Recipe"; import type { RecipeId } from "@/domain/recipe/types"; import type { Ingredient } from "@/domain/ingredient/Ingredient";
export class RecipeView { private constructor( public readonly id: RecipeId, public readonly name: string, public readonly ingredients: Ingredient[], public readonly instructions: string, public readonly portions: number, public readonly updatedAt: string ) {}
static fromDomain(recipe: Recipe) { const { id, name, ingredients, instructions, portions, updatedAt } = recipe.properties; return new RecipeView( id, name, ingredients, instructions, portions, updatedAt.toLocaleDateString() ); }
get mealSizeIcon() { switch (this.portions) { case 1: return "single"; case 2: return "couple"; case 3: return "family-one-kid"; default: return "family-two-kids"; } } }
Как видите, представление — это общедоступное представление объекта предметной области. У представлений не так много правил, как у объектов предметной области (на самом деле их нет). У вас гораздо больше свободы для реализации в представлениях того, что наиболее удобно для вас и вашей команды. Давайте посмотрим, что мы можем узнать из этого примера:
- Вы можете импортировать типы доменов и использовать их, например, для ввода машинописного кода по своему усмотрению.
- Конструктор также является приватным: его нужно создать с помощью метода
static fromDomain
. Причина, по которой мы передаем экземпляр домена и деконструируем свойства вместо того, чтобы просто передавать свойства, заключается в том, что экземпляры домена могут иметь геттеры, требуемые представлением, которых нет в свойствах. - Свойства представления не обязательно должны совпадать со свойствами доменного объекта. В этом примере мы преобразуем дату
updatedAt
в строку, которая будет отображаться в пользовательском интерфейсе. Хороший это выбор или нет, зависит исключительно от потребностей разрабатываемого вами приложения. - При необходимости вы можете расширить класс View геттерами или даже дополнительными свойствами. В этом примере я добавил геттер
mealSizeIcon
.
Помните ту проблему, которую мы упоминали в первой части этой статьи по поводу упрощения вашего компонента? То, что вы размещаете в представлениях, — это одна из вещей, которые помогут вам писать более тупые и простые компоненты. Во многих случаях это может быть более удобным решением, чем создание все более мелких компонентов (подробности см. в советах и рекомендациях ниже).
Возможно, теперь вам интересно, как мы должны поместить эти объекты View в компоненты? Или даже, где создаются экземпляры этих представлений? Если да, то вы на правильном пути.
Случаи использования
Если первичный — это то, что соединяет домен, вторичный и пользовательский интерфейс, то варианты использования — это дороги, благодаря которым происходит это соединение. Всякий раз, когда пользовательскому интерфейсу требуется доступ к чему-либо из домена, есть вариант использования для его предоставления. Давайте посмотрим, как это выглядит:
import type { RecipeRepository } from "@/domain/recipe/repository/RecipeRepository"; import type { UserId } from "@/domain/user/types"; import { RecipeView } from "@/primary/recipe/RecipeView";
export class GetRecipesUseCase { constructor(private readonly recipeRepository: RecipeRepository) {}
async execute(userId: UserId): Promise<RecipeView[]> { const recipes = await this.recipeRepository.getRecipes(userId);
return recipes.map(RecipeView.fromDomain); } }
"Источник."
В своей самой простой форме UseCase будет выполнять метод репозитория и возвращать в пользовательский интерфейс представления этих доменов. Здесь стоит отметить несколько вещей:
UseCases
создаются с помощью репозиториев, а не классов, которые их реализуют. (Это D в SOLID: зависит от абстракций, а не от конкретики).UseCases
имеют один общедоступный метод «выполнение», но могут иметь столько частных методов, сколько необходимо.- Это единственное место, где разрешено выполнение доменных методов. Запрос на изменение объектов домена происходит только в рамках вариантов использования.
UseCases
всегда возвращают Views (или void).
Несмотря на то, что в этом примере это не показано, варианты использования могут оказаться весьма набитыми логикой. Когда есть отношения между доменами, которые необходимо отобразить, UseCases
можно построить с таким количеством репозиториев, которое необходимо для его обработки. Это потому, что вы захотите спроектировать свои варианты использования так, чтобы они были максимально удобными для вашего пользовательского интерфейса. Это еще один способ сделать ваши компоненты тупыми и простыми.
Представьте, например, что вы хотите показать список рецептов, которые можно приготовить из сезонных ингредиентов. Для этого нам может потребоваться, чтобы IngredientRepository получил список ингредиентов текущего сезона, а затем вызывался бы RecipeRepository
с фильтром, содержащим эти ингредиенты. Вся эта логика может находиться в соответствующем GetSeasonalRecipes
варианте использования.
Другой распространенный пример: если ваше приложение требует, чтобы вы часто предоставляли идентификатор вошедшего в систему пользователя для запросов, вы можете решить, чтобы UseCase делал этот запрос для получения этого идентификатора (например, используя UserRepository), вместо того, чтобы компонент принимал заботиться об этом.
При наличии нескольких вариантов использования, требующих нескольких репозиториев, вам может потребоваться соблюдать осторожность, чтобы избежать циклических зависимостей.
Сервис или индекс варианта использования
Вы получите большое количество вариантов использования в каждом из ваших доменов. Таким образом, ваш каталог use-cases
должен содержать индекс всех вариантов использования, чтобы обеспечить легкий доступ ко всем вариантам использования домена. Этот индекс принимает форму класса, который мы назвали «Сервис». Вот как может выглядеть эта служба:
import type { RecipeRepository } from "@/domain/recipe/repository/RecipeRepository"; import type { RecipeToSave } from "@/domain/recipe/types"; import type { UserId } from "@/domain/user/types"; import type { UserRepository } from "@/domain/user/repository/UserRepository"; import { CreateRecipeUseCase } from "@/primary/recipe/use-cases/CreateRecipeUseCase"; import { GetRecipesUseCase } from "@/primary/recipe/use-cases/GetRecipesUseCase";
export class RecipeService { private getRecipesUseCase: GetRecipesUseCase; private createRecipeUseCase: CreateRecipeUseCase;
constructor( private readonly recipeRepository: RecipeRepository, private readonly userRepository: UserRepository ) { this.getRecipesUseCase = new GetRecipesUseCase(recipeRepository); this.createRecipeUseCase = new CreateRecipeUseCase( recipeRepository, userRepository ); }
async getRecipes(userId: UserId) { return await this.getRecipesUseCase.execute(userId); }
async createRecipe(form: RecipeToSave) { return await this.createRecipeUseCase.execute(form); } }
"Источник."
Как видите, Сервис просто индексирует функции «выполнения» вариантов использования. Таким образом, когда вы используете их в своей структуре, вам не нужно внедрять варианты использования один за другим.
Вы можете просто внедрить Services. Кроме того, гораздо приятнее иметь возможность вызывать recipeService.getRecipes(userId)
в своих компонентах, а не getRecipesUseCase.execute(userId)
.
Среднее
Во многих местах мы использовали интерфейсы Repositories
для ссылки на функции, которые должны предоставлять нам объекты предметной области. Нам еще предстоит реализовать ни один из этих репозиториев. Мы сделаем это дальше.
Ресурс
Вся связь вашего приложения с внешним миром реализуется классом Resource. И когда я говорю все, я имею в виду все. Когда компоненту нужны какие-то данные для отображения, ему все равно, поступают ли эти данные из REST API, GraphQL API, локального хранилища, хранилища управления состоянием, WebSockets, пограничных функций, firebase, supabase, файлов cookie, IndexedDB или чего-то еще. еще. Ресурс единственный, кто должен заниматься этими вопросами.
Ваш ресурс будет реализовывать репозиторий, определенный в домене. И он сделает все, что нужно для выполнения этого контракта. И вы, разработчик внешнего интерфейса, должны убедиться, что он делает это максимально эффективно. Однако вы заметите, что эта задача становится намного проще теперь, когда у вас есть только одно место, где вам нужно подумать об этом.
Начнем с простого примера:
import type { RecipeRepository } from "@/domain/recipe/repository/RecipeRepository"; import type { UserId } from "@/domain/user/types"; import type { Recipe } from "@/domain/recipe/Recipe"; import type { ApiRecipe } from "@/secondary/recipe/ApiRecipe"; import type { RestClient } from "@/secondary/RestClient";
export class RecipeResource implements RecipeRepository { constructor(private readonly restClient: RestClient) {}
async getRecipes(userId: UserId): Promise<Recipe[]> { const apiRecipes = await this.restClient.get<ApiRecipe[]>( `/users/${userId}/recipes` );
return apiRecipes.map((apiRecipe) => apiRecipe.toDomain()); }
async getFavoriteRecipes(userId: UserId): Promise<Recipe[]> { // }
async createRecipe(userId: UserId, form: RecipeToSave): Promise<Recipe> { // }
async updateRecipe(recipeId: RecipeId, form: RecipeToSave): Promise<Recipe> { // }
async deleteRecipe(recipeId: RecipeId): Promise<void> { // } }
Здесь важна часть RecipeResource implements RecipeRepository
. Вы будете видеть ошибку TypeScript в своей среде IDE, пока все методы в репозитории не будут реализованы в ресурсе.
В показанном здесь простом примере RecipeResource
извлекает информацию только из REST API. И поэтому он построен только с RestClient
(это просто обертка над выборкой).
Если позже мы захотим заменить fetch на Axios или какой-либо другой HTTP-клиент, мы сможем сделать это, не мешая репозиторию. Кроме того, если Ресурсу также потребуется подключиться, например, к GraphQL API, вы можете просто добавить свой собственный graphQLClient
в конструктор и использовать его.
API-адаптеры
Ответ, который предоставляет API, вряд ли будет точно соответствовать вашему домену во внешнем интерфейсе. Адаптер — это то, что позволит вам преобразовать ответ на объекты домена.
Вот пример:
export class ApiRecipe { constructor( public readonly id: RecipeId, public readonly name: string, public readonly ingredients: ApiIngredient[], public readonly instructions: string, public readonly portions: number, public readonly updatedAt: string ) {}
toDomain(): Recipe { const ingredients = this.ingredients.map((ingredient) => ingredient.toDomain() );
return Recipe.fromProperties({ id: this.id, name: this.name, ingredients: ingredients.map((ingredient) => ingredient.properties), instructions: this.instructions, portions: this.portions, updatedAt: new Date(this.updatedAt), }); } }
"Источник".
В этом примере мы преобразуем строку updatedAt
, отправленную API, в экземпляр Date, как и ожидалось доменом.
Клиент автоматически преобразует ответ API в экземпляры
ApiRecipe
. Вот почему мы можем безопасно ввести ответ сrestClient.get<ApiRecipe[]>
и использовать его вapiRecipes.map((apiRecipe) => apiRecipe.toDomain())
Магазин
Ваши компоненты не получают прямого доступа к библиотеке управления состоянием. Все данные, которые нужны компоненту, вызываются из сервиса, который вызывает вариант использования, который взаимодействует с репозиториями, реализованными ресурсами. Именно тогда ресурс взаимодействует с хранилищем от имени компонентов.
Давайте расширим наш предыдущий пример, чтобы увидеть, как это работает. Скажем, мы хотим сохранить рецепты в магазине при первом обращении к API. И мы также хотим избежать повторного обращения к API, если рецепты уже сохранены:
export class RecipeResource implements RecipeRepository { constructor( private readonly restClient: RestClient, private readonly store: RecipeStore ) {}
async getRecipes(userId: UserId): Promise<Recipe[]> { const recipesInStore = this.store.recipes; if (recipesInStore.length !== 0) { return recipesInStore.map(Recipe.fromProperties); }
const apiRecipes = await this.restClient.get<ApiRecipe[]>( `/users/${userId}/recipes` ); const recipes = apiRecipes.map((apiRecipe) => apiRecipe.toDomain()); this.store.saveRecipes(recipes.map((recipe) => recipe.properties));
return recipes; } // ... }
Для этого конструктору передается RecipeStore
. Метод getRecipes
теперь сначала проверяет, есть ли уже рецепты в магазине. Если это так, он возвращает объекты домена, создающие их экземпляры из сохраненных данных. Если сохраненных рецептов нет, то мы извлекаем их, а затем сохраняем в хранилище, чтобы при следующем вызове функции она вернула сохраненные рецепты. Вот об этом, действительно.
Как видите, при таком подходе никогда не будет вызова API в действиях вашего магазина. Все, что вы когда-либо будете делать с магазином, — это сохранять и извлекать данные, как и предполагалось.
Осторожно
Есть особый случай, когда наиболее удобным решением будет заставить ваш компонент напрямую реагировать на изменения в магазине. Я еще не уверен, как лучше всего это сделать, чтобы это соответствовало принципам этой архитектуры. Однако, по моему опыту, это действительно исключение. Вам нужно иметь 2 или более родственных компонента с взаимозависимыми данными, отображаемыми на одной странице.
В 99% случаев вам достаточно один раз получить данные из хранилища, а внутреннюю систему реактивности вашего фреймворка обработать все остальное через реквизиты, состояние компонента, события и т. д.
В любом случае, вероятно, есть совместимый способ сделать это (вероятно, используя механизм Pinia или Vuex subscribe
), но я пока не придумал. Если разберетесь, дайте знать!
UI
Наконец-то мы здесь! Теперь мы поговорим о коде фреймворка! У нас есть все, что нам нужно, чтобы вызвать силу нашего домена и использовать ее в наших компонентах. К сожалению, я не смогу рассказать вам, как лучше всего это сделать в каждом интерфейсном фреймворке, но я расскажу вам, как я научился делать это во Vue, и расскажу, что вам нужно предпринять. во внимание, чтобы принять лучшее решение в рамках вашего выбора.
Во Vue мы используем механизм Provide/Inject, чтобы дать компонентам доступ к нужным им сервисам. Вы можете либо предоставить использование на уровне приложения, либо предоставить на уровне компонента, если вы знаете, где в дереве компонентов это сделать.
import { createApp } from "vue"; import { createPinia } from "pinia";
import App from "./App.vue";
import { RecipeResource } from "@/secondary/recipe/RecipeResource"; import { RestClient } from "@/secondary/RestClient"; import { useRecipeStore } from "@/secondary/recipe/RecipeStore"; import { RecipeService } from "@/primary/recipe/use-cases"; import { UserResource } from "@/secondary/user/UserResource";
// Services const pinia = createPinia(); const restClient = new RestClient();
const userResource = new UserResource();
const recipeStore = useRecipeStore(); const recipeResource = new RecipeResource(restClient, recipeStore); const recipeService = new RecipeService(recipeResource, userResource);
// Setup const app = createApp(App);
app.use(pinia); app.provide<RecipeService>("recipeService", recipeService);
app.mount("#app");
Как видите, все ваши компоненты действительно нуждаются в доступе к службам.
После этого вы можете использовать их в компонентах на досуге:
export default defineComponent({ setup() { const recipeService = inject<RecipeService>("recipeService")!;
return { recipeService, }; },
data() { return { recipes: [] as RecipeView[], }; },
async created() { this.recipes = await this.recipeService.getRecipes("me"); }, });
Внедрение предпочтительнее простого импорта, поскольку оно значительно упрощает тестирование. Таким образом, вы можете легко создавать свои собственные фиктивные объекты и внедрять их в компоненты во время тестирования.
Таким образом, вы можете легко протестировать все возможные состояния данных, вводимые в ваши компоненты. Для React, возможно, использование Контекста для включения сервисов было бы правильным, но я позволю вам судить об этом.
Дополнительный
Если вы зашли так далеко, у вас уже есть все элементы, необходимые для того, чтобы начать собственное путешествие в доменно-ориентированной архитектуре внешнего интерфейса. Сейчас я просто поделюсь несколькими примечательными уроками, которые я усвоил в своем личном путешествии.
Диаграмма потока данных
Вот аккуратная диаграмма, чтобы запомнить основные идеи о том, как данные передаются в архитектуре:
Синим цветом обозначены объекты API, красным — объекты представления, а фиолетовым — объекты домена.
Данные могут передаваться в обоих направлениях (входящие и исходящие), но всегда должны идти по одному и тому же пути. Входящие данные из внешнего источника (да, в том числе из Магазина) принимают форму объектов API. Ресурс получает их, создает экземпляры домена и передает их варианту использования. Затем вариант использования возвращает объекты представления компонентам.
Обратите внимание, что домен занимает часть пространства вторичного и первичного. Это должно представить влияние домена в этих областях: ресурс реализует репозиторий домена, а варианты использования — это единственные места, где вызываются методы домена.
Секреты и уловки
Иногда самой сложной частью реализации этой архитектуры будет решить, что будет в домене, что в представлении, а что может законно остаться в компонентах. То, как вы думаете об этом вопросе применительно к внешнему коду, сильно отличается от того, как вы думаете об этом применительно к бэкенду.
Как узнать, что является частью Домена, а что нет
В книге Скотт Миллетт и Ник Тьюн просят нас подумать о самом важном в приложении, о той его части, которая действительно приносит пользу. Весь смысл DDD в том, чтобы создать условия, в которых разработчики могут сосредоточить свое внимание на этом.
Это здорово, и вы обязательно должны это сделать. Однако в повседневной жизни, когда вы получаете задачу по реализации новой функции и вам нужно решить, какие свойства и методы включить в вашу предметную модель, вам потребуется более конкретный ответ. Ответ будет варьироваться от компании к компании и от проекта к проекту. Что, если ваше решение делает особенным UX/UI, а не бизнес-аналитику, лежащую в его основе? Означает ли это, что свойства, связанные с UX/UI, перейдут в домен? Чтобы ответить на этот вопрос, нужно оказаться в таком положении.
Одна вещь, которую я усвоил (трудным путем), заключается в том, что я никогда не должен предполагать, что все, что предоставляет серверная часть, это то, что должно быть в модели предметной области впереди. Это упускает смысл этого упражнения. Как разработчик интерфейса, я призываю вас не бояться переосмысливать свои решения. Посмотрите, используются ли/где/как все свойства, которые вы получаете сзади. Внедряйте в свою доменную модель только то, что вам нужно для достижения ваших целей, и оставляйте все остальное.
Если позже кому-то понадобится добавить другие свойства в модель предметной области, чтобы расширить вашу функциональность, позвольте им это сделать, и это хорошо, потому что вы хотите, чтобы этот человек сам подумал о свойствах, которые ему понадобятся. Вам не нужно думать за него сейчас.
Лично я счел полезным думать, что если решение включить его в приложение исходит от владельца продукта (человека продукта, в идеале того, кто имеет полное представление о проблеме и решении), то оно обычно в Домене. Если решение исходит от дизайнера (или того, кто решает пользовательский интерфейс), то вы, вероятно, можете разместить его в представлении.
Как узнать, что поместить в представления, а что оставить для компонентов
Чтобы ответить на этот вопрос, нам, как разработчикам интерфейса, придется (снова) подумать о приложении в целом, но на этот раз с точки зрения UX/UI. Вопросы, которые могут у вас возникнуть, когда вы думаете о том, что разместить на домене, скорее всего, будут адресованы специалисту по продукту. Вопросы, которые могут у вас возникнуть при размышлении о том, что добавить в представление, скорее всего, будут адресованы UI/UX-дизайнеру.
Обратите внимание на предполагаемый дизайн и подумайте, какая информация и/или визуальные очереди информации всегда появляются вместе. Пример, который я использовал ранее в своем приложении кулинарной книги: дизайнер может решить, что каждый раз, когда отображается сложность рецепта, будет отображаться определенный значок, представляющий эту сложность. Если это всегда так, то это веский аргумент в пользу размещения ссылки на этот значок рядом со свойством сложности рецепта или вместе с ним. Еще один хороший пример, который я часто вижу, — это отображение статуса или состояния процесса (т. е. является ли рецепт черновиком, выполняется ли банковский перевод, отменена ли встреча). Обычно при передаче такой информации дизайнеры предпочитают всегда использовать определенные визуальные очереди. Вероятно, они могут быть включены в ваше представление. Элементы, относящиеся к одному компоненту, могут оставаться в компоненте.
Сила представления действительно проявляется, когда у компании есть последовательная система дизайна. Если это так, вы можете найти все ответы, которые вам нужны, просто пройдя и разобравшись в системе дизайна. Например, если в системе вы видите, что даты всегда отображаются в одном или двух форматах, вы можете включить эти форматы в свои представления. В этом случае компоненту не нужно преобразовывать данные в нужный формат.
Я не могу говорить достаточно высоко о ценности хорошо поддерживаемой системы проектирования для разработчиков интерфейса, реализующих эту архитектуру. Гибкость, которой вы и ваша команда можете достичь даже в очень сложных решениях, просто не имеет себе равных.
Везде столько занятий. Есть ли более функциональный способ реализовать это?
Я не эксперт в функциональном программировании, но да, я считаю, что есть. По правде говоря, я сам не большой любитель занятий (по крайней мере, не был). Единственное, что, по моему мнению, сильно выигрывает от наличия формы класса, — это модель предметной области.
Я считаю, что важно четко указать, какие свойства являются приватными и доступны только для чтения, а какие нет. Вероятно, вы сможете найти совместимые способы реализации остальной части архитектуры с помощью чистых функций, если вам это нужно. И если да, то напишите мне, я бы с удовольствием посмотрел!
Заключение
Я никогда не был так счастлив кодировать, как когда реализую эту архитектуру (или, по крайней мере, ее самые ценные уроки). Для меня это означало разницу между случайным написанием команд на экране до тех пор, пока они не сработают, и фактическим ощущением контроля над происходящим. Вот почему я пишу эту длинную статью. Если я могу помочь кому-то испытать такую же радость, я выполнил свою часть работы.
При этом эта архитектура, да и вообще DDD, не для всех и каждого проекта. Миллет и Тьюн говорят, что если вы создаете еще одно приложение для списка дел, то вам, вероятно, не нужен DDD. Я думаю, это потому, что в списке задач нет сложности. Любой разработчик может подойти к коду и все понять, потому что все знают, как работают списки дел. Однако если вы создаете уникальный список дел с функциями, которые делают его особенным для пользователей, то, возможно, учения DDD окажутся вам полезными.
Для меня основными учениями являются:
- Не тратьте время на размышления о том, как справиться с возрастающей сложностью вашей кодовой базы. Обсуждения, которые я провел с моей командой по этому вопросу (все они повлияли на мнение, выраженное в этой статье), были самыми полезными в моей карьере и лежат в основе лучшего кода, который я когда-либо видел. в состоянии производить до сих пор.
- Не тратится время, потраченное на размышления о домене. В доменно-ориентированной архитектуре рабочий день, связанный с внедрением новой сложной функции, выглядит для меня совершенно иначе, чем день без нее. В первом случае следуйте за мной в течение дня, и вы увидите, как я смотрю в потолок, хожу за кофе, разглядываю дизайн, совершаю короткие прогулки и разговариваю с членами команды, не написав ни строчки кода за 5 часов, просто думаю… и они возвращаются и пишут решение, которым я могу гордиться менее чем за 2 часа. В последнем вы, вероятно, увидите, как я пишу код в течение 2 дней, натыкаюсь на необъяснимые ошибки, ни с кем не разговариваю и предлагаю решение, с которым я просто рад, что мне больше не нужно иметь дело.
- В своей кодовой базе определите четкие обязанности, и когда вы заметите появление новых обязанностей, также разделите их.
- Защитите модель. Подобно тому, как в шаблоне проектирования Flux любые изменения в хранилище ограничиваются определенным образом, изменения в объектах предметной области также должны быть ограничены. Используйте доступные вам языковые функции, чтобы сообщить разработчику об этих (полезных) ограничениях.
- Пишите тесты. У меня нет возможности обсуждать здесь тестирование, но это большая и неотъемлемая часть DDD (и она должна быть одинаковой для кодирования любого серьезного приложения, независимо от его дизайна). Послушайте, как дядя Боб обсуждает себя здесь.
- Все проекты имеют архитектуру. Отказ от выбора архитектуры — это архитектурное решение. Такое решение всегда должно приниматься осознанно, взвешивая все за и против, и доводя до сведения всех мотивы окончательного решения. Если говорить о плюсах и минусах…
Плюсы и минусы этой архитектуры
В завершение, вот краткое изложение плюсов и минусов, которые я смог собрать из своего опыта.
Плюсы
- Помещает разработчиков в положение, в котором они должны думать о предметной области, что является просто другим способом сказать «думать о наилучшем возможном решении бизнес-проблемы».
- Создает поддерживаемый, организованный, надежный, тестируемый (и, надеюсь, протестированный) код.
- Более полезный опыт программирования и, следовательно, более полезный опыт работы (по крайней мере, лично).
Минусы
- Увеличение стоимости входа. Это особенно ново для большинства интерфейсов. Мне потребовалось довольно много времени, чтобы получить представление о том, как и почему все архитектурные решения, которые мне внезапно понадобилось начать учитывать при написании кода.
- Увеличенный шаблон. Какое-то время это было моей самой большой проблемой с этим конкретным вариантом доменной архитектуры для внешнего интерфейса. В моих личных проектах я принял определенные решения, которые помогли бы уменьшить количество шаблонов, но с тех пор я понял, что то, что я экономлю от написания меньшего количества шаблонов, не компенсирует несоответствия, которые появляются в кодовой базе из-за этих решений. В настоящее время я решаю проблему с шаблонами, создавая столько сниппетов и шаблонов файлов, сколько мне нужно, чтобы написать шаблон за меня, чтобы мне не приходилось этого делать.