Какой он, код надежного приложения?

Перевод статьи Daniel Oliveira о том, как писать код надежного приложения, используя подход Роберта К. Мартина.

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

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

Архитектура программного обеспечения предлагает модели и правила для определения структур (например, классов, интерфейсов и структур) в системе и того, как они взаимодействуют друг с другом. Эти правила способствуют повторному использованию и разделению задач этих элементов. Это позволяет легко изменять детали реализации, такие как СУБД или интерфейсная библиотека. Рефакторинг и исправление ошибок имеют минимальное влияние. А добавление новых функций становится простым.

В этой статье я объясню модель архитектуры, предложенную в 2012 году Робертом К. Мартином, дядей Бобом. Он является автором такой классики как «Чистый код» и «Идеальный программист». В октябре этого года он выпустит еще одну книгу « Чистая архитектура».

Модель имеет то же название, что и книга, и она построена на простых понятиях:

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

Постепенно мы будем изучать каждый слой более подробно. В качестве примера мы будем использовать знакомое нам приложение: счетчики. Мы не потратим времени на его изучение, поэтому сможем сфокусировать на предмете статьи.
Вы можете найти демо-версию надежного приложения здесь, а примеры кода будет TypeScript. Некоторые из приведенных ниже частей кода написаны на React и Redux. Знания этих решений могут помочь в их понимании. Тем не менее концепции чистой архитектуры гораздо более универсальны. Вы поймете это даже без предварительного ознакомления с указанными инструментами.

Сущности

Сущности изображены на диаграмме как корпоративные бизнес-правила. Они включают в себя бизнес-правила, универсальные для компании. Сущности представляют собой объекты, которые являются основными для своей области деятельности. Они являются компонентами с самым высоким уровнем абстракции.
В нашем примере есть очень очевидная сущность: Counter.


export type Counter = number;

export function createCounter(value: number = 0): Counter {
  return value;
}

export function increment(counter: Counter): Counter {
  return createCounter(counter + 1);
}

export function decrement(counter: Counter): Counter {
  return createCounter(counter - 1);
}

Use cases - прецеденты

Прецеденты представлены в диаграмме как бизнес правила надежного приложения. Они представляют все варианты его использования. Каждый элемент этого слоя обеспечивает интерфейс к внешнему слою и действует как хаб, который взаимодействует с другими частями системы. Они отвечают за полное выполнение прецедентов и обычно называются Interactors.
В нашем примере прецендентами являтся incrementing или decrementing нашего counter:


import { CounterGateway } from './CounterGateway';

export interface ChangeCounterInteractor {
  increment(): void;
  decrement(): void;
}

export function createChangeCounterInteractor(counterGateway: CounterGateway): ChangeCounterInteractor {
  return {
    increment() {
      counterGateway.increment();
    },

    decrement() {
      counterGateway.decrement();
    }
  };
}

Обратите внимание, что функция ChangeCounterInteractor принимает параметр типа CounterGateway. Мы обсудим его позже. Но пока можно сказать, что Gateways - это то, что стоит между прецендатами и следующим слоем.

Адаптеры интерфейса

Этот уровень состоит из границы между бизнес-правилами системы и инструментами, которые позволяют ему взаимодействовать с внешним миром, например, с базами данных и графическими интерфейсами. Элементы этого слоя действуют как посредники, получая данные из одного уровня и передавая их вперёд другому, адаптируя данные по мере необходимости.
В нашем примере у нас есть несколько интерфейсных адаптеров. Одним из них является компонент React, который представляет counter и его элементы управления для increment и decrement:


import * as React from 'react';
import { CounterViewData } from '../CounterViewData';
import { ChangeCounterInteractor } from '../../../useCases/ChangeCounterInteractor';

interface CounterWidgetProps {
  counter: CounterViewData;
  changeCounterInteractor: ChangeCounterInteractor;
}

export const CounterWidget = (props: CounterWidgetProps) => {
  const { counter, changeCounterInteractor } = props;

  return (

{counter.value}

); };

Обратите внимание, что компонент не использует экземпляр Counter для представления его значения, а вместо него CounterViewData. Мы сделали это изменение, чтобы отделить представление логики от бизнес-данных. Примером этого является логика выставки счетчика на основе режима просмотра (римские или индусско-арабские цифры). Реализация CounterViewData:


import { Counter } from '../../entities/Counter';
import { ViewMode } from '../../entities/ViewMode';
import romanize from '../../utils/romanize';

export interface CounterViewData {
  value: string;
  viewModeButtonText: string;
}

export function createCounterViewData (counter: Counter, viewMode: ViewMode): CounterViewData {
  return {
    get value(): string {
      switch (viewMode) {
        case ViewMode.regular:
          return counter.toString();
        case ViewMode.roman:
          return romanize(counter);
        default:
          throw new Error('Unexpected view mode');
      }
    },

    get viewModeButtonText(): string {
      switch (viewMode) {
        case ViewMode.regular:
          return 'Change to roman numbers';
        case ViewMode.roman:
          return 'Change to Hindu-Arabic numbers';
        default:
          throw new Error('Unexpected view mode');
      }
    }
  };
}

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

Фреймворки и драйверы

Инструменты, используемые вашей системой для взаимодействия с внешним миром, составляют самый внешний слой. Обычно мы не пишем код в этом слое, который содержит такие библиотеки, как React / Redux, API-интерфейсы браузера и т. д.

Правило зависимости

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

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

Например, элемент в слое «Use cases» не может знать ничего о каком-либо классе или модуле, связанном с GUI или постоянством данных. Аналогично, Сущность не может знать в каких Use cases она используется.
Это правило может вызвать у вас вопросы. Например, возьмите use case. Он запускается в результате взаимодействия пользователя с интерфейсом. Его выполнение включает обновление в некоторых постоянных хранилищах данных, таких как база данных. Как Interactor может выполнять соответствующие вызовы в процедурах обновления, не зависимо от адаптера интерфейса, который отвечает за сохранение данных?


Ответ находится в элементе, который мы упоминали ранее: Gateways. Они отвечают за установление интерфейсов, необходимых для работы Use cases. После того как интерфейс установлен, Адаптеры интерфейса должны выполнить свою часть договора, как показано на диаграмме выше. У нас есть интерфейс CounterGateway и реализация с использованием Redux:


export interface CounterGateway {
  increment(): void;
  decrement(): void;
}

import { Dispatch } from 'redux';

import { CounterGateway } from '../../useCases/CounterGateway';
import { increment, decrement } from './actions';

export function createReduxCounterGateway (dispatch: Dispatch): CounterGateway {
  return {
    increment () {
      dispatch(increment());
    },

    decrement () {
      dispatch(decrement());
    }
  };
}

Возможно, это вам не понадобится

Конечно, это приложение-пример было несколько сложнее для надежного приложения счетчика increment / decrement. И я хотел бы пояснить, что вам не нужно все это для небольшого проекта или прототипа. Но поверьте мне, по мере того, как ваше приложение станет больше, вы захотите максимизировать повторное использование и управляемость. Хорошая архитектура программного обеспечения делает проекты устойчивыми к прогрессу.

Хорошо ... И что?

В этой статье мы раскрыли подход к развязке сущностей наших систем. Это упрощает их управляемость и расширение. Например, для создания того же надежного приложения с использованием Vue.js нам нужно будет только переписать компоненты CounterPage и CounterWidget. Исходный код примера приложения приведен здесь.

Другие материалы по теме написания кода надежного приложения:

6 простых советов по написанию чистого кода

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ