Это пост, не зависящий от языка: он действителен для каждого объектно-ориентированного императивного языка, такого как Java, C #, PHP.
Начнем с самого начала: адаптер веб-службы
У меня было требование: скачивать сообщения из определенной группы в LinkedIn для анализа их контента. Пока все хорошо: для любой интеграции с моими приложениями я следую Шестиугольной Архитектуре .
По словам Кокберна , гексагональная архитектура имеет ряд портов (обычно интерфейсы, определенные на языке), где вы можете подключить адаптеры, чтобы приложение действительно работало. Постоянство имеет дело с интерфейсами репозитория; интеграция веб-сервисов с интерфейсом WebServiceName. В моем случае реализация — это объект, который запрашивает API-интерфейс LinkedIn через HTTP.
Остальная часть приложения зависит только от интерфейсов портов : тестирование становится проще, поскольку для новых тестовых случаев вы можете подключить Test Double для веб-службы, которая способна возвращать все фальшивые ответы XML (или JSON), которые вам нужны.
С этой привычкой я начал тест-драйв этого дизайна:
Scribe — это имя библиотеки OAuth, используемой для выполнения запросов при реализации сервиса. PostsParser составляет службу и является первым объектом в домене моего приложения. Код немного упрощен, исключая необходимость в ограничениях по времени и различных опциях в запросе.
Подводя итоги: теперь мой код зависит от интерфейса, и я могу протестировать большую его часть с заглушкой, возвращающей поддельные ответы XML. На самом деле я просто поместил один объект перед ним, чтобы проанализировать XML, поэтому с этого момента остальная часть системы будет работать с Plain Old YourLanguage Objects . Правильно? Правильно.
Вы можете увидеть проблему еще?
Разве добытчики не были злыми? Я издеваюсь или заглушаюсь?
Да, методы получения объектов, содержащих логику, не являются моим определением чистого кода. Однако, когда я пишу адаптеры, я заполняю их классами: getPosts (), getUsers (), getComments () …
Я могу разделить методы в различных интерфейсах и иметь только одну реализацию. На этот раз давайте попробуем что-то другое и получим вдохновение от того, что называют (ошибочно) лондонской школой.
Я смотрел презентацию на Ruby (чтобы писать с помощью Java-приложения, как программист на PHP … Я говорил вам, что эта статья не зависит от языка) под названием «Почему вы не получаете фиктивные объекты?».
Выделенная проблема заключалась в том, что мы часто пишем макеты, которые становятся заглушками, и мы проверяем переданные им аргументы и результат вычислений:
$mock = $this->getMock(); $mock->expects($this->once) ->method('whatIsThis') ->with(42) ->will($this->returnValue("The answer")); ...bit of work to obtain $response from a real object composing the mock... $this->assertEquals("According to Deep Thought, 42 is The answer", $response);
Основанная на состоянии (утверждение) и основанная на поведении проверка ( с (42) ) в то же время становятся запахом для меня . Этот вид тестов также приводит к тестированию и производству кода, которые являются зеркальными.
В нашем примере LinkedIn я должен проверить параметры ( groupId == 23 и postsToFetch == 10 или что-то еще), и что класс, составляющий службу, выполняет свою работу (возвращая объекты домена Post, содержащие правильные значения, проанализированные из XML).
Голливудские объекты
Также была интересная дискуссия о голливудских объектах в списке рассылки GOOS . Голливудские объекты были определены как гипотетические объекты, которые не имеют геттеров, а просто методы с обратными вызовами:
mainObject.getField(); //becomes: mainObject.tellField(objectWhoNeedsIt);
Недостающий элемент в этом обсуждении (я, возможно, пропустил это, даже если кто-то написал), заключался в том, что objectWhoNeedsIt был передан в стек, а tellField () — это уровень косвенности, который я нахожу бесполезным в большинстве случаев.
Но если objectWhoNeedsIt передается в конструктор mainObject, он становится соавтором. Вызов метода становится просто mainObject.tellField ().
Мое новое решение
Теперь, когда я обновил свои идеи по составлению объектов, давайте попробуем исключить метод получения в LinkedInService:
Inverted Dependency Inversion … хорошее имя. PostsParser, в свою очередь, передает свои вновь созданные объекты Post кому-либо еще, и так далее, и так далее. Нет необходимости в геттерах ScribeLinkedInService, и мне даже не нужна заглушка для сервиса; все тестовые двойники, которые я использую, все еще являются имитацией моих собственных типов, но исключая услугу.
Повторите со мной:
Почему это так? Каждый объект работает, и часть системы может измениться, не затрагивая остальную часть приложения (сообщения не изменяются). Вызовы методов, которые запрашивают состояние, раскрывают очень много из двух взаимодействующих объектов, поскольку данные передаются в двух направлениях как параметры и возвращаемые значения.
Мы не всегда можем использовать так называемые голливудские объекты , но при необходимости вызовы методов, которые просто сообщают, идут только в одном направлении и упрощают протокол. Когда протокол проще, вы получаете преимущества, заменяя объекты для тестирования или добавления нового поведения в систему.
Эй, вся асинхронная парадигма в SOA или события между DD-агрегатами также основана на этом преимуществе односторонней связи.
Почему я издеваюсь реже, чем могу?
Инструменты, которые мы используем, оказывают глубокое (и коварное!) Влияние на наши привычки мышления и, следовательно, на наши мыслительные способности. — Дейкстра
I originally used only JUnit and PHPUnit for TDD: the absence of a mocking framework in the first and of decent matchers in the second has lead me to favor state-based verification with lot of assert*() even when mocks proved equally capable. As a consequence, I wrote a lot of getters instead of following the Tell, Don’t Ask principle. State-based verification is also how I learned (and taught) TDD, since it is the most immediate method.
But now I have transformed my LinkedIn code: it’s easier to test with mocks, and it’s easier for me to substitute objects.