Pull to refresh

Интерфейсы vs. классы

Website development *
Обсуждая с различными людьми — в большинстве своём опытными разработчиками — классический труд «Приёмы объектно-ориентированного проектирования. Паттерны проектирования» Гаммы, Хелма и др., я с изумлением встретил полное непонимание одного из базовых подходов ООП — различия классов и интерфейсов.



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

Поэтому я попытался систематизировать своё понимание вопроса в этой заметке.

Главное отличие класса от интерфейса — в том, что класс состоит из интерфейса и реализации.

Любой класс всегда неявно объявляет свой интерфейс — то, что доступно при использовании класса извне. Если у нас есть класс Ключ и у него публичный метод Открыть, который вызывает приватные методы Вставить, Повернуть и Вынуть, то интерфейс класса Ключ состоит из метода Открыть. Когда мы унаследуем какой-то класс от класса Ключ, он унаследует этот интерфейс.

Кроме этого интерфейса, у класса есть также реализация — методы Вставить, Повернуть, Вынуть и их вызов в методе Открыть. Наследники Ключа наследуют вместе с интерфейсом и реализацию.

И вот здесь таятся проблемы. Предположим, у нас есть некая модель, которая предполагает использование ключа для открытия двери. Она знает интерфейс Ключа и поэтому вызывает метод Открыть.

Но, предположим, некоторые двери открываются не таким вот поворотным ключом, а магнитной карточкой — которая ведь тоже по своей сути ключ! Интерфейс этой карточки никак принципиально не отличается от интерфейса обычного ключа — можно Открыть ключом, а можно Открыть карточкой.

И мы хотим сделать класс Магнитную Карточку, который тоже будет содержать интерфейс Ключа. Для этого мы унаследуем Магнитную Карточку от Ключа. Но вместе с интерфейсом унаследовалась и реализация, которая в методе Открыть вызывает методы Вставить, Повернуть и Вынуть — а это совершенно не подходит для Магнитной Карточки.

Нам придётся самое меньшее перегружать в Магнитной Карточке реализацию метода Открыть, используя уже последовательность Вставить, Провести и Вынуть. Это уже плохо, потому что мы не знаем детали реализации класса Ключ — вдруг мы упустили какое-то очень важное изменение данных, которое должно было быть сделано — и было сделано в методе Ключ:: Открыть? Нам придётся лезть во внутреннюю реализацию Ключа и смотреть, что и как — даже если у нас есть такая техническая возможность (open source навсегда и так далее), это грубое нарушение инкапсуляции, которое ни к чему хорошему не приведёт.

Именно так и пишут Гамма и др.: наследование является нарушением инкапсуляции.

Можете попробовать самостоятельно поразмышлять над такими вопросами:
— Что делать с тем фактом, что Ключ вставляется просто в скважину, а Магнитная Карточка — обязательно сверху (не посередине и не снизу)?
— Что делать, когда нам понадобиться сделать Бесконтактную Карточку, которую надо не вставлять, а подносить?

Правильный подход к данному вопросу — отделить интерфейс от реализации. Во многих современных языках для этого предусмотрен специальный синтаксис. В отсталых языках можно использовать хаки, например, в Цэ-два-креста — чисто абстрактные классы и множественное наследование (извините за обсценную лексику — из песни слова не выкинешь).

Мы должны опираться на интерфейсы, а не классы.

Объявим интерфейс Ключ, содержащий метод Открыть.

Объявим класс Поворотный Ключ, реализующий интерфейс Ключ при помощи своих методов Вставить, Повернуть и Вынуть.

Объявим класс Магнитная Карточка, тоже реализующий интерфейс Ключ, но уже по-своему — и без каких-либо неприятных пересечений с реализацией Поворотного Ключа. Этого помогло нам достичь отделение интерфейса от реализации.

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

Вам может показаться странным, но это именно то, что отличает человека от животного — использование интерфейсов вместо классов. Вы наверняка помните классический опыт с обезьяной, которую приучили гасить огонь водой из ведёрка; а потом поставили ведёрко на плот посреди бассейна, но обезьяна всё равно бегала по мостику на плот и черпала воду из ведёрка, вместо того, чтобы черпать воду прямо из бассейна. То есть обезьянка использовала класс Вода-в-Ведёрке вместо интерфейса Вода (и даже больше, скажу по секрету: вместо интерфейса Средство-для-Тушения).

Когда мы мыслим классами — уподобляемся животным. Люди мыслят (и программируют) интерфейсами.

Использование интерфейсов даёт большие возможности. Например, класс может реализовывать несколько интерфейсов: класс Ключ-от-Домофона может содержать интерфейсы Ключ и Брелок.

Что касается наследования классов, которое, как вы помните, нарушает инкапсуляцию — часто вместо наследования лучше использовать делегирование и композицию. Не забыли Бесконтактную Карточку? Так хочется сделать её родственной Магнитной Карточке! (Например, чтобы знать, что их обе можно положить в Отделение-для-Карточек в Бумажнике.) Однако у них, кажется, нет ничего общего: интерфейс Ключ их роднит в той же мере, что и Поворотный Ключ с Ключом-от-Домофона.

Решение? Делаем класс (а может, и интерфейс — подумайте, что здесь подойдёт лучше) Карточка, реализующий интерфейс Ключ за счёт делегирования Магнитной Карточке либо же Бесконтактной Карточке. А чтобы узнать, что такое делегирование и композиция, а так же при чём тут абстрактная фабрика — обратитесь к книгам, посвящённым паттернам проектирования.

У использования интерфейсов вместо классов есть ещё много преимуществ. Вы сами сможете увидеть их на практике. Оставайтесь людьми!

P.S. К моему удивлению, я не смог найти на «Хабрахабре» ни блога, посвящённого ООП, ни блога, посвящённого программированию в целом. Если они есть — укажите, пожалуйста, а пока за неимением лучших вариантов размещаю в персональном блоге.
Tags:
Hubs:
Total votes 4: ↑4 and ↓0 +4
Views 261K
Comments Comments 160