Конспект POODR. Creating Flexible Interfaces
Некоторые ребята уверены, что ООП — это про инкапсуляцию, наследование и полиморфизм, а сами объекты — просто такая обертка над данными. Это не так, ООП — это про сообщения, которые объекты отправляют друг другу. А лучше всего об этом рассказано в POODR, Practical Object-Oriented Design in Ruby Сэнди Метц.
Это настолько полезная книга, что я перечитываю ее каждый год. Чтобы перестать уже ее перечитывать, хочу закрепить знания с помощью конспекта. В этом посте — конспект четвертой главы, Creating Flexible Interfaces.
Осторожно: это мой субъективный конспект. Не забудьте прочитать оригинал, книга того стоит.
Гибкие интерфейсы
Приложение состоит из классов и модулей, но определяют его сообщения. Именно они отражают живое, работающее приложение. Поэтому в дизайне стоит заострять внимание на сообщениях, которые объекты передают друг другу.
Публичный интерфейс — это набор сообщений, которые объект ожидает от других объектов. Еще проще: классы состоят из методов. Некоторые из методов задумывались так, чтобы ими пользовались другие классы. Эти методы и составляют публичный интерфейс.
Публичные интерфейсы:
- отражают основную ответственность класса;
- будут вызываться другими;
- не будут меняться ни с того, ни с сего;
- надежны — другие классы могут на них положиться;
- тщательно задокументированы в тестах.
Приватные интерфейсы:
- содержат в себе детали реализации;
- не будут вызываться другими;
- будут меняться и исчезать;
- ненадежны — другим классам лучше на них не полагаться;
- не участвуют в тестах.
The public parts of a class are the stable parts; the private parts are the changeable parts. When you mark methods as public or private you tell users of your class upon which methods they may safely depend. When your classes use the public methods of others, you trust those methods to be stable. When you decide to depend on the private methods of others, you understand that you are relying on something that is inherently unstable and are thus increasing the risk of being affected by a distant and unrelated change.
В поисках интерфейса
Ситуация: пишем приложение для велотуров. Делаем фичу: клиент, чтобы выбрать тур, запрашивает список туров, подходящих по сложности, дате и с доступными велосипедами в аренду.
В голову сразу приходят имена классов: Customer, Trip, Route, Bike. Так происходит, потому что это очевидные вещи из реального мира. А в приложении — существительные, у которых будут данные и поведение (domain objects).
Такие объекты — ловушка. Очень легко застрять в них, распределяя все поведение приложения по ним. Крутаны замечают такие объекты, но не концентрируются на них. Вместо этого держат фокус на сообщениях между ними. Эти сообщения помогут открыть важные, но пока невидимые, объекты.
Первое, что приходит в голову про выбор туров: пусть Customer
вызывает Trip#suitable_trips(on_date, of_difficulty, need_bike)
. Сразу же нужно спросить себя: а должен ли получатель (Trip
) уметь отвечать на это сообщение (suitable_trips
)?
Похоже, что нет: ок, что Trip
ищет туры по дате и сложности, но почему он должен знать хоть что-то о велосипедах?
Фокус на сообщениях меняет основной вопрос дизайна. Вместо «Так, у меня есть класс. Что он будет делать?» появляется «Мне надо отправить это сообщение. Кто должен на него отвечать?».
You don’t send messages because you have objects, you have objects because you send messages.
Пробуем другой вариант: пусть Customer
вызывает Trip#suitable_trips(on_date, of_difficulty)
, а затем для каждого тура проверяет, есть ли доступные велосипеды, вызывая Bicycle#suitable_bicycle(trip_date, route_type)
.
Проблема в том, что Customer
знает не только, что он хочет, но и как остальные объекты должны взаимодействовать друг с другом. Customer
берет на себя слишком много и становится God Object.
«Что» вместо «как»
Другая фича: перед стартом тура надо убедиться, что велосипеды в порядке.
Пусть Trip
знает, как проверить велик и просит Mechanic
это сделать. У Trip
есть bicycles
. Для каждого велосипеда Trip
вызывает clean_bicycle
, pump_tires
, lube_chaing
и check_brakes
у Mechanic
.
Проблема в том, что Trip
знает слишком много о том, что и как Mechanic
делает. Если в Mechanic
появится новый метод проверки велосипеда (check_repair_kit
), измениться должен будет Trip
.
Сфокусируемся на том, что нужно Trip
, а не на том, как это получить. Пусть Trip
просто просит Mechanic
проверить велосипеды, вызывая Mechanic#prepare_bicycle
.
Так ответственность за то, как подготовить велосипед, переместилась из Trip
в Mechanic
. Когда Mechanic
изменится, Trip
продолжит корректно работать. А еще у Mechanic
уменьшился публичный интерфейс, значит, меньше шансов, что изменения в нем что-то сломают в остальных объектах.
Независимый контекст
Контекст — это то, что объект знает об обкружающих его объектах. Сейчас контекст Trip
— это существование объекта Mechanic
, который умеет отвечать на prepare_bicycles
.
Контекст — штука от которой никуда не деться, которую приходится таскать за собой в приложении и тестах. Чем он больше, тем труднее тестировать и повторно использовать объект: работать с Trip
без объекта похожего на Mechanic
не получится.
The context that an object expects has a direct effect on how difficult it is to reuse. Objects that have a simple context are easy to use and easy to test; they expect few things from their surroundings. Objects that have a complicated context are hard to use and hard to test; they require complicated setup before they can do anything. The best possible situation is for an object to be completely independent of its context. An object that could collaborate with others without knowing who they are or what they do could be reused in novel and unanticipated ways.
Trip
ведь хочет быть готовым к поездке, ему все равно, кто будет этим заниматься. Пусть тогда Trip
вызывает Mechanic#prepare_trip(self)
, а тот сам уже готовит велосипеды.
Теперь все знание о том, что и как механики проверяют в велосипедах, живет в Mechanic
. А у Trip
уменьшился контекст.
В истории с Mechanic
мы прошли три этапа:
- я знаю, что я хочу и как это сделать с твоей помощью (
Mechanic#clean_bicycle
); - я знаю, что я хочу и что ты делаешь (
Mechanic#prepare_bicycles
); - я знаю, что я хочу и полностью доверяю тебе, делай, что нужно (
Mechanic#prepare_trip
).
Как сообщения открывают новые объекты
Вернемся к проблеме с поиском туров. Это нормально, что Customer
отправляет suitable_trips
— это именно то, что он хочет. Проблема с получателем — это точно не Trip
. Нам нужен какой-то другой объект.
Пусть это будет TripFinder
: Customer
вызывает TripFinder#suitable_trips(on_date, of_difficulty, need_bike)
, TripFinder
подбирает туры, вызывая Trip#suitable_trips(on_date, of_difficulty)
и Bicylce#suitable_bicycle(trip_date, route_type)
.
Теперь в TripFinder
находится все знание о том, что считать подходящим туром. Он знает все нужные правила. Его можно использовать отдельно от Customer
.
Как создавать хорошие интерфейсы
Создавайте явные интерфейсы
Your goal is to write code that works today, that can easily be reused, and that can be adapted for unexpected use in the future. Other people will invoke your methods; it is your obligation to communicate which ones are dependable. Every time you create a class, declare its interfaces. Methods in the public interface should
- Be explicitly identified as such
- Be more about what than how
- Have names that, insofar as you can anticipate, will not change
- Take a hash as an options parameter
Уважайте чужие публичные интерфейсы
Не используйте приватные методы. Если без этого не обойтись, еще раз обдумайте дизайн. Если ничего не выходит, хотя бы изолируйте его использование в одном месте.
Минимизируйте контекст
Construct public interfaces with an eye toward minimizing the context they require from others. Keep the what versus how distinction in mind; create public methods that allow senders to get what they want without knowing how your class implements its behavior.
P. S. Ещё больше постов о программировании, тестах и культуре разработки у меня в Телеграме.