![image](https://habrastorage.org/webt/nk/lv/yf/nklvyfdyfnfi1qugodvftzm4wb0.png)
В TDD-сообществе существует совет, который говорит о том, что мы не должны использовать mock-объекты для типов, которыми не владеем. Я считаю, что это хороший совет, и стараюсь следовать ему. Конечно, есть люди, которые говорят, что мы вообще не должны использовать mock-объекты. Независимо от того, какого мнения вы придерживаетесь, совет «не имитировать то, что не ваше» – содержит в себе еще и скрытый смысл. Люди часто пропускают его мимо ушей, видя слово «mock» и впадая в ярость.
Этот скрытый смысл заключается в том, что следует создавать интерфейсы, клиенты, мосты, адаптеры между нашим приложением и сторонним кодом, которым мы пользуемся. Будем ли мы создавать mock-объекты этих интерфейсов в наших тестах даже не так важно. Важно то, что мы создаем и используем интерфейсы, которые лучше отделяют наш код от стороннего. Классическим примером этого в мире PHP будет создание и использование HTTP клиента в нашем приложении, который использует Guzzle HTTP client, вместо использования Guzzle напрямую.
Почему? Хорошо, для начала, Guzzle имеет гораздо более мощный API, чем тот, который вашему приложению (в большинстве случаев) нужен. Создание своего HTTP клиента, который предоставляет только необходимый набор из API Guzzle, ограничит разработчиков приложения в том, что они смогут с этим клиентом сделать. Если API Guzzle изменится в будущем, нам надо будет необходимо внести изменения в одном месте, вместо того, чтобы исправлять его вызовы во всем приложении в надежде, что ничего не сломается. Две очень хорошие причины, и я даже не упомянул mock-объекты!
Я не думаю, что этого трудно достичь. Сторонний код обычно лежит в отдельной папке нашего приложения, часто это
vendor/
или library/
. Он также располагается в другом пространстве имен и имеет другое соглашение об именовании, чем то, что используется в нашем приложении. Сторонний код довольно легко определить и, с небольшой долей дисциплины, мы можем сделать код нашего приложения менее зависимым от сторонних частей.Что, если мы применим те же правила к устаревшему коду?
Что если мы будем смотреть на наш легаси-код, так же, как и на сторонний? Это может быть трудно сделать, или даже контрпродуктивно, если устаревший код используется исключительно в режиме поддержки, когда мы только правим баги и немного подстраиваем небольшие его части. Но если мы пишем новый код, который (пере)использует устаревший, я считаю, что стоит рассматривать его так же, как и сторонний код. По крайней мере с точки зрения нового кода.
Если возможно, устаревший и новый код должны располагаться в разных папках и пространствах имен. Прошло много времени с тех пор, как я последний раз видел систему без автозагрузки, так что это вполне выполнимо. Но вместо того, чтобы слепо использовать легаси-код в новом коде, что если мы сделаем интерфейсы для него и будем пользоваться ими?
Устаревший код часто полон «божественных» объектов, которые делают слишком много вещей. Они используют глобальное состояние, имеют публичные свойства или магические методы, которые дают доступ к приватным свойствам так, как будто они публичные, имеют статические методы, которые просто очень удобно вызывать кому угодно и откуда угодно. Так вот это самое удобство и привело нас к той ситуации, в которой мы находимся.
Другая, может даже более серьезная проблема с устаревшим кодом в том, что мы готовы изменять его, исправлять, взламывать его потому, что не рассматриваем его как сторонний код. Что мы делаем, когда видим баг или хотим добавить новую возможность в сторонний код? Мы описываем проблему и/или создаем pull request. То, что мы не делаем – это не идем в папку
vendor/
и не правим код там. Почему мы так делаем с устаревшим кодом? А потом скрещиваем пальцы и надеемся, что ничего не сломалось.Вместо того, чтобы слепо использовать устаревший код в новом коде, давайте попробуем написать интерфейсы, которые будут включать только требуемое подмножество API старого «божественного» объекта. Скажем, у нас есть объект
User
в устаревшем коде, который знает все обо всем. Он знает как изменять email и пароль, как повышать пользователей форума до модераторов, как обновлять публичные профили пользователей, устанавливает настройки уведомлений, сохраняет сам себя в базе и многое другое.src/Legacy/User.php
<?php
namespace Legacy;
class User
{
public $email;
public $password;
public $role;
public $name;
public function promote($newRole)
{
$this->role = $newRole;
}
public function save()
{
db_layer::save($this);
}
}
Это грубый пример, но отображает проблему: каждое свойство публичное и может быть легко изменено на любое значение, нам нужно помнить явно вызывать метод
save
после любого изменения для сохранения и т. д.Давайте ограничим сами себя и запретим обращаться к этим публичным свойствам и попробуем угадать, как устаревшая система работает при повышении прав пользователя:
src/LegacyBridge/Promoter.php
<?php
namespace LegacyBridge;
interface Promoter
{
public function promoteTo(Role $role);
}
src/LegacyBridge/LegacyUserPromoter.php
<?php
namespace LegacyBridge;
class LegacyUserPromoter implements Promoter
{
private $legacyUser;
public function __construct(Legacy\User $user)
{
$this->legacyUser = $user;
}
public function promoteTo(Role $newRole)
{
$newRole = (string) $newRole;
// Ты думал, что $role в устаревшей системе это строка? Угадай теперь!
$legacyRoles = [
Role::MODERATOR => 1,
Role::MEMBER => 2,
];
$newLegacyRole = $legacyRoles[$newRole];
$this->legacyUser->promote($newLegacyRole);
$this->legacyUser->save();
}
}
Теперь, когда мы хотим повысить права
User
в новом коде мы используем интерфейс LegacyBridge\Promoter
, который имеет дело со всеми тонкостями повышения пользователя в устаревшей системе.Изменение языка наследия
Интерфейс для устаревшего кода дает нам возможность улучшить дизайн системы и может избавить нас от возможных ошибок в именовании, которые были сделаны давно. Процесс изменения роли пользователя с модератора на участника это не «promotion» (повышение), а скорее «demotion» (понижение). Никто не мешает нам создать два интерфейса для этих разных вещей, даже если устаревший код будет выглядеть так же.
src/LegacyBridge/Promoter.php
<?php
namespace LegacyBridge;
interface Promoter
{
public function promoteTo(Role $role);
}
src/LegacyBridge/LegacyUserPromoter.php
<?php
namespace LegacyBridge;
class LegacyUserPromoter implements Promoter
{
private $legacyUser;
public function __construct(Legacy\User $user)
{
$this->legacyUser = $user;
}
public function promoteTo(Role $newRole)
{
if ($newRole->isMember()) {
throw new \Exception("Can't promote to a member.");
}
$legacyMemberRole = 2;
$this->legacyUser->promote($legacyMemberRole);
$this->legacyUser->save();
}
}
src/LegacyBridge/Demoter.php
<?php
namespace LegacyBridge;
interface Demoter
{
public function demoteTo(Role $role);
}
src/LegacyBridge/LegacyUserDemoter.php
<?php
namespace LegacyBridge;
class LegacyUserDemoter implements Demoter
{
private $legacyUser;
public function __construct(Legacy\User $user)
{
$this->legacyUser = $user;
}
public function demoteTo(Role $newRole)
{
if ($newRole->isModerator()) {
throw new \Exception("Can't demote to a moderator.");
}
$legacyModeratorRole = 1;
$this->legacyUser->promote($legacyModeratorRole);
$this->legacyUser->save();
}
}
Не такое уж большое изменение, но цель кода стала гораздо яснее.
Теперь, когда вам в следующий раз понадобится вызвать некоторые методы из устаревшего кода, попробуйте сделать интерфейс для них. Это может быть невыполнимо, это может быть слишком дорого. Я знаю, что статический метод этого «божественного» объекта и правда очень просто использовать и с его помощью можно выполнить работу гораздо быстрее, но хотя бы рассмотрите такой вариант. Вы просто сможете немного улучшить дизайн новой системы, которую создаете.