Перевод ироничного поста из блога Боба Мартина в котором он рассуждает о том, насколько неудачным является использование слова interface
в современных языках программирования, и какую путаницу и проблемы оно несёт разработчикам.
— Что ты думаешь об интерфейсах?
— Имеешь в виду интерфейсы в Java или C#?
— Да. Классная фича этих языков?
— Просто великолепная!
— Правда? А что такое интерфейс? Это то же самое что и класс?
— Ну… Не совсем!
— В каком плане?
— Ни один из его методов не должен иметь реализации.
— Значит это интерфейс?
public abstract class MyInterface {
public abstract void f();
}
— Нет, это абстрактный класс.
— Так, а в чём разница?
— Абстрактный класс может иметь реализованные методы.
— Да, но этот класс их не имеет. Тогда почему его нельзя назвать интерфейсом?
— Абстрактный класс может иметь нестатические поля, а интерфейс не может.
— У моего класса их тоже нет, почему он не интерфейс?
— Потому что!
— Такой себе ответ… В чем реальное отличие от интерфейса? Что такого можно делать с интерфейсом, чего нельзя делать с этим классом?
— Класс, который наследуется от другого, не может унаследоваться от твоего.
— Почему?
— Потому что в Java нельзя наследоваться от нескольких классов.
— А почему?
— Компилятор тебе не позволит.
— Очень странно. Тогда почему я могу реализовать(implements
), а не отнаследоваться(extend
) от него?
— Потому что компилятор позволяет множественную реализацию интерфейсов, но наследовать ты можешь только один класс.
— Интересно, зачем такие ограничения?
— Потому что наследование от множества классов опасно.
— Вот так новости! И чем же?
— Смертельным Бриллиантом Смерти(Deadly Diamond of Death)!
— Звучит пугающе! Но что это значит?
— Это когда класс наследует два других класса, оба и которых наследуют третий.
— Ты имеешь ввиду что-то типо этого?
class B {}
class D1 extends B {}
class D2 extends B {}
class M extends D1, D2 {}
— Верно, это очень плохо!
— Почему?
— Потому что класс B может содержать переменные!
— Вот так?
class B {private int i;}
— Да! И как много переменных i
будет в экземпляре класса M
?
— Понятно. Т.е раз D1
и D2
содержат переменную i
, a M
наследуется от D1
и D2
то ты ожидаешь, что экземпляр класса M
должен иметь две разных переменные i
?
— Да! Но т.к M
наследуется от B
у которого только одна переменная i
, то ты ожидаешь что в M
у тебя тоже будет всего одна i.
— Вот так неоднозначность.
— Да!
— Получается что Java(и C#) не могут во множественное наследование классов, потому что кто-то может создать "Смертельный Бриллиант Смерти"?
— Не просто может создать. Каждый априори создавал бы их т.к все объекты неявно наследуют Object
.
— Ясно. А авторы компилятора не могли пометить Object
как частный случай?
— Ну… не пометили.
— Интересно почему. А как решается эта проблема в других компиляторах?
— Компилятор C++ позволяет делать это
— Я думаю Eiffel тоже.
— Черт, даже в Ruby смогли решить эту проблему!
— Ладно, получается что "Смертельный Бриллиант Смерти" это проблема, которую решили еще в прошлом веке, и она не фатальна и даже не ведёт к смерти.
— Вынужден согласиться.
— Давай вернемся к первоначальному вопросу. Почему это не интерфейс?
public abstract class MyInterface {
public abstract void f();
}
— Потому что он использует кейворд class
и язык не позволит унаследоваться от множества классов.
— Верно. Получается, что кейворд interface
был изобретен для предотвращения множественного наследования классов?
— Да, наверное.
— Так почему бы разработчикам Java (да и C#) не воспользоваться любым из решений проблемы множественного наследования?
— Откуда я знаю?
— Кажется я знаю.
— ??
— Лень!
— Лень?
— Да, им было лень разбираться с проблемой. Вот они и создали новую фичу, которая позволила им не решать её. Этой фичей стал interface
.
— Т.е ты хочешь сказать, что разработчики Java ввели понятие интерфейса чтобы избежать лишней работы?
— У меня нет другого объяснения!
— Звучит грубовато. Да и в любом случае, круто что у нас есть интерфейсы. Они тебе чем нибудь мешают?
— Ответь себе на вопрос: Почему класс должен знать что он реализует именно интерфейс? Разве это не должно быть скрыто от него?
— Имеешь в виду, что производный тип должен знать что именно он делает — наследует или реализует(extends or implements
)?
— Абсолютно! И если ты поменяешь класс на интерфейс, то в код скольки наследников придется вносить изменения?
— Во всех. В Java во всяком случае. В C# разобрались хотя бы с этой проблемой.
— Да уж. Ключевые слова implements
и extends
излишни и опасны. Было бы лучше если бы Java использовала решения C# или C++.
— Ладно, ладно. Но когда тебе реально нужно было множественное наследование?
— Я бы хотел делать так:
public class Subject {
private List<Observer> observers = new ArrayList<>();
private void register(Observer o) {
observers.add(o);
}
private void notify() {
for (Observer o : observers)
o.update();
}
}
public class MyWidget {...}
public class MyObservableWidget extends MyWidget, Subject {
...
}
— Это же паттерн "Наблюдатель"!
— Да. Это правильный "Наблюдатель".
— Но он не скомпилируется, т.к ты пытаешься отнаследоваться от двух классов сразу.
— Да, в этом и трагедия.
— Трагедия? Какого… Ты же можешь просто унаследовать MyWidget
от Subject
!
— Но я не хочу, чтобы MyWidget
знал что за ним наблюдают. Мне бы хотелось разделять ответственности(separation of concerns). Быть наблюдаемым и быть виджетом, это две совершенно разные ответственности.
— Тогда просто реализуй функции регистраци и уведомления в MyObservableWidget
.
— Что? Дублировать код для каждого наблюдаемого класса? Нет спасибо!
— Тогда пусть твой MyObservableWidget
содержит ссылку на Subject
и делегирует ему все что нужно.
— И дублировать код делегирования? Это какая-то фигня.
— Но тебе все равно придется выбрать что-то из предложенного
— Знаю. Но я ненавижу это.
— У тебя нет выхода. Либо нарушай разделение ответственностей, либо дублируй код.
— Да. Это язык сам толкает меня в это дерьмо.
— Да, это очень грустно.
— И?
— Могу лишь сказать, что кейворд interface
— вреден и губителен!
Буду признателен за ошибки и замечания в ЛС.