В чем смысл выносить код в абстрактный класс и в чем смысл выносить код в интерфейс?
Что проверяют
Понимаете ли вы, что это инструменты для разных задач: абстрактный класс переиспользует реализацию, интерфейс описывает контракт. Для Android-разработчика на Kotlin следом часто спрашивают, чем интерфейс Kotlin отличается от Java-интерфейса и почему в слое данных почти всегда интерфейсы.
Как отвечать
- абстрактный класс: общий предок для близких классов. Может хранить состояние в полях, иметь конструктор с параметрами,
protected-члены и готовые методы. Наследоваться можно только от одного класса; - интерфейс: контракт, который класс обещает выполнить. Класс реализует сколько угодно интерфейсов. В Kotlin интерфейс может содержать методы с реализацией по умолчанию и объявлять свойства, но не может хранить состояние: у свойства нет поля, только геттер;
- главный критерий: если вы выносите код ради общей реализации и общего состояния, это абстрактный класс. Если ради того, чтобы вызывающий код зависел от возможности, а не от конкретного класса, это интерфейс.
Примеры из Android. ViewModel и RecyclerView.Adapter сделаны абстрактными классами: они несут внутреннее состояние и жизненный цикл, которые интерфейсом не выразить. А UserRepository в слое данных объявляют интерфейсом: ViewModel зависит от него, Hilt через @Binds подставляет реальную реализацию, а в тесте вы передаёте фейк без моков и сети.
Стоит упомянуть альтернативу наследованию. В Kotlin классы по умолчанию final, и язык подталкивает к композиции: делегирование class Logged(repo: Repo) : Repo by repo переиспользует реализацию без абстрактного предка. Для интерфейса с одним методом есть fun interface, который принимает лямбду.
Где спотыкаются
Отвечают по-старому: «интерфейс не содержит реализации». В Java методы по умолчанию есть с восьмой версии, в Kotlin с самого начала, так что различие теперь в состоянии и конструкторе.
Строят глубокие иерархии BaseActivity и BaseFragment, куда сваливают аналитику, диалоги и загрузку. Через год каждый экран тянет чужую логику, а менять её страшно.
Заводят интерфейс на каждый класс «на будущее». Если реализация одна и в тестах её не подменяют, интерфейс только добавляет прыжок при чтении кода.