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

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

ДДД

Проектирование, ориентированное на предметную область (DDD), было задумано и популяризировано Эриком Эвансом, чья книга Дизайн, управляемое предметной областью: борьба со сложностью в основе программного обеспечения, опубликованная в 2003 году. Это превосходно, и есть много других замечательных DDD. материал онлайн. Большинство дискуссий на сегодняшний день относится к приложениям, созданным в объектно-ориентированном стиле. И во многих вступительных материалах по DDD основное внимание уделяется терминологии: домены, границы доменов, объекты доменов и т. д. Легко зациклиться на всех задействованных формулировках, и не сразу понятно, как шаблоны DDD будут выглядеть в современный JS-код. Где вы размещаете объекты домена в приложении реагирования? Что следует считать событием домена?

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

Вот как я бы перефразировал основы DDD на практике:

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

2. Создайте простейшую реализацию, которая моделирует эти взаимно идентифицированные требования. Минимизируйте когнитивную нагрузку.

3. Будьте внимательны при проектировании частей системы, ориентированных на бизнес.

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

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

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

«Вот что будет делать эта штука, шаг за шагом, по порядку. Вот ожидаемый ввод, вот формат вывода. Это когда он будет работать и как часто. Это то, за что он не несет ответственности…”

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

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

2. Создайте простейшую реализацию, которая моделирует эти взаимно определенные требования. Минимизируйте когнитивную нагрузку.

Это означает предпочтение простоте и ясности, а не:

  • Возможность повторного использования
  • Общие практики
  • Будущие требования
  • Практики, предложенные сопровождающими фреймворка
  • Как вы сделали это в прошлый раз

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

Держите код платформы (повторно используемый код для выполнения различных подзадач) отдельно от кода бизнес-логики (что система должна делать для ваших пользователей/заинтересованных сторон). Код вашей платформы должен отражать отсутствие в нем бизнес-контекста, возможность повторного использования. Он должен быть более или менее доступным для копирования и вставки в несвязанный проект JS.

import cloudAuthService from '../services/auth-service';

class Authentication {
  async login() {
    const token = await cloudAuthService.login();
    return { token };
  }
  async logout() {
    await authService.logout();
  }
  async refreshToken(existingToken) {
    const { token } = await cloudAuthService.refresh(existingToken);
    return { token };
  }
}

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

Ваш код бизнес-логики — все остальное, относящееся к вашему приложению; что он рендерит, как называется API, код взаимодействия с пользователем, авторизация и идентификация — все должно быть написано так, чтобы отражать, что это такое и что оно делает. Он должен быть конкретным, он должен ссылаться на константы, связанные с бизнесом. Большая часть этого кода не будет использоваться повторно, и повторное использование не должно быть приоритетом при работе с кодом бизнес-логики. Даже когда использование совместно используется в этих частях вашей кодовой базы, сохраняйте (путем именования и т. д.) отдельные домены, которые у вас есть. Не позволяйте общим техническим ресурсам создавать ненужные ограничения для отдельных доменов бизнеса.

import { getRandomNum } from '../utils';
import { COMBAT_ACTION_ATTACK } from '../constants/actions/combat';

const chooseOpponentsAction = (attacks) => {
 const attackList = Object.keys(attacks);
 // @todo this check could probably be handled earlier on, and we trust here
 if (attackList.length === 0) {
   throw new ReferenceError("opponent has no attacks");
 }

 const attackName = attackList[getRandomNum(attackList.length - 1)];

 return {
   type: COMBAT_ACTION_ATTACK,
   attack: attacks[attackName],
  };
}

Приведенный выше код представляет собой некоторую бизнес-логику из моего проекта на стороне игрового движка. Он случайным образом выбирает одну атаку из списка атак противника, чтобы использовать ее на следующем ходу в бою. Эта функция инкапсулирует поведение и знание бизнес-модели/контекста. У него также есть (достаточно) четко определенный контракт в формате, специфичном для приложения: объект атаки в этом случае имеет type и attack . Границы этой функции хорошо нарисованы, ее можно полностью переписать (и так и будет, это очень грубо) без какого-либо влияния на остальную часть приложения.

3. Будьте преднамеренны в проектировании частей системы, ориентированных на бизнес.

Обязанности, которые вы возлагаете на свои модули/классы/функции, заключаются в том, как вы создаете «домены» и линии между ними. Позвольте вашему коду четко и полностью моделировать потребности бизнеса. Минимизируйте время и внимание, затрачиваемое на части системы, которые не являются критически важными для бизнеса (например, используйте устаревший код, сторонние библиотеки).

Затем? Меняй, постоянно. Если вам нужно повторно использовать этот код, который вы написали в другом месте, затем перепишите его, чтобы сделать его пригодным для повторного использования. Но не раньше. Мы часто пытаемся написать реализацию «правильным образом» с первого раза, но впервые прикасаемся к какому-либо коду, когда знаем о нем меньше всего. Наше понимание растет на протяжении всего проекта. И, конечно же, фактические требования часто меняются в процессе разработки проекта. Существующий код был правильным решением, когда он был написан. Теперь, день спустя или год спустя, ваши потребности изменились — рефакторинг — это естественно, не стесняйтесь.

// later refactor of auth client
import authService from '../services/auth-service';
import { redirect } from '../router';
import madeUpAppleSDK from '../auth/external/madeupapple';

const LOGOUT_URL = '/logout/';
const AUTH_TYPES = [
  'google',
  'madeupapple'
];

class Authentication {
  async login({
    authType,
    redirectUrl
  }) {
    if (!AUTH_TYPES.includes(authType)) {
      throw new TypeError(`Invalid auth type ${authType}`);
    }
    let token = null;
    if (authType === 'google') {
      token = await authService.login();
    } else {
      token = await madeUpAppleSDK.login();
    
    if (redirectUrl) {
      redirect(redirectUrl)
    }

    return { token };
  }

  async logout() {
    await authService.logout();
    redirect(LOGOUT_URL);
  }

  async refreshToken(existingToken) {
    const { token } = await authService.refresh(existingToken);
    return { token };
  }
}
export default new Authentication();

Приведенный выше код демонстрирует промежуточный рефакторинг предыдущего примера и включает дополнительные сведения о поведении продукта. Иногда авторизация должна быть перенаправлена ​​​​после входа в систему, в других случаях нет. Он всегда перенаправляет после выхода из системы. Теперь существует два типа аутентификации. Логика определения типа авторизации проста, если/то, но только с двумя типами, этого пока достаточно. Этот рефакторинговый класс делает гораздо больше для нашего приложения, но он также более субъективен в отношении того, как выглядит поведение при входе в систему и выходе из нее. По мере того, как мы делаем наш код более конкретным, поведение и ограничения, которые мы реализуем, определяют модель нашей системы.

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

Красные флажки плохо смоделированного кода

  • Сложные изменения. Когда скромное изменение бизнес-функции требует больших/неудобных изменений в программном обеспечении, это сигнал о том, что система плохо моделирует бизнес. Это происходит на каждом уровне кодовой базы, от чрезмерно самоуверенных служебных функций до неудобной архитектуры базы данных.
  • Не уверен, что произойдет. Не совсем ясно, где используется фрагмент кода или каковы будут все последствия его выполнения. Системы слишком сильно зависят от публикации/подписки событий, что затрудняет сбор воедино поведения во время выполнения.
  • Дополнительные уровни элегантности. Вместо жестко заданных значений где-то есть объект карты и поиск. В приложение встроен небольшой мини-API, но он всегда используется только одним способом. Чтобы понять поведение отдельного файла, вы должны прочитать большое количество других идиосинкразических абстракций в кодовой базе. Все признаки чрезмерной разработки или, по крайней мере, предварительной разработки.

Как выглядит успех DDD

Этот список, безусловно, не является исчерпывающим:

  • Согласованность. Содержимое данной функции, класса или модуля имеет одну цель. Код не пытается сделать слишком много, и это не мешанина из плохо определенных действий/взаимодействий. Обязанности разделены четко и разумно.
  • Нет побочных эффектов. Функции, методы и даже конфигурации написаны для реализации одного поведения. У них нет скрытых зависимостей, и они не вызывают эффектов, выходящих за рамки их (самоочевидной) цели.
  • Код прост для понимания. Когда вы просматриваете его для отладки проблемы или добавления функциональности, вы можете быстро найти нужный код. Источники данных ясны, поток управления понятен. Это окупается каждый раз, когда инженер просматривает код по любой причине.
  • Интуитивно понятный. Изменения в приложении могут быть реализованы плавно, потому что приложение построено «так, как вы ожидаете». Реализация в коде не создает предположений или ограничений, которых нет в реальном мире. И предположения/ограничения, которые существуют в бизнес-функции, применяются в системе.

В итоге

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

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

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