Статьи

Рефакторинг устаревшего кода: Часть 8. Инвертирование зависимостей для чистой архитектуры

Старый код Гадкий код. Сложный код. Код спагетти. Глупый бред. В двух словах, Кодекс Наследия . Это серия, которая поможет вам работать и справляться с этим.

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

Это то, что мы видели в наших статьях и учебниках. Чистая архитектура.

На высоком уровне это похоже на схему выше, и я уверен, что вы уже знакомы с ней. Это предложенное архитектурное решение Роберта С. Мартина.

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

Затем есть несколько других типов модулей или классов вокруг нашей бизнес-логики. Их можно рассматривать как простые вспомогательные вспомогательные модули. У них разные цели, и большинство из них незаменимы. Они обеспечивают связь между пользователем и нашим приложением через механизм доставки. В нашем случае это интерфейс командной строки. Существует еще один набор вспомогательных классов, которые соединяют нашу бизнес-логику с нашим уровнем персистентности и со всеми данными на этом уровне, но в нашем приложении такого уровня нет. Затем есть классы помощи, такие как фабрики и строители, которые строят и предоставляют новые объекты для нашей бизнес-логики. Наконец, есть классы, представляющие точку входа в нашу систему. В нашем случае GameRunner можно считать таким классом, или все наши тесты также являются точками входа по-своему.

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

A. Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
Б. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Вот и все, последний принцип SOLID и, вероятно, тот, который больше всего влияет на ваш код. Это довольно просто для понимания и довольно просто для реализации.

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

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

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

GameRunner , используя функции из RunnerFunctions.php , создает класс Game а затем использует его. С другой стороны, наш класс Game , представляющий нашу бизнес-логику, создает и использует объект Display .

Итак, бегун зависит от нашей бизнес-логики. Это верно. С другой стороны, наша Game зависит от Display , что не очень хорошо. Наша бизнес-логика никогда не должна зависеть от нашей презентации.

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

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

Интерфейс — это специальный тип, который допускает только определение методов и переменных. Это самая абстрактная конструкция в объектно-ориентированном программировании. Любая реализация всегда должна реализовывать все методы своего родительского интерфейса. Конкретный класс может реализовывать несколько интерфейсов.

За исключением объектно-ориентированных языков семейства C, другие, такие как Java или PHP, не допускают множественного наследования. Таким образом, конкретный класс может расширять один абстрактный класс, но он может реализовывать несколько интерфейсов, даже в случае необходимости. Или, с другой стороны, один абстрактный класс может иметь много реализаций, в то время как многие интерфейсы могут иметь много реализаций.

Для более полного объяснения DIP, пожалуйста, прочитайте учебник, посвященный этому принципу SOLID .

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

Рассматривая список методов Display , мы видим 12 открытых методов, включая конструктор. Это довольно большой интерфейс, вы должны держать это число как можно ниже, выставляя интерфейсы так, как они нужны клиентам. Принцип интерфейса разделения имеет несколько хороших идей по этому поводу. Возможно, мы попытаемся решить эту проблему в следующем уроке.

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

Таким образом, вместо Game зависимости от более конкретного Display , они оба зависят от очень абстрактного интерфейса. Game используется интерфейс, а Display его реализует.

Фил Карлтон сказал: «В компьютерных науках есть только две сложные вещи: аннулирование кэша и именование».

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

В старые времена венгерской нотации мы бы так и сделали.

Для этой диаграммы мы использовали фактические имена классов / файлов и фактическую прописную букву. Интерфейс называется «IDisplay» с заглавной буквой «I» перед «Display». На самом деле были языки программирования, требующие такого именования интерфейсов. Я уверен, что есть несколько читателей, все еще использующих их и улыбающихся прямо сейчас.

Проблема с этой схемой именования — неуместная проблема. Интерфейсы принадлежат их клиентам. Наш интерфейс принадлежит Game . Таким образом, Game не должна знать, использует ли она интерфейс или реальный объект. Game не должна беспокоиться о реализации, которую она действительно получает. С точки зрения Game , он просто использует «Display», вот и все.

Это решает проблему именования Game to Display . Использование суффикса «Impl» для реализации несколько лучше. Это помогает устранить беспокойство из Game .

Это также намного более эффективно для нас. Думайте об Game как она выглядит прямо сейчас. Он использует объект Display и знает, как его использовать. Если мы назовем наш интерфейс «Display», мы уменьшим количество изменений, необходимых в Game .

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

Теперь это значительно лучше. Наша реализация была названа «CLIDisplay», так как она выводится в CLI. Если нам нужен вывод HTML или интерфейс рабочего стола Windows, мы можем легко добавить все это в нашу архитектуру.

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

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

Есть такой способ. В тестировании есть понятие, называемое «Пересмешка».

Википедия определяет Mocking как таковое: «В объектно-ориентированном программировании фиктивные объекты — это симулированные объекты, которые имитируют поведение реальных объектов контролируемыми способами».

Такой объект очень помог бы нам. На самом деле, нам даже не нужно что-то настолько сложное, как имитация всего поведения. Все, что нам нужно, это поддельный, глупый объект, который мы можем отправить в Game вместо реальной логики отображения.

Давайте создадим интерфейс под названием Display со всеми открытыми методами текущего конкретного класса.

Как вы можете видеть, старый Display.php был переименован в DisplayOld.php . Это всего лишь временный шаг, который позволяет нам убрать его с пути и сосредоточиться на интерфейсе.

1
2
3
interface Display {
 
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
interface Display {
    function statusAfterRoll($rolledNumber, $currentPlayer);
    function playerSentToPenaltyBox($currentPlayer);
    function playerStaysInPenaltyBox($currentPlayer);
    function statusAfterNonPenalizedPlayerMove($currentPlayer, $currentPlace, $currentCategory);
    function statusAfterPlayerGettingOutOfPenaltyBox($currentPlayer, $currentPlace, $currentCategory);
    function playerAdded($playerName, $numberOfPlayers);
    function askQuestion($currentCategory);
    function correctAnswer();
    function correctAnswerWithTypo();
    function incorrectAnswer();
    function playerCoins($currentPlayer, $playerCoins);
}

Да. Интерфейс — это просто набор объявлений функций. Представьте это как файл заголовка C. Никаких реализаций, только объявления. Он не может содержать реализацию вообще. Если вы попытаетесь реализовать какой-либо из методов, это приведет к ошибке.

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

1
Fatal error: Cannot instantiate interface Display

Это потому, что Game пытается создать новое отображение самостоятельно в строке 25 в конструкторе.

Мы знаем, что не можем этого сделать. Интерфейс или абстрактный класс не могут быть созданы. Нам нужен реальный объект.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class DummyDisplay implements Display {
 
    function statusAfterRoll($rolledNumber, $currentPlayer) {
        // TODO: Implement statusAfterRoll() method.
    }
 
    function playerSentToPenaltyBox($currentPlayer) {
        // TODO: Implement playerSentToPenaltyBox() method.
    }
 
    function playerStaysInPenaltyBox($currentPlayer) {
        // TODO: Implement playerStaysInPenaltyBox() method.
    }
 
    function statusAfterNonPenalizedPlayerMove($currentPlayer, $currentPlace, $currentCategory) {
        // TODO: Implement statusAfterNonPenalizedPlayerMove() method.
    }
 
    function statusAfterPlayerGettingOutOfPenaltyBox($currentPlayer, $currentPlace, $currentCategory) {
        // TODO: Implement statusAfterPlayerGettingOutOfPenaltyBox() method.
    }
 
    function playerAdded($playerName, $numberOfPlayers) {
        // TODO: Implement playerAdded() method.
    }
 
    function askQuestion($currentCategory) {
        // TODO: Implement askQuestion() method.
    }
 
    function correctAnswer() {
        // TODO: Implement correctAnswer() method.
    }
 
    function correctAnswerWithTypo() {
        // TODO: Implement correctAnswerWithTypo() method.
    }
 
    function incorrectAnswer() {
        // TODO: Implement incorrectAnswer() method.
    }
 
    function playerCoins($currentPlayer, $playerCoins) {
        // TODO: Implement playerCoins() method.
    }
}

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

Теперь давайте использовать его в Game , инициализируя его в конструкторе.

1
2
3
4
5
6
7
8
9
function __construct() {
 
    $this->players = array();
    $this->places = array(0);
    $this->purses = array(0);
    $this->inPenaltyBox = array(0);
 
    $this->display = new DummyDisplay();
}

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

1
2
3
4
5
6
7
8
9
function __construct(Display $display) {
 
    $this->players = array();
    $this->places = array(0);
    $this->purses = array(0);
    $this->inPenaltyBox = array(0);
 
    $this->display = $display;
}

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

1
2
3
function setUp() {
    $this->game = new Game(new DummyDisplay());
}

Вот и все. Нам нужно было изменить одну строку в наших модульных тестах. В настройках мы отправим в качестве параметра новый экземпляр DummyDisplay . Это внедрение зависимости. Использование интерфейсов и внедрение зависимостей особенно полезно, если вы работаете в команде. Мы в Syneto заметили, что указание типа интерфейса для класса и его внедрение поможет нам лучше понять намерения клиентского кода. Любой, кто смотрит на клиента, знает, какой тип объекта используется в параметрах. И классным бонусом является то, что ваша IDE будет автоматически завершать методы для этих параметров, потому что она может определять их типы.

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

1
2
3
class CLIDisplay implements Display {
    // … //
}

Переименуйте его в CLIDisplay и сделайте так, чтобы он реализовал Display .

01
02
03
04
05
06
07
08
09
10
11
12
function run() {
    $display = new CLIDisplay();
    $aGame = new Game($display);
    $aGame->add(«Chet»);
    $aGame->add(«Pat»);
    $aGame->add(«Sue»);
 
    do {
        $dice = rand(0, 5) + 1;
        $aGame->roll($dice);
    } while (!didSomebodyWin($aGame, isCurrentAnswerCorrect()));
}

В RunnerFunctions.php в функции run() создайте новый экран для CLI и передайте его Game когда он будет создан.

Раскомментируйте и выполните ваши золотые мастер-тесты Они пройдут.

Это решение эффективно приводит к архитектуре, как на схеме ниже.

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

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