Главная » Хабрахабр » [Перевод] Устаревший код – сторонний код

[Перевод] Устаревший код – сторонний код

image

В TDD-сообществе существует совет, который говорит о том, что мы не должны использовать mock-объекты для типов, которыми не владеем. Я считаю, что это хороший совет, и стараюсь следовать ему. Конечно, есть люди, которые говорят, что мы вообще не должны использовать mock-объекты. Независимо от того, какого мнения вы придерживаетесь, совет «не имитировать то, что не ваше» – содержит в себе еще и скрытый смысл. Люди часто пропускают его мимо ушей, видя слово «mock» и впадая в ярость.
Этот скрытый смысл заключается в том, что следует создавать интерфейсы, клиенты, мосты, адаптеры между нашим приложением и сторонним кодом, которым мы пользуемся. Будем ли мы создавать mock-объекты этих интерфейсов в наших тестах даже не так важно. Важно то, что мы создаем и используем интерфейсы, которые лучше отделяют наш код от стороннего. Классическим примером этого в мире PHP будет создание и использование HTTP клиента в нашем приложении, который использует Guzzle HTTP client, вместо использования Guzzle напрямую.

Хорошо, для начала, Guzzle имеет гораздо более мощный API, чем тот, который вашему приложению (в большинстве случаев) нужен. Почему? Если API Guzzle изменится в будущем, нам надо будет необходимо внести изменения в одном месте, вместо того, чтобы исправлять его вызовы во всем приложении в надежде, что ничего не сломается. Создание своего HTTP клиента, который предоставляет только необходимый набор из API Guzzle, ограничит разработчиков приложения в том, что они смогут с этим клиентом сделать. Две очень хорошие причины, и я даже не упомянул mock-объекты!

Сторонний код обычно лежит в отдельной папке нашего приложения, часто это vendor/ или library/. Я не думаю, что этого трудно достичь. Сторонний код довольно легко определить и, с небольшой долей дисциплины, мы можем сделать код нашего приложения менее зависимым от сторонних частей. Он также располагается в другом пространстве имен и имеет другое соглашение об именовании, чем то, что используется в нашем приложении.

Что, если мы применим те же правила к устаревшему коду?

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

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

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

Что мы делаем, когда видим баг или хотим добавить новую возможность в сторонний код? Другая, может даже более серьезная проблема с устаревшим кодом в том, что мы готовы изменять его, исправлять, взламывать его потому, что не рассматриваем его как сторонний код. То, что мы не делаем – это не идем в папку vendor/ и не правим код там. Мы описываем проблему и/или создаем pull request. А потом скрещиваем пальцы и надеемся, что ничего не сломалось. Почему мы так делаем с устаревшим кодом?

Скажем, у нас есть объект User в устаревшем коде, который знает все обо всем. Вместо того, чтобы слепо использовать устаревший код в новом коде, давайте попробуем написать интерфейсы, которые будут включать только требуемое подмножество API старого «божественного» объекта. Он знает как изменять email и пароль, как повышать пользователей форума до модераторов, как обновлять публичные профили пользователей, устанавливает настройки уведомлений, сохраняет сам себя в базе и многое другое.

src/Legacy/User.php

<?php
namespace Legacy;
class User
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(); }
}

Не такое уж большое изменение, но цель кода стала гораздо яснее.

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Спустя пять лет вышла очередная версия DOSBox под номером 0.74-2

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

Данные пользователей Windows на ПК с поддержкой сенсорного ввода пишутся в отдельный файл

Это сделано для удобства пользователя и ускорения процесса его работы. Большое количество моделей ноутбуков и all-in-one рабочих станций в наше время имеют поддержку сенсорного ввода. Но, как оказалось, у компьютерных систем с активированной поддержкой тач-ввода есть одна малоизвестная функция, которая ...