Недавно в команде произошел случай, который заставил обратить наше внимание на статью 2013 года. Материал не потерял актуальности, а перевода я не нашел, поэтому решил заполнить пробел. Но сначала немного предыстории.
Комьюнити-менеджеры обратили внимание на редко всплывающий баг в приложении: если пользователь закрепит контент вверху своего профиля, а затем модератор этот контент по каким-то причинам забанит, то у пользователя нет возможности ничего с этим закреплённым и забаненным контентом сделать: открепить его нельзя и другими способами от него избавиться тоже не получится. Типичный краевой случай, который редко встречается в реальной жизни. Я хоть и менеджер, но решил не отвлекать никого из команды на эту мелочь, а пофиксить баг самостоятельно, заодно стряхнуть немного пыль с навыков разработки.
Буквально через два уровня абстракций я оказался в коде, датированном 2016-2017 годами, то есть занялся software archeology. В какой-то момент меня возмутило, что вместо интерфейса репозитория в конструктор класса сервиса была передана реализация. Ещё одним уровнем абстракции ниже оказалось, что это всё-таки был интерфейс, но назывался он просто ContentRepository, а не ContentRepositoryInterface, как написал бы любой адепт ООП и принципов SOLID. Это уже не лезло ни в какие ворота, и я потребовал у архитектора оснований (кстати, рекомендую его статью о том, как мы вдвое ускорили построение лент подписок). Он в свою очередь невозмутимо показал пункт внутренних правил оформления кода, где была проставлена ссылка на статью 2013 года («А» — археология, как и было сказано).
Под катом — перевод этого материала.
Как мы из «пиши код с учётом интерфейса, а не реализации» очутились в «просто прикрути тут интерфейс, это сейчас модно»?
Вот вам немного правил, выполняя которые вы сможете жить в мире и согласии с вашими коллегами — программистами-рок-звёздами-быдлокодерами.
Именование
В нашей команде приписывание слова Interface в конце названия интерфейса приравнивается к нарушению трудовой дисциплины и может послужить основанием для увольнения. Ну, возможно не до такой степени, но лучше не пробуйте. То же самое относится к префиксу I (да, Microsoft, ты всё правильно понял). Сейчас я объясню почему.
Самим наличием интерфейса вы подразумеваете, что у него возможны несколько реализаций. Обычно если вы где-то видите TranslatorInterface, то поблизости есть реализация вида Translator implements TranslatorInterface. Здесь я сразу задумываюсь: что такого уникального в этом Translator, что он получил эксклюзивное право называться Translator? Любая другая реализация потребует поясняющего наименования, типа XmlTranslator или CachedTranslator, но именно эта реализация почему-то считается реализацией «по умолчанию», что вытекает из её приоритетного имени Translator — без дополнительных ремарок. Плохо ли это? Я считаю, что да. Такой подход сбивает людей с толку: они не понимают, должны ли они требовать на входе тип TranslatorInterface или просто Translator? Писать код иногда с учётом интерфейса, а иногда реализации?
Ещё хуже, что такое именование подразумевает, будто реализация — это правильная штука, а интерфейс — так, ярлык, хотя на самом деле всё должно быть ровно наоборот. Возьмём, например, такой пример клиентского кода:
<?php class KlingonDecoder { public function __construct(TranslatorInterface $translator)
Такая декларация конструктора гласит: мне нужен интерфейс типа «translator» для того, чтобы нормально функционировать. Но это выглядит глуповато. На самом деле нам нужен объект Translator, а никакой не интерфейс. И этот объект играет строго определённую роль, выполняет конкретный контракт под названием Translator. Надеюсь, я выражаюсь однозначно: Translator здесь — это неотъемлемая часть всей конструкции, без которой клиент работать не будет. Клиенту неважно, интерфейс это или реализация, ему неинтересно, как именно там всё внутри устроено. Клиент не хочет быть зависимым от этих подробностей. В этом заключается мощь интерфейсов.
Реализации «по умолчанию»
Таким образом, проблема выбора наглядного имени переносится на плечи реализации интерфейса. Если мы переименуем TranslatorInterface в Translator, нашему классу Translator понадобится новое имя. Людям свойственно решать эту проблему не включая голову, и писать что-то типа DefaultTranslator. Это возвращает нас к исходной проблеме: что такого уникального в этой реализации, что она заслужила титул Default? Не ленитесь, подумайте хорошенько о том, что именно она делает и чем отличается от других потенциальных реализаций. В процессе размышлений вы можете узнать о своём классе что-то новенькое, например что у него слишком много ответственностей.
«Nameable»
Ещё одна вредная привычка — использование суффикса -able для именования интерфейсов. Я думаю, что я переживу что-нибудь типа Translatable, или, скажем, Serializable. Но Timestampable? Jsonable? Такой мир мы хотим оставить в наследство нашим детям? Английский язык, засранец, ты говоришь на нём? (Прим. переводчика: цитата из фильма «Криминальное чтиво»). Попробуйте построить нормальное предложение, будет намного лучше:
<?php class Product implements CastsToJson, HasTimestamp
А теперь громко и вслух: «Продукт переводится в JSON и имеет метку времени». Это красиво; так, не побоюсь сказать, сам Шекспир бы написал.
Соблюдайте контракт
Язык PHP, в ходе своего, мягко говоря, органического развития, стал весьма расслаблен, когда дело касается интерфейсов. Посмотрите на этот код:
<?php interface Animal { public function makeNoise(); } class Dog implements Animal { public function makeNoise() {} public function fetchStick() {} } // elsewhere: public function myClient(Animal $animal) { $animal->fetchStick(); }
Несмотря на то, что myClient() принимает на вход тип Animal и не может знать о том, является ли переданный параметр $animal экземпляром класса Dog, PHP позволяет вам вызвать метод fetchStick(), если он есть у $animal. Такая гибкость может быть весьма полезна, но в очень ограниченном количестве случаев. В остальных случаях никогда не вызывайте метод объекта, если он не является частью интерфейса, который вы требуете на входе. (Прим. переводчика: современные IDE подсвечивают такие ошибки).
Разделение интерфейсов
Если вы обнаружили, что ваш клиентский код зависит от интерфейса с кучей методов, которые вашему клиенту не нужны, это, вероятно, признак чрезмерно развесистого интерфейса. Аналогично если ваша реализация вынуждена реализовывать много методов путём подстановки заглушек:
<?php class Fish implements Animal { public function makeNoise() { throw new NotImplemented("Fish don't make noise"); } }
Это хороший признак того, что вам стоит выделить метод makeNoise() в отдельный интерфейс. Как насчёт MakeNoise или Noisy?
Роли
Если рассматривать интерфейсы как роли, то это может позволить удобно переиспользовать код, скрывая этот факт от клиентов. Допустим, что ваши цены лежат где-то в базе данных. Бизнес-логика формирования заказов зашита в OrderBuilder, но вы не хотите, чтобы OrderBuilder знал о том, что цены находятся в базе данных, потому что в будущем это может поменяться. Вы можете решить эту задачу при помощи вот такой композиции:
<?php interface ProductRepository { /* определяет find(), add()... */ } interface ProductPricer { public function priceProduct(Product $product); } class DbProductPriceRepository implements ProductRepository { /* реализует find(), add()... */ } class DbProductPricer implements ProductPricer { public function __construct(ProductRepository $productRepository){ /* ... */ } public function priceProduct(Product $product) { /* ... */ } }
Чтобы избавиться от необходимости много барабанить по клавиатуре, вы можете делегировать роль ProductPricer классу DbProductPricer. Конечно, в этом случае он будет выполнять двойную функцию, но клиенты об этом не узнают. Конкретно в данном примере это, вероятно, не будет хорошим долгосрочным решением по мере роста приложения, но для быстрого прототипирования сойдёт.
<?php class DbProductPriceRepository implements ProductRepository, ProductPricer { /* implements find(), add()... */ public function priceProduct(Product $product) { /* ... */ } }
Кстати, роли отлично работают бок о бок с сущностями (entities):
<?php class Teacher implements User {} class Pupil implements User {} class Parent implements User {}
Единственная реализация
ProductPricer — хороший пример интерфейса, потому что к нему легко придумать различные бизнес-правила, применяемые в разных ситуациях: GermanProductPricer, BelgianProductPricer. Также могут быть разные технические реализации: DbProductP
ricer, SoapProductPricer, или CachedProductPricer, оборачивающая другую реализацию.
Но так бывает не всегда. Возможно, что у вашего бизнеса есть Один Правильный Способ вычисления цен, и Один Истинный Источник, в котором они хранятся. Моё личное правило гласит, что если вы можете представить себе более одного способа реализации — используйте интерфейс. Если вы не можете себе представить другие способы — не используйте интерфейс. Хороший пример — OrderTotalCalculator. Есть только один способ сложить различные цены в заказе, и в интерфейсе здесь смысла не будет.
Aware
Я, честно говоря, не решил для себя, как относиться к использованию суффикса Aware. Я считаю, что само по себе это не является проблемой. Но, разумеется, ContainerAware — это явное зло и ему не следовало вообще появляться в Symfony. Впрочем, это уже анти-паттерн к принципу внедрения зависимостей, а не проблема с именованием классов. Согласны?
ссылка на оригинал статьи https://habr.com/ru/company/funcorp/blog/545350/
Добавить комментарий