Объекты и проектирование типов

Что это такое

Система типов Java складывается из классов, интерфейсов, обобщений и различия между тем, что тип обещает, и тем, как он устроен. Класс задаёт состояние и поведение, интерфейс описывает контракт без реализации, а обобщения позволяют написать контейнер или алгоритм один раз и применять их ко многим типам, не теряя проверок компилятора. Проектировать этими средствами — значит решать, что именно обещает каждый тип и где допускается вариативность.

Почему это важно

Система типов — главный инструмент, благодаря которому большую кодовую базу можно безопасно менять. Удачно выбранный интерфейс позволяет подменить зависимость, в том числе в тесте. Финальный класс или неизменяемое значение сообщает читателю, что перед ним нечто постоянное, а обобщённый тип переносит ошибку приведения из времени работы во время компиляции. Однако при небрежном обращении те же средства дают глубокие иерархии, которые трудно менять, и абстракции, у которых нет причины существовать. Мастерство в том, чтобы решить, какие обещания стоит закрепить в типах.

Ключевые идеи

Класс — это обещание о состоянии и поведении

Класс объединяет поля с методами, которые с ними работают, а видимость решает, кому доступна каждая часть. Внутренности класса не становятся всеобщим делом только тогда, когда поля приватны, а методы выражают намерение. Неизменяемый класс, состояние которого нельзя поменять после создания, безопасно разделять между потоками и использовать как ключ; заодно он убирает целый класс проблем конкурентности.

Интерфейс — это контракт, а реализация — деталь

Интерфейс говорит, на что могут рассчитывать те, кто вызывает код, и ничего о том, как это сделано. Вторую реализацию — тестового двойника или другое хранилище — допускает именно зависимость от интерфейса, а не от конкретного класса. Интерфейс с единственной реализацией и без перспектив второй обычно оказывается косвенностью ради косвенности.

interface PaymentGateway { Receipt charge(Money amount); }
class StripeGateway implements PaymentGateway { ... }
class FakeGateway  implements PaymentGateway { ... }   // используется в тестах

Обобщения переносят ошибки типов во время компиляции

Обобщённая коллекция указывает, что в ней лежит, поэтому извлечение элемента не требует приведения, а вставка неверного типа не компилируется. Параметры типа можно ограничивать. Тогда алгоритм вправе требовать, чтобы аргумент был сравнимым или подтипом. Стирание обобщений во время выполнения — причина того, что некоторые проверки нельзя выразить, а отдельные конструкции дают предупреждения, которые стоит понимать.

Наследование — сильная связанность, композиция обычно слабее

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

Возможность отсутствия значения — проектное решение, которое теперь выражается в типе

Ссылка может отсутствовать, и большую часть истории языка эта возможность никак не документировалась. Аннотации и современные средства языка позволяют сделать отсутствие частью типа, и тогда компилятор может потребовать проверки. Там, где это доступно, такое использование превращает отказ во время работы в ошибку компиляции на границе.

Исключения — часть контракта, и проверяемые об этом говорят

Проверяемые исключения метода объявлены, и компилятор за этим следит, поэтому тот, кто вызывает метод, не может проигнорировать режим отказа, который автор счёл восстановимым. Непроверяемые исключения предназначены для ошибок программирования. Проектное решение в том, какие отказы попадают в сигнатуру: слишком много — и каждый вызов тонет в шуме, слишком мало — и скрывается отказ, который следовало обработать.

Практика

Замените конкретную зависимость интерфейсом

Возьмите класс, который сам создаёт своего помощника, и измените его так, чтобы он принимал интерфейс, а затем подставьте подделку в тесте.

Готово, когда: тест выполняется без настоящего помощника, а рабочий путь по-прежнему им пользуется.

Сделайте класс значения неизменяемым

Возьмите класс, поля которого можно менять после создания, и сделайте его неизменяемым, чтобы им было безопасно делиться между потоками.

Готово, когда: ни один метод не меняет состояние после создания и один пользователь класса не видит чужих изменений.

Частые ошибки

  • Наследование ради переиспользования кода — подкласс привязан к реализации родителя; исправить: держите другой объект в поле.
  • Интерфейс с единственной реализацией — косвенность пока ничего не даёт; исправить: выделяйте её, когда появилась вторая реализация или она нужна тесту.
  • Использование «сырых» обобщённых типов — возвращаются приведения и ошибки во время работы; исправить: указывайте параметр типа.
  • Публичные поля — внутренности становятся интерфейсом; исправить: держите состояние приватным и выражайте намерение.
  • Проверяемые исключения на каждом шагу — код, который их вызывает, тонет в шуме и ничего осмысленно не перехватывает; исправить: оставьте их для отказов, которые можно обработать осмысленно.

Что читать дальше