Pull to refresh

Comments 161

"...что, когда я некоторое время назад встретил у сразу нескольких программистов откровенное непонимание концепции..."

Очень странные программисты...
За статью спасибо, только думаю стоило бы имена интерфейсов выделить префиксом, например - ИКлюч, ИСредствоДляТушения, ИСредствоОбученияДляНепонятливыхПрограммистов =)
Несколько неудобно было читать.
Это вы берёте пример с Microsoft? Или с Borland (Delphi)?

Венгерская нотация для обозначения интерфесов ненужна.
adobe.com AS3: все интерфейсы ISelectable IFocused I... I... I...
ставить перед именами интерфейсов «I», является, вроде как, хорошим тоном, и рекомендуется во многих учебниках
А бывают программисты, которые не различают этих понятий?... а программисты ли они тогда?
Бывают начинающие программисты, которые только знакомятся с ООП.

Бывают и не начинающие, которые ещё помнят, что это Организация освобождения Палестины^W^W^W^W^W^W^W это ООП проходили в прошлом веке, а после этого писали на чём-нибудь, не особо это ООП требовавшее.

Как видим из обсуждения, не только что бывают, но их большинство. Во всяком случае на хабре. :-)
страшно жить становится...
Не то что бы физически страшно, но количество денег которое уходит на то, что бы пояснить 90% разработчиков правильные принципы декомпозициии системы, просто потрясающе. Только 5-10% разработчиков способны быстро въехать, адекватно мыслить и вносить полезные замечания в проектирование системы. Остальные - просто информационный шум. Почитайтье дискуссию ниже. Люди тотально не понимают что такое интерфейс и зачем он нужен. Это действительно серьёзная интелектуальная проблема. Особенно учитывая тот факт, что отстранять людей от обсуждения не совсем гуманно. Приходится тратить деньги на их убеждение. Но не всех можно убедить. Есть ещё и некоторый интеллектуальный барьер. Как бы шовинистически это и не звучало.
хех :) дык ООП не единственная парадигма программирования :) а есть много вариантов когда без нее обходятся или даже нельзя её применять или не нужно :)
Не согласен. Думать в терминах классов очень полезно. Интерфейсы полезны, когда нужно выстроить иерархию неких абстрактных методов типа icomparable. Но когда строится иерархия однотипных, похожих друг на друга классов полезно применять наследование. Интерфейсы влекут за собой избыточное кодирование, изменение означает полную переработку по всей иерархии. В наследовании есть очень полезная штука с дефолтной реализацией плюс еще могут быть поля. Короче на одних только интерфейсах каши не сваришь.
Интерфейсы кстати тоже могут нарушать инкапсуляцию - например, мне нужно выполнять какой-то базовый метод так, а не иначе - спасает наследование реализации. А интерфейсы нарушают. :)
ПС: пример надуман :) можно наследовать Карточку и Ключ от общего абстрактного класса.
ППС: Если у классов есть общие поля, то композицией не отделаешься. Имхо естественно наследовать реализацию.
ПППС: Бывают те кто пришли из спп - они пытаются контракты сделать через наследование. И сразу перестроиться нельзя.
...Но когда строится иерархия однотипных, похожих друг на друга классов полезно применять наследование...
------------------------------------
А какие ещё способы построения иерархии (помимо наследования) вы практикуете?

Смысл интерфейса не в построении иерархий, а в жёстком затребовании от класса реализации необходимой функциональности, которая при этом будет доступна строго определённым образом. Если есть общая реализация, то можно ввести дополнительный слой в виде абстрактного класса для некоей ветви.
Еще практикую наследование интерфейсов от интерфейсов :) Страшно звучит да?
Вообще говоря не представляю как можно строить систему классов в отрыве от их состояния.
Не, не страшно. Только я спросил - что помимо наследования? :-)
Наследование никто не отменял. Просто иерархии должны строится правильно. Лучше всего если по принципу подстановки Лисков.

А вообще мы все в любом случае думаем в терминах интерфейсов. В ооп, не разрушая инкапсуляцию по другому невозможно. Любой класс задаёт так же и интерфейс (интерфейс в общем смысле, а не конструкция языка), который он сам и реализует. Используя любой объект, вы делаете это через его интерфейс. Если объект наследует от кого-то, то мы программируем, используя интерфейс задаваемый этим самым суперклассом.

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

Насчет последнего пункта: Например доступ к некторому контракту должны иметь только доверенные лица, кому попало его вызывать нельзя. Скрытие имен интерфейсов то бишь.
Однако приведя класс к приватному интерфейсу его может выполнить кто угодно. Это как раз вина интерфейсов, они по природе своей публичны :)
Уважаемый, скажу вам по секрету, что абстрактный класс тоже может имплементить интерфейс! Никто не говорит, что абстрактные классы зло, просто в данном конкретном случае он неуместен. Объясню: интерфейс Ключ не может быть абстрактным, потому что природа открытия двери в его реализациях полностью различна. В одном случае метод +open() использует механизм открытия путем поворота самого ключа, в другом, используя карто-приемник. Вопрос - какую имплементацию вы хотите засунуть в интерфейс Ключ, сделав его абстрактным классом?
Если у нас будет несколько имплементаций ключей одного типа(например ключ с секретом, который нужно надавить как-нибудь, прежде чем поворачивать и обычный ключ) имело бы смысл заимплементить интерфейс Ключ в абстрактном классе КлючПоворотный и заэкстендить этот класс в классах ОбычныйКлюч и КлючССекретом.
Автору - респект! Отлично описал, но до некоторых все равно не доходит.
Хорошо, что на хабре есть таки люди которые понимают дело правильно. :-)
Приводя пример ключа и карточки ты нарушаешь один из принципов, который в этой книге также написан - [открытое] наследование применяется, когда наследник "является" предком. В приведённом тобой примере магнитная карточка не "является" ключом, потому что (из твоих слов) ключ при открывании выполняет операции, которые карточка выполнять не должна. И если при переопределении метода "открыть" мы упускаем важное изменение информации, значит карточка никак не может "являться" ключом, и значит наследование этого метода здесь использовать нельзя. Нужно либо делать его закрытым либо вообще не наследовать. То же самое касается других типов карточки - они не "являются" ключами, и поэтому их нельзя наследовать.

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

Чесно говоря, мне не понятна фраза "наследование нарушает инкапсуляцию".. Неправильное - это понятно... А вот чем обычное не угодило...?
Наследование потому нарушает инкакпсуляцию, что может давать способ доступиться к данным, которые закрыты. Технически это порой удобно, канонически - принцип нарушает.
Это в каком языке наследование позволят доступиться к закрытым данным?
Да практически в любом. Только не нужно упрощённо понимать под закрытыми данными private переменные. Закрытыми данными, к примеру, может являться реализация некоего интерфейсного метода. Унаследовав класс мы можете её переопределить, не переопределив правильно другие методы которые он пользует. Всё это есть нарушение инкапсуляции (сокрытия).
Это есть неправильная реализация наследования. Неправильно писать код можно многими способами. Наследование в этом плане не лучше и не хуже чего-то другого.
Код надо писать так, что бы даже случайно его нельзя было испортить. Потому и говорят, что наследование есть не самый удачный подход. И он хуже других подходов. Это, разумеется, не значит что на нём надо ставить крест.
Копипаст всяко не лучше :)
Строгое понятие "является" не совпадает с жизненным. В жизни квадрат является прямоугольником, а при наследовании - не является, потому что у прямоугольника можно изменить длину и ширину, а у квадрата нельзя.
Точно так же приведённая в примере карточка не является приведённым в примере ключом.
В жизни квадрат является частным случаем прямоугольника. И у него МОЖНО изменить длину и ширину. Но в отличие от прямоугольника это изменение должно делаться сразу для обоих величин (ибо они равны). Так что всё верно. Как и то, что карточка является ключом. Даже ввод пароля на Хабр с абстрактной точки зрения также есть реализация Ключа. Учимся-учимся мыслить абстрактно.:-)
>это изменение должно делаться сразу для обоих величин
Именно поэтому квадрат не является прямоугольником. Если бы квадрат являтся прямоугольником, то все утверждения(контракты), верные для прямоугольника были бы верны для квадрата. Например, контракт "если мы установим ширину в 20 а длину в 10 то ширина будет равна 20 а длина 10" выполняется для прямоугольника, но не выполняется для квадрата. Поэтому квадрат не является прямоугольником с точки зрения наследования.
В моём миру квадрат таки является прямоугольником. Уж звиняйте. :-)
О, пришли неэвклидовы геометры и стали минусовать за то, что назвал квадрат прямоугольником. Видимо аудитория хабра серъёзно молодеет. Пошли волна регистраций из начальных классов и детских садов.
Минус за переход на личности
Пасиб. Как мог я попытался пояснить что общего у двух фигур и почему. :-)
Правило, которое говорит, что наследование моделирует взаимоотношение "является", определяет это самое "является" с точки зрения поведения объекта. Это называется принцип подстановки Лисков. Объект в иерархии наследования должен соблюдать контракт своего суперкласса. То есть в частном случае с прямоугольником контракт предлагаемый суперклассом “прямоугольник” утверждает, что, вызывая метод setWidth(), вы измените ширину фигуры и только её. Квадрат нарушает этот контракт, изменяя также и высоту, поэтому ему не место в этой иерархии. В обще случа,е все объекты иерархии должны свободно заменяться друг на друга без нарушения работоспособности системы, потому что они все "являются" объектом их суперкласса, поскольку носят его тип. Квадрат это классический пример этой проблемы. Так же в книгах очень популярен пингвин. Хотя все мы знаем, что он птица, он не умеет летать, поэтому наследовать пингвина от птицы неправильно. Он нарушает поведение, которое должно быть у класса имеющего тип птицы.
Вот тут логическая ошибка: ...контракт предлагаемый суперклассом “прямоугольник” утверждает, что, вызывая метод setWidth(), вы измените ширину фигуры и только её...
Дописка ...только её... лишняя. Правильный метод класса должен следить за тем, что бы сохранить сущность этого класса. Посему в прямоугольнике это будет только изменение ширины. А в квадрате и длины также. Но в обоих случая сохраняется важный принцип: меняется ширина и сохраняется логическая суть фигуры.
В том то и дело, что в данном случае логическая суть фигуры не важна. Важно только её поведение. Квадрат ведёт себя не так, как прямоугольник, поэтому квадрат не должен наследовать от прямоугольника. Этот момент сложен для понимания, поскольку противоречит общепринятым понятиям. Но с точки зрения правильной иерархии наследования квадрат это не прямоугольник. Определения из геометрии не важны, в программировании далеко не всегда можно переносить существующие в реальном мире связи в модель домена нашей программы.

Возможно, пример с setWidth не самый лучший. Скажем, есть класс прямоугольника и у него есть функция setDimensions(int width, int height).Этот метод вполне хорош для прямоугольника. Для квадрата он не имеет смысла, потому что квадрат ведёт себя не так как прямоугольник. Клиентский код, вызывая эту функцию ждёт, что изменится и то и другое на заданные им величины. Что же делать квадрату? Если он изменит и то и другое это может нарушить работу клиентского кода. Значит, классы прямоугольник и квадрат нельзя свободно заменить друг на друга в программе и это нарушает принцип подстановки.

Надо отметить, что всё это верно только с чисто концептуальной точки зрения. В реальном коде можно и на компромисс пойти, если это сделает код лучше, например уберёт избыточность.
Логическая суть фигуры очень важна, ибо именно она и диктует её поведение. Смысл ООП проектирования в том и состоит, что надо уметь находить абстракции и выделять из них наиболее общие. Прямоугольник является достаточно общей абстракцией дла квадрата ибо во многом их сущность и поведение идентичны. За одним исключением (равенство сторон).
Если же следовать вашей логике, то абстракций практически нет, ибо у каждого типа есть что-то особенное. И оно действительно есть, вот только смысл абстракций в том, что надо уметь соотносить чего в двух типах больше: сходств или различий.
Находить абстракции и выделять из них наиболее общие это главная задача скорее объектного анализа. А при переходе от анализа к кодированию иерархий, как раз и вылезают всяческие проблемы. В начале девяностых бытовало такое мнение: давайте переносить объекты в домен приложения один к одному, так мы решим всё проблемы с проектированием архитектуры. Примеры типа прямоугольника как раз и приводились, чтобы показать, что тут тоже есть свои сложности, и все решения надо принимать индивидуально.

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

Насчёт дублирования кода есть ведь разные варианты его устранения можно использовать делегирование, можно использовать миксины (если позволяет язык)
А какие-такие подводные камни у этого примера? Оно ведь и понятно, что абстракция потому и абстракция, что у реальных типов будут незначительные отличия. Ставить их во главу угла (Ах!, а вы слыхали, что у квадрата ширина с длиной одинаковы, а значит это ... кашмар какой... не прямоугольник!!!). Так что никакого скептицизма тут нет. Есть чёткое понимание термина - абстракция. А насчёт более гибких и безопасных подходов к реализации отличий тут уже обсудили. Наследование общей части, добавляя отличия в потомок - не лучший подход. Паттерн Strategy (он же - Inversion of control. Он же - Dependency injection) есть более гибкий и безопасный метод.
Ну я и говорю делегирование.
Кстати инверсия зависимостей это гораздо более широкое понятие, чем паттерн стратегия. Инъекция зависимостей тоже, она конечно тесно с ним связана, но не тоже самое.
Чёрт его знает. Я не понимаю почему инверсия зависимостей есть более широкое понятие. Это разумеется делегирование, но делегирование это что-ли технический термин встраивания ссылки. А вот Стрэтеджи это и есть паттерн постулирующий тот факт, что некая ссылка на поле обладет предопределённым интерфейсом и может быть легко подменена другой реализацией. Весь Core Spring на этом и построен.
Когда говорят, что квадрат является прямоугольником в математическом смысле, то не предполагается изменения размеров фигур. Поэтому противоречия нет, т.к. setWidth не включается в математический интерфейс. Квадрат является прямоугольником потому, что у него 4 стороны и 4 прямых угла. Класс Квадрат должен унаследовать именно эти инварианты (тут можно как раз избежать дублирования кода).

Теперь положим, что нам все же нужен метод setWidth у Прямоугольника. Введение возможности изменения размера прямоугольника приводит к появлению у него специфического контракта, который не должен переходить Квадрату, имеющему свой контракт изменения размеров. Получается, что эти фигуры имеют как общие, так и различные черты, а это означает, что они должны наследовать от общего абстрактного класса фигуры с 4-мя сторонами и 4-мя прямыми углами, а методы setWidth получать, например, от интерфейса ИзменяемаяФигура и реализовывать независимо.
Ну или в квадрате этот метод будет также менять и другой параметр, сохраняя сущность квадрата.
Именно. Получается, что контракты методов setWidth и setHeight в интерфейсе ИзменяемаяФигура подразумевают только факт (возможного) изменения размера. А Квадрат и Прямоугольник, реализуя эти методы, добавляют к их контрактам собственные дополнительные условия. Аналогия с введением при наследовании собственных полей классов.
Так для этого и существует полиморфизм. Именно и что бы ввести отличия конкертного типа от изначально постулированной абстракции. :-)
Квадрат является частным случаем прямоугольника, а прямоугольник, к примеру, частным случаем четырёхугольника. ООП даёт возможность делать свою иерархию, но это вовсе не значит, что она будет правильной. Так можно сказать, что солнце зелёное, объяснив это тем, что разговорный язык позволил сделать подобное, значит данное утверждение - истина.

>Квадрат ведёт себя не так, как прямоугольник, поэтому квадрат не должен наследовать от прямоугольника. Этот момент сложен для понимания, поскольку противоречит общепринятым понятиям.

Квадрат ведёт себя именно как прямоугольник, любой прямоугольник с одинаковыми сторонами - квадрат, но не любой квадрат является прямоугольником. У всех квадратов есть нечто общее, это все свойства и методы применяемые к прямоугольнику, потому именно прямоугольник будет стоять в основе иерархии.
Тьфу ты сам себя тут с вами запутал :) Любой квадрат является прямоугольником.
P.S. Вот что значит вместо того, чтобы писать как положено, обсуждать всякую дурь.
Пишу на этот раз правильно: не любой прямоугольник является квадратом.
Не любой. Но любой квадрат является прямоугольником. Я тут битых шесть часов это хабравцам поясняю. А они в отместку минусуют. Видимо по человечески не согласны. Ну чтож.. будем считать что поэтам всё простительно... :-)
А я вот пояснил, но под утро случился логический прокол, вследствие чего не очень хорошо получилось. Ну да ладно, если писать классы, то там особо поизголяться в логике не получится.
если прямоугольнику задать ширину равную высоте - получит ли он специфичные "квадратные" методы?
нет? тогда это дрянная декомпозиция =)
если setWidth() устанавливает не только ширину, но ещё и длину, то это плохой метод.
У квадрата нельзя устанавливать ширину отдельно от длины.

Собственно говоря, у квадрата вообще не должно быть отдельно ширины и длины, должно быть какое-то одно свойство, в отличие от прямоугольника.
именно поэтому квадрат нарушает интерфейс прямоугольника. и такое наследование будет нарушать принцип подстановки.
Да-да, совершенно верно.
Квадрат является частным видом прямоугольника с точки зрения геометрии.

С точки же зрения наследования интерфейсов квадрат не является прямоугольником. Более того, хрестоматийный пример: окружность является точкой. У Точки есть координаты и цвет; у Окружности они тоже есть (координаты — как координаты центра), плюс ещё радиус.
Мы не переопределяем метод Открыть. Мы его реализуем. Потому как его застолбил интерфейс. И карточка и амбарный ключ действительно реализуют интерфейс Ключ. Всё у автора верно. Воду из ведёрка он не черпает. :-)
У автора мы именно переопределяем метод "открыть", потому что он уже реализован в базовом классе.

А реализует амбарный замок интерфейс ключ или не реализует - определяется исключительно тем, как программист определит абстракцию предметной области.
Процитирую автора:

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

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

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

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

Читаем - ...класс Поворотный Ключ, РЕ-А-ЛИ-ЗУ-ЮЩИЙ интерфейс Ключ
Читаем - ...класс Магнитная Карточка, тоже РЕ-А-ЛИ-ЗУ-ЮЩИЙ интерфейс Ключ
Не там читаем. Я говорил про "плохой пример".
Я все же не стал бы называть С++ отсталым языком. Просто в нем нет ничего лишнего. Полностью абстрактный класс - это и есть интерфейс, как ни крути. Наличие дополнительных ключевых слов в языке в общем-то ничего не меняет.
Абсолютно верно. Интерфейс — это понятие, а не ключевое слово interface. В различных языках понятие может быть выражено разными способами. И способ C++ — это синтаксис такой, а не хак.
Всё верно, интерфейс можно смоделировать абстрактным классом. Можно даже и обычным. НО! Это потенциально опаснее и мене наглядно. Посему в современные языки и были введены соответствующие ключевые слова.
Хм, все же это наверное зависит от стиля программирования. Добавляйте к каждому названию интерфейса префикс "I" и получите наглядность. В коде такой класс тоже отчетливо виден - все методы ограничены virtual и =0. Все же насколько я понимаю интерфейс в том же Java или C# абсолютно идентичен _полностью_ абстрактному классу C++, так где же потенциальная опасность? Если ошибаюсь - поправьте
Интерфейсы в Java и C# не могут иметь члены-данные. В остальном идентичны.
полностью абстрактный класс не будет полностью абстрактным, если будет содержать данные :) Зачем данные полностью абстрактному классу?
:)идентичен _полностью_ абстрактному
Отнес второе слово к первому.
С небольшими оговорками это так. Но наследовать можно только от одного абстрактного класса, а от интерфейсов сколько надо. Интерфейсы появились как компенсация множественного наследования, которое предали анафеме. Опять гибкость пострадала в угоду беспроблемности. Впрочем это может быть и хорошо.
конечно, не нужны :)
Я как то пропустил слово "полностью".
И не могут иметь никаких реализаций методов!
Неа, абстрактный клас гораздо круче интерфейса. Больше может и умеет.
А танк круче чем Феррари. Тяжелее и умеет стрелять.
Хотите сказать танк идентичен феррари?
С точки зрения перемещения в пространстве - да. С точки зрения лобового столкновения - танк круче!
Так и есть. А вот мы в C#, например, бывает решаем использовать интерфейсы или абстрактные базовые классы исходя из возможностей наследования. Там можно реализовать любое количество интерфейсов, но унаследоваться можно только от одного базового класса.
С точки зрения ООП, лишнего там вагон и маленькая тележка. Зато нужного - не хватает. С точки зрения эффективности - ситуация другая.
Так то оно так, но полностью абстрактный класс, содержащий исключительно чистые виртуальные функции может содержать и переменные, что строго говоря для интерфейсов недопустимо.
Мда... С какого времени абстрактный класс в С++ стал "хаком"? Вполне нормальное средство. Замените слово "интерфейс" в своем языке на С++шний "абстракт", и получите "интерфейс". В чем разница наследованием интерфейсов и классов?
Правда в С++ есть и другие проблемы, с основном связанные с криворукостью программистов. Это язык на котором нельзя писать, если не понимаешь, что делаешь. В случае упомянутого языка это приведет к совсем плачевным последствиям, в отличии от, скажем, бэйсика.
Скорее С++ оружие профессионала, нежели детская игрушка.
писалось оновременно с ответом VlexZ. Нужно мысленно прикрутить выше.
Крест поставил, как смел выразиться автор:)
Мне кажется очень много написано. "Песня долгая, зато нудная"(©УПИ). Нехватает емкого, короткого определения. Должно звучать как выстрел, если хотите, приговор. А сейчас за деревьями леса не видно.
Класс - сущность, которая задает некоторое общее поведение для объектов.
Интерфейс - это контракт взаимодействия между классом и внешним миром.
И все вроде ясно:) Остальное расскажет код;)
Точных определений в книжках дофига и больше. А понимания вот у многих нет. И код это очень хорошо рассказывает. :-)
Интересно, а в Smalltalk (как первой реализации ООП) были интерфейсы?
Smalltalk использует подход основанный на duck typing (как в ruby, например). Вызов метода рассматривается, как сообщение посылаемое объекту. Соответсвенно, вы можете вызывать любой метод у любого объекта лишь бы он ответил на него. Интерфейсы, как особые конструкции языка не совсем сочетаются с таким принципом.
Идеально. Я не знаю ни способов рассказать эту тему лучше, чем автор, ни слов, могущих полностью выразить моё восхищение. Большое спасибо!
UFO just landed and posted this here
> Сам как раз GoF читаю
Goblet of Fire? )
Gang of Four, я так понимаю.
UFO just landed and posted this here
Я вот не могу понять, откуда берётся мнение, что в php (пятая версия, конечно) неполноценное ООП? Инкапсуляция присутствует, наследование имеется, паттерны GOF реализуемы. Что же ещё надо? :)

Или вас смущает, что в языке изначально ориентированном на duck typing появились интерфейсы и type hinting?
UFO just landed and posted this here
Этот Ваш чудик на http://valera.ws/2007.11.11~oop_in_php пиарит .NET и делает вид, что Java не существует. Тактично не договаривая, что NET во многом калька с куда как более серъёзной технологии Java.
UFO just landed and posted this here
Это не я спросил. Я не писал, не пишу и никогда не буду писать на PHP. Для меня это соседняя тупиковая улица на которую люди забрели по юношеской наивности. Популярность этго языка обусловлена всего-лишь низким порогом вхождения, который был сделан для того, что бы популяризовать WEB для чайников. То, что по этой дорожке пошло много профессионалов, никак не делает этот язочёнок хоть сколь-либо интересным и мощным.
UFO just landed and posted this here
Я и говорю, профессионалы также пошли на этот путь. Оно понятно, что мастеру всё равно чем "махать", но при прочих равных, более мощным и удобным инструментом вы "махали" бы куда как более эффективно. Просто в силу эффективности инструмента. Оно и лук мощное оружие для опытного полководца, но с автоматом как-то сподручнее. Особенно для того же мощного полководца. :-)
UFO just landed and posted this here
Минусы не от меня. :-)
Я не ставлю, я их только получаю, пытаясь пояснить, что квадрат это также прямоугольник. :-)
UFO just landed and posted this here
О, сча пыхатели до гроба тут пересчитаются... ))))
полиморфизм подразумевает возможность динамически подставлять вместо одного объекта другой, при условии идентичности интерфейсов. при чем тут жесткая типизация?
UFO just landed and posted this here
Прочитал. Это всё несеръёзно. Не верьте ему :). Эти проблемы не делают ООП в php неполноценным. Некоторые из них так и вообще отношения к ООП не имеют (перегрузка то причём). Рассматривайте статью лишь как частное мнение автора о том каким бы он хотел видеть язык. Если брать в расчёт только пятую версию с ооп всё хорошо в пхп, да кое что неудобно, но принципы соблюдены.
Интерфейсы (как и перегрузки) НЕВЕРОЯТНО усложняют объектную модель, поэтому надо изначально правильно планировать объекты...для примера ваш пример:
Открыть(Метод родительского класса) -> Вставить, Провести и Вынуть (Методы дочернего класса1)
-> Приложить магнит(Метод дочернего класса2)
Надо вообще сразу писать код без ошибок и лучше на ассемблере. Причём писать так, что бы никогда потом не менять ибо в нём изначально всё было учтено. Свежая мысль. А главное - Мудрая.
Согласен...редкая птица долетит до середины Днепра, как и редкий проект сразу все предусмотрит :)
Хорошая статья, с точки зрения "пропаганды" интерфейсов.
Но пример, имхо, уж сильно неудачный. Должен ли Ключ уметь открывать пиво? Банку с консервами? Выдвижной ящик в столе? ;)
Зависит от уровня абстракции. Если интерфейс Ключ заявлен как предмет для открытия дверей, то штопор - не катит. Если как предмет для вскрытия чего бы то не было, то штопор есть также реализация ключа. То есть этот уровень уже диктует Ваша задача.
Просто у меня тут крамольная мысль возникла, что Ключ вообще не должен ни чего открывать ;)
В Вашем примере, он на самом деле пассивен, по сути своей, и используется лишь как вспомогательное средство для открывания двери.
На мой взгляд, именно игнорирование этого и было первопричиной трудностей, а вовсе не наследование реализации.
Разумеется, очень имхостое имхо.
Ключ и не должен ничего открывать. Назначение интерфейса Ключ - сказать, что для того, что бы что-то открыть, надо вызвать метод Open(). Именно его и с таким названием. А что он фактически будет делать, зависит от реализации. Возможно он будет как раз закрывать бутылку, дабы открыть мозг для ясности восприятия. :-)
Всё равно не согласен. И имхо, не должно быть в интерфейсе ключа метода Open :)
Потому что главное в ключе не то, что он может всовываться и поворачиваться, а структура зубчиков ;)
А то, что старые модели ключей для передачи структуры зыбчиков надо всовывать и поворачивать - это всего лишь недостатки реализации ;)
Структура зубчиков главное в реализации тяжелого амбарного ключа. А в интерфейсе ключ главное тот факт, что тому кто будет пользоваться реализацией этого интерфейса, будет достаточно вызвать метод Open(), дабы что-то нужное открыть. В этом его божественное предназначение.

Угу. Я и раньше предполагал, что серъёзный уровень абстракции доступен лишь одному проценту людей. Остальные выглядят как слепые котята. Грустно конечно. :-(
Угу, есть у меня один знакомый, который как-то в одной ситуации предлагал сделать один из параметров интерфейса строковым, под предлогом, что в строку всё что угодно сериализовать. Тоже о высоких материях рассуждал, об абстракции.... Дескать, смотрите как у нас всё абстрактно будет - мы же всё, что угодно, в строку запихать можем!
А в Вашим примере - ключ не должен ничего открывать, он должен доказать, что его обладатель имеет право на окрытие чего-либо. Каким образом это передаётся (структурой зубчиков, данными из встроенного чипа или штрих-кодом - дело реализации).
Это и есть главное, что их объединяет - то что все ключи (будь они комнатные, амбарные или магнитные) являются пропуском.
А если этого не понимать - разумеется буду проблемы при наследовании реализаций. Вы выстроили архитектуру, которая требует замещения наследуемой реализации, вместо её расширения, и говорите "посмотрите, какая гадость это ваше наследование реализации"!
Разумеется, когда мы полностью замещаем унаследованную реализацию - это плохой признак. Который говорит, что c архитектурой что-то не то, что у нас в одной куче несовместимые вещи.
Вы бы ещё гаечные ключи туда добавили...
ЗЫ: Интерфейсы - замечательная штука, и ни одна мало-мальски серьёзная система без них нормально построена быть не может, но наличие интерфейсов само по себе не избавляет от продумывания архитектуры ;)
Сумбурно вы написали. Ограничусь постулатами. :-)

1. Продумывание интерфейсов это и есть часть продумывания архитектуры.
2. Интерфейс Ключ не должен ничего открывать и не должен ничего доказывать, он должен однозначно информировать как им пользоваться. Точка.
3. Делать параметр строковым - не имеет никакого отношения к правильному уровню абстракций, это лишь некая техническая гибкость за счёт использования более свободного типа данных. Это не всегда хорошо. Иногда как раз нужна жётскость. Ещё раз - это не ключевая абстракция, никаких выводов тут делать нельзя.
Да, пожалуй, что сумбурно. Органичусь и я постулатами, ибо мы кажется спорим вообще о разных вещах :)
1. Совершенно согласен: строить архитектуру на базе интерфейсов - можно, нужно и вообще это единственный правильный путь, который завещал Великий Ленин. Алюминь!
2. Совершенно неуместно доказывать плюсы интерфейсов, используя в качестве аргумента кривую архитектуру, особенно утверждая, что всякое наследование реализации - это зло.
3. Принцип Интерфейс < - имплементация < - расширенная имплементация - это есть не только приемлемо, но часто даже гуд.
4. Принцип Интерфейс < - имплементация < - замещённая имплементация - это в большинстве случаев есть бэд, а часто и вообще неприемлемо.
ЗЫ: Будучи увлечён в круговорот своих сумбурных мыслей, я Вас спутал с автором топика. Искренне прошу пардона.
Ну вот видите, мы по сути во многом согласны. :-)
И да, гаечный ключ - добавлю. По сути, такой же ключ как и амбарный или карточный. Им открывают моторы. Ну и что, что им надо вертеть? Это уже специфика реализации. Главное то, что им открывают! Можно на поддон картера и магнитную карту повесить. Будет работать, не сомневайтесь. Просто не целесообразно. ;-)
Соглашусь с этим только тогда, когда ключ от Вашего запертого амбара будут свободно продавать в магазине всем желающим. Вместе с гаечным ;)
Для пущей ясности, озвучу свою позицию в терминах интерфейсов:
В моём понимании, ключ от амбара (квартиры, сейфа в банке) имплементирует IPermission, а гаечный ключ - какой-нибудь ITool, или IHelper. И поэтому по сути своей, они совершенно разные, хоть и называются одинаково, и крутиться могут :)
Может быть, так будет яснее, что я имею в виду.
А если мы их все пытаемся втиснуть в рамки одной иерархии - разумеется, будут проблемы. Это я и пытался сказать с самого начала, что автор в примере смешивает действие ключа как механического tool-а (вставить, провернуть и т.д.) и как средства получения доступа к чему-то.
Теперь менее сумбурно? :)
не-не-не... Вы всё правильно пишете, но вы ненавязчиво привнесли дополнительный уровень защиты. Автор привёл простейшиё пример - ключ (со всем барахлом типа паролей/кодов/защит) в руках у вас и вы только счёлкаете пальцами и говорите - Open! Это же был учебный пример. Не нужно его рассматривать с точки зрения аутентификации и авторизации. Опять же - абстрагируйтесь... ;)
Если хотите что бы не всем желающим, добавьте в интерфейс параметр пароль. :-)
... который будет очень уместен в реализации гаечного ключа ;)
В общем случе - да. Но если вам это кажется надуманным, абстрагируйтесь и примите тот факт, что этот параметр уже механически реализован в зубцах физическокго ключа. Уровень абстракции при этом сохраняется. ;)
Учиться на плохихи примерах тому, как проектировать систему не есть хорошо, и тем более приводить подобные примеры в качестве показательных.
Использование String в качестве хранилища данных очень похоже на оперирование объектами Object. Впринципе нас окружает куча объектов и по сути Ключ тоже объект. Но суть грамотного проектирования в том, чтобы остановиться на каком то логическом уровне абстракции, позволяющем расширение свойств объекта, и в то же время недопускающем наделение объекта свойствами, ему не присущими. Поэтому, допустим в примере с ключом, автор не выделил например интерфейс ХреньВКармане или ВтыкательВоЧтоТо. А ограничился простым интерфейсом - Ключ.
У метода Открыть, например, может быть параметр, указывающий, что открыть; и, конечно же, не каждый ключ сработает с каждым открываемым объектом — он будет возращать FALSE или выбрасывать исключение в случае неудачи…
Кстати, а каким он должен быть, с Вашей точки зрения? IKey::Open(ISomething)?
Key::Open(Openable) тогда уж.
Впрочем, здесь ещё подумать надо. Может, лучше другой способ применить — в зависимости от поставленной задачи.
Ага, отчего уж тогда не Opener::Open(Openable)? ;)
Только вот почему-то IComparer/IComparable у меня не вызывает неприятия, а вот Opener/Openable - вызывает. Сижу вот и думаю, почему.
Интересно, это только у меня так?
Эти размышления, подозреваю, имеют не больше смысла, чем выбор между Key и Opener :-)
Возможно.
Но тогда, имхо, неизбежно нужно признать, что если Comparer/Comparable и Opener/Openable ничем не отличаются, то какой-нибудь IObject тоже вполне имеет право на жизнь, c каким-нибудь методом Object::DoSomething(Subject).
Т.е. я думаю, что абстракция должна всё же иметь некие рамки, и мне очень интересно понять для себя, исходя из какого принципе эти рамки ставить.
Как вариант: Comparer/Comparable даёт хоть какое-то представление, чего можно ожидать в результате, а вот Opener/Openable - уже не даёт. имхо.
Т.е. понятие интерфейса как "контракта взаимодействия" должно всё же нести за собой некую семантику этого взаимодействия.
Блог разработка, по-моему, подходит для этой интересной статьи.
Не интересно! Не понимаю, неужеле здесь столько людей не различающих такие понятия?! И вообще из-за таких товарищей программистов - "наколеночников" все проблемы в софте ;-)
Да ну, дрочево какое то с ключами.
Все гораздо проще: хороший интерфейс подразумевает поведение. И все. Не больше не меньше.

Руль с педалями — интерфейс. А к чему оно будет прикручено, к автомату с газировкой, компу или трактору — детали реализации. Но бредово рулем писать тексты, хотя в игровых автоматах такое бывает. )) У них такая реализация.

Можно добавить еще интерфейс руль2 — получится руль с подрулевыми переключателями. Потом руль3 — еще и с кнопками. А за рулем такое нагромождение "классов" скрывается, что об этом даже и думать страшно :)

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

Если судить в рамках вашего примера, то наследования от интерфейса также нарушает инкапсуляцию, поскольку интерфейс обязует класс раскрывать детали своей реализации путем имплементации определенных методов.

Наследовать магнитную карту от ключа - логически неправильно. А наследование само по себе может быть public, protected, private и т.д., т.е. можно лавировать как угодно. А атрибуты видимости классу на то и данны, чтобы скрыть некоторые детали своей реализации даже от потомков.
Статья интересная. Но было бы лучше, если бы автор вначале явно сформулировал само определение интерфейса. Оно достаточно простое. Интерфейс — это способ взаимодействия двух сред.

В статье рассматриваются 2 среды: человек в роли субъекта и дверь в роли объекта взаимодействия. Подразумевается, что Ключ — это интерфейс между двумя субъектами. Я считаю, что это не совсем корректно. Ключ — это просто условное обозначение инструмента взаимодействия.

В роли ключа (инструмента) может быть вообще что угодно: голос, например. Интерфейс же нам определяет способ взаимодействия через инструмент. Для поворотного ключа интерфейс один (механический: вставить ключ, повернуть, вынуть), для карты - второй (считывающий: вставить карту, провести, вынуть, дождаться разрешения), для звукового ключа - третий (назвать кодовое слово, дождаться разрешения).

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

Я правильно понял вашу идею, что чтобы сделать класс, отличающийся от существующего одним методом, надо создать класс, определить в нем все методы (например, делегирующие другому классу) и в один метод внести правку? Мне кажется, это излишняя работа. Гораздо проще отнаследовать класс и переопределить в нем только один метод. DRY.
Нет, неправильно. Но если взять похожую идею — чтобы реализовать дополнительную функциональность, вместо наследования прибегнуть к делегированию всех методов плюс сделать что-то своё, — то мы получим паттерн Декоратор, и это вовсе не моя идея.

Что касается процитированного эпизода, то подумайте, при чём здесь может быть наследование — ни Магнитная Карточка не является видом Бесконтактной, ни наоборот.
Да ладно? Магнитная карточка является частным случаем Бесконтактной.
Неужели? Бесконтактную подносят к замку, а магнитную вставляют и проводят. Общего у них ничего, кроме форм-фактора, нет.
Ладно, с карточками ясно. Допустим у меня есть класс "Машина". Умеет кучу всего. Открывать двери, закрывать двери, блокировать двери, заводиться, ехать, увеличивать скорость, уменьшать скорость, выключаться, открывать капот, закрывать капот, ну и так далее. Теперь я хочу сделать класс "Кабриолет". Он умеет все то же самое, что и "Машина", но вдобавок может "открывать крышу" и "закрывать крышу". Как вы рекомендуете сделать этот класс? С помощью каких механизмов/конструкций?
Здесь либо наследование, либо декорация.

Если мы предполагаем, что умение открывать-закрывать крышу может быть применено не только к машинам, то лучше сделать декоратор.

Если только к машине — то можно унаследовать кабриолет от машины.

Но вообще, мне кажется, лучше всё же декоратор.
Ага, значит все-таки есть случаи, когда полезно наследовать реализацию? :)
Никто и не говорит, что их нет.
Предположим, у нас некоторые машины оборудованы кондиционерами, а некоторые — нет.

Если использовать наследование, то придётся создать ещё классы Машина-с-Кондиционером и Кабриолет-с-Кондиционером.

А ещё некоторые машины оснащены противотуманными фарами.

Получаются классы Машина, Кабриолет, Машина-с-Кондиционером, Кабриолет-с-Кондиционером, Машина-с-Кондиционером-и-Противотуманными-Фарами, Кабриолет-с-Кондиционером-и-Противотуманными-Фарами, Машина-с-Противотуманными-Фарами, Кабриолет-с-Противотуманными-Фарами.

Почему-то мне это не нравится, а вам?
Про фабрику классов я в курсе :)
Интерфейсы и перегрузки - это ядерное оружие, попадет не в те руки и хана проекту :)
Почему? Я начал изучать Java и у меня сложилось впечатление что интерфейсы _упрощают_ понимание кода.
Может я глупость скажу, но если совсем абстрагироватся, то помимо ключа, есть ещё и замок, и не факт, что реализация метода open() ключа подойдёт к методу open() замка.

Теоретически должен быть ещё один класс который принимает метод ключа, метод замка, и проверяет подходит ли реализация.
Да, конечно же, с одной стороны — ключ, а с другой — замок, здесь ещё поразмыслить надо, как их связать.
Обычно Ключи делают к замку сразу, мастер знает как устроен замок и ключ делает именно так чтобы он подходил к замку, и потому интерфейс замка известен.
А когда своим ключом пытаешься открыть неизвестный замок — это уже попытка взлома или попытка создать универсальный ключ)
Боже, наконец-то!!! Спасибо!!!
Нам, блондинкам, тут очень тяжело(
Sign up to leave a comment.

Articles