Следить за обновлениями блога можно в моём канале: Эргономичный код

Введение
Почти 30 лет назад в классической книге по шаблонам проектирования Design Patterns: Elements of Reusable Object-Oriented Software, авторы сформулировали один из самых известных, но недопонятых принципов в истории программирования:
Program to an interface, not an implementation.
— Erich Gamma et. al, Design Patterns: Elements of Reusable Object-Oriented Software
Зачем "программировать в интерфейсы"? Для того чтобы реализацию этого интерфейса можно было менять без изменений клиентского кода.
Далее авторы объясняют как следовать этому совету:
Don’t declare variables to be instances of particular concrete classes. Instead, commit only to an interface defined by an abstract class.
— Erich Gamma et. al, Design Patterns: Elements of Reusable Object-Oriented Software
Многие воспринимают это буквально и просто вносят в программу дополнительную сущность (интерфейс или абстрактный класс), которая дублирует список и сигнатуры методов одного конкретного класса.
Проблема в том, что использование ключевого слова abstract или interface само по себе не создаёт абстракцию и не защищает клиента от изменения реализации. Зато эти "заголовочные интерфейсы" по капельке, но каждый день подъедают человеческие, машинные и временные ресурсы.
Поэтому в Эргономичном подходе я отказался от повсеместного использования интерфейсов и применения принципа инверсии зависимостей по умолчанию.
Давайте рассмотрим на примере вымышленной истории, почему использование ключевого слова interface не является ни достаточным, ни необходимым для защиты клиентского кода от смены реализации интерфейса.
А потом увидим, насколько легко превратить конкретный класс в интерфейс и добавить новую реализацию, если он описывает абстракцию.
Наконец, рассмотрим несколько эвристик, которые помогают создавать классы описывающие абстракции, а не реализации.
Эпизод первый: Скрытая угроза. Интерфейсы.
Представим, что некий молодой архитектор Артемий начинает новый проект. Так как бизнес требует от него минимизировать "time to market", он решает, что собрать на коленке прототип будет быстрее всего на базе Spring Data JPA. Но он будет "программировать в интерфейсы", чтобы быстро всё переписать на более поддерживаемую технологию, если проект стрельнет.
Артемий пишет примерно такой код:
package pro.azhidkov.programtointerfaces.v1
import org.springframework.data.repository.CrudRepository
import javax.persistence.Entity
// ...
@Entity
class User(
@Id var id: Long,
var login: String,
var password: String
)
@Repository
interface UsersRepo : CrudRepository<User, Long>
@Service
class UsersService( // тут по идеи тоже должен быть интерфейс, но сократим его ради экономии места
private val usersRepo: UsersRepo
) {
@Transactional
fun updatePassword(id: Long, newPass: String) {
val user = usersRepo.findByIdOrNull(id)
user.password = newPass
}
}
Казалось бы, всё восхитительно - мы не завязываемся ни на какие детали реализации и в любой момент сможем сменить технологию работы с БД, да и сам тип БД.
И когда приложение перерастает штанишки прототипа, Артемий, решив перейти на Spring Data JDBC, просто меняет зависимость в скрипте сборки, делает замену по проекту "@Entity" на "@Table" и… О чудо! Всё собирается!
Однако, после поспешного релиза в прод выясняется, что ничего не работает. Точнее в режиме чтения приложение работает, а вот никакие модификации не сохраняются. После судорожного отката релиза и суток дебага, Артемий в документации к Spring Data JDBC выясняет, что она не реализует такую "небольшую деталь" как Dirty Checking и автомагически ничего не сохраняет. Тогда Артемий везде добавляет *Repo.save() и всё, кризис преодолён.
package pro.azhidkov.programtointerfaces.v2
import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
// ...
@Table
class User(
var id: Long,
var login: String,
var password: String
)
@Repository
interface UsersRepo : CrudRepository<User, Long>
@Service
class UsersService(
private val usersRepo: UsersRepo
) {
@Transactional
fun updatePassword(id: Long, newPass: String) {
val user = usersRepo.findByIdOrNull(id)
user.password = newPass
usersRepo.save(user)
}
}
Правда эта правка превратит код Артемия в анти-паттерн с точки зрения JPA
