Объекты и проектирование типов
Что это такое
Система типов Java складывается из классов, интерфейсов, обобщений и различия между тем, что тип обещает, и тем, как он устроен. Класс задаёт состояние и поведение, интерфейс описывает контракт без реализации, а обобщения позволяют написать контейнер или алгоритм один раз и применять их ко многим типам, не теряя проверок компилятора. Проектировать этими средствами — значит решать, что именно обещает каждый тип и где допускается вариативность.
Почему это важно
Система типов — главный инструмент, благодаря которому большую кодовую базу можно безопасно менять. Удачно выбранный интерфейс позволяет подменить зависимость, в том числе в тесте. Финальный класс или неизменяемое значение сообщает читателю, что перед ним нечто постоянное, а обобщённый тип переносит ошибку приведения из времени работы во время компиляции. Однако при небрежном обращении те же средства дают глубокие иерархии, которые трудно менять, и абстракции, у которых нет причины существовать. Мастерство в том, чтобы решить, какие обещания стоит закрепить в типах.
Ключевые идеи
Класс — это обещание о состоянии и поведении
Класс объединяет поля с методами, которые с ними работают, а видимость решает, кому доступна каждая часть. Внутренности класса не становятся всеобщим делом только тогда, когда поля приватны, а методы выражают намерение. Неизменяемый класс, состояние которого нельзя поменять после создания, безопасно разделять между потоками и использовать как ключ; заодно он убирает целый класс проблем конкурентности.
Интерфейс — это контракт, а реализация — деталь
Интерфейс говорит, на что могут рассчитывать те, кто вызывает код, и ничего о том, как это сделано. Вторую реализацию — тестового двойника или другое хранилище — допускает именно зависимость от интерфейса, а не от конкретного класса. Интерфейс с единственной реализацией и без перспектив второй обычно оказывается косвенностью ради косвенности.
interface PaymentGateway { Receipt charge(Money amount); }
class StripeGateway implements PaymentGateway { ... }
class FakeGateway implements PaymentGateway { ... } // используется в тестах
Обобщения переносят ошибки типов во время компиляции
Обобщённая коллекция указывает, что в ней лежит, поэтому извлечение элемента не требует приведения, а вставка неверного типа не компилируется. Параметры типа можно ограничивать. Тогда алгоритм вправе требовать, чтобы аргумент был сравнимым или подтипом. Стирание обобщений во время выполнения — причина того, что некоторые проверки нельзя выразить, а отдельные конструкции дают предупреждения, которые стоит понимать.
Наследование — сильная связанность, композиция обычно слабее
Расширение класса привязывает подкласс к реализации родителя, поэтому изменение в родителе может сломать ребёнка. Реализация интерфейса или хранение другого объекта в поле выражает ту же вариативность без такой связанности. Правило «предпочитайте композицию наследованию» существует потому, что к наследованию легко потянуться, а отменить его трудно.
Возможность отсутствия значения — проектное решение, которое теперь выражается в типе
Ссылка может отсутствовать, и большую часть истории языка эта возможность никак не документировалась. Аннотации и современные средства языка позволяют сделать отсутствие частью типа, и тогда компилятор может потребовать проверки. Там, где это доступно, такое использование превращает отказ во время работы в ошибку компиляции на границе.
Исключения — часть контракта, и проверяемые об этом говорят
Проверяемые исключения метода объявлены, и компилятор за этим следит, поэтому тот, кто вызывает метод, не может проигнорировать режим отказа, который автор счёл восстановимым. Непроверяемые исключения предназначены для ошибок программирования. Проектное решение в том, какие отказы попадают в сигнатуру: слишком много — и каждый вызов тонет в шуме, слишком мало — и скрывается отказ, который следовало обработать.
Практика
Замените конкретную зависимость интерфейсом
Возьмите класс, который сам создаёт своего помощника, и измените его так, чтобы он принимал интерфейс, а затем подставьте подделку в тесте.
Готово, когда: тест выполняется без настоящего помощника, а рабочий путь по-прежнему им пользуется.
Сделайте класс значения неизменяемым
Возьмите класс, поля которого можно менять после создания, и сделайте его неизменяемым, чтобы им было безопасно делиться между потоками.
Готово, когда: ни один метод не меняет состояние после создания и один пользователь класса не видит чужих изменений.
Частые ошибки
- Наследование ради переиспользования кода — подкласс привязан к реализации родителя; исправить: держите другой объект в поле.
- Интерфейс с единственной реализацией — косвенность пока ничего не даёт; исправить: выделяйте её, когда появилась вторая реализация или она нужна тесту.
- Использование «сырых» обобщённых типов — возвращаются приведения и ошибки во время работы; исправить: указывайте параметр типа.
- Публичные поля — внутренности становятся интерфейсом; исправить: держите состояние приватным и выражайте намерение.
- Проверяемые исключения на каждом шагу — код, который их вызывает, тонет в шуме и ничего осмысленно не перехватывает; исправить: оставьте их для отказов, которые можно обработать осмысленно.
Что читать дальше
- Язык и JVM (05-backend-data/04-java/01-the-language-and-the-jvm) — как эти типы компилируются и проверяются.
- Java (05-backend-data/04-java) — язык, которому принадлежит эта система типов.
- Сборка и зависимости (05-backend-data/04-java/03-builds-and-dependencies) — как компилируется и упаковывается код, построенный из этих типов.