Как стать автором
Обновить

Комментарии 349

НЛО прилетело и опубликовало эту надпись здесь

А это общее правило параноика для любого ЯП (хотя больше всего его любят в C++): если вы до конца не определились как вам оформить класс, то делайте его приватным, делайте его конструктор приватным, делайте все методы приватными и невиртуальными, и запрещайте наследование. Чтобы изменить любой из этих пунктов нужна вполне конкретная причина.

НЛО прилетело и опубликовало эту надпись здесь

Не надоедает, если понимать, зачем это делается. Конечно, нехорошо, что лучшая практика штрафуется, но этот штраф довольно легкий и требует всего лишь выработки одной хорошей привычки.

Или пары шаблонов в студии.
НЛО прилетело и опубликовало эту надпись здесь
Я с IDEA не работал, но разве она не поддерживает написание своих сниппетов?
НЛО прилетело и опубликовало эту надпись здесь

Я же написал в явном виде — "нужна причина" :-) У вас причина есть, соответственно вы осознанно будете делать классы/методы/конструкторы паблик. Но не у всех есть такая причина.

А на мой взгляд приватный класс по умолчанию — очень некрасивый шаг по отношению к пользователям библиотеки.

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

Меня всегда удивляло стремление отдельных людей выдумывать «правила», а потом героически их преодолевать!
Инкапсуляция на 100% — это утопия, так не бывает в жизни. ООП — лишь средство более элегантно решить некоторые (но далеко не все) проблемы, но не нужно его идеализировать и обожествлять! Если предоставить «клиенту» посмотреть на «внутренности» — это не плохо, просто «клиент» (пользователь библиотеки), принимая решения, должен осознавать последствия сделанного им выбора. Я уверен, что разумный доступ к внутренностям объекта (и одновременным пониманием, чем это чревато) позволяет писать более эффективный код, и делать это быстрее, а не тратить время на героическое преодоление правил. Правила должны помогать осуществлять задумки, а не мешать им. А вот ответственность за сильное связывание, спагетти-код и т.д. лежит не на языке, и даже не на библиотеке, а на конкретном разработчике ее использующем.
И если библиотека хорошо выполняет свою задачу, пусть она трижды вся public, я буду ее использовать, и скажу ее разработчику лишь спасибо!
Меня всегда удивляло стремление отдельных людей выдумывать «правила», а потом героически их преодолевать!

Я не пропагандирую это "правило параноика" и сам ему не следую. Не это конкретное правило, но правила вообще имеют смысл когда уровень квалификации "среднего" участника команды приводит к регулярным наступаниям на грабли. С точки зрения поставки фич бизнесу всегда проще сказать "всегда делай вот так", чем ждать пока коллега соберёт все грабли, пропустит все дедлайны, научится и станет делать "правильно" осознанно.


И если библиотека хорошо выполняет свою задачу, пусть она трижды вся public, я буду ее использовать, и скажу ее разработчику лишь спасибо!

Ну не всё так просто:


  • У разработчика при таком подходе будут связаны руки: т.к. всё public и всё часть контракта библиотеки, нельзя будет просто так пойти и отрефакторить что-то. Каждый новый класс — это minor релиз, каждое переименование любого класса — это major релиз. Patch релизы будут предполагать только изменения кода реализации существующих классов и методов.
  • Вы в итоге разработчику спасибо не скажете, т.к. на ранних стадиях развития библиотеки каждый второй релиз скорее всего будет major — с изменённым контрактом.

вот так копаешься в либах от Microsoft: вроде всё понятно, так и так, ща вот тут подлезу и будет шоколадно вообще… итут НННА ТЕБЕ INTERNAL.

Я тоже копался в некоторых либах от MS. И знаете, internal там далеко не самое страшное зло.
Наследники гиперчувствительны к любым изменениям предка.

И что же теперь, ограничиваться одним уровнем наследования от абстрактного класса?
Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм

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

Не автор, но прокомментирую :-)


Часто забывают, что ООП — это от слова "объект", а не от слова "класс". Задача программиста — сделать так, чтобы получались объекты, обладающие требуемым поведением. В среднестатистическом мейнстримном языке типа C#/Java есть 2 подхода:


  1. Если мне нужно новое поведение, я наследую существующий класс и переопределяю какой-то аспект его поведения.
  2. Если мне нужно новое поведение, я немного иначе "строю" объект, поведение которого мне нужно изменить.

Если весь код написан таким образом, что единственный способ сконструировать объект это new ЧтоТоТам(), конечно тут кроме наследования ЧтоТоТам нет вариантов. Но можно же изначально заложиться на new ЧтоТоТам(new КакЯДелаюВотЭто(), new ИЕщёКакЯДелаюВотЭто()). В таком случае получается на порядок больше мелких классов, где одни классы описывают какой-то конкретный аспект поведения, а другие — просто скручивают несколько таких аспектов поведения в один "настроенный объект". Статья автора, если я правильно понял, про такой подход.

1) Это называется АОП — Аспектно-ориентированное программирование. И оно — не панацея, так как не позволяет безболезненно связать между собой разные аспекты.
2) Это верно только для простых алгоритмов, которые будут вызываться изнутри кода. Вроде SortedVector(SortingAlgorithm); SortedVector(new Bubble()); SortedVector(new QSort());
Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта, ваш подход ещё хуже, так как требует объявить ваши КакЯДелаюВотЭто и ИЕщёКакЯДелаюВотЭто друзьями класса ЧтоТоТам. Далеко не все ЯП позволяют потомкам «друзей» оставаться «друзьями».
Это называется АОП

Это ни в коем случае не АОП. Это самое обычное ООП + делегирование.


Это верно только для простых алгоритмов… Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта, ваш подход ещё хуже

Посмотрите примеры использования паттерна "Стратегия" — это собственно делегирование в чистом виде и есть.

Это ни в коем случае не АОП

Думаете? Подумайте лучше.

Посмотрите пожалуйста внимательнее на вашу ссылку, ключевое слово — сквозная функциональность:


Для программы, написанной в парадигме ООП, любая функциональность, по которой не была проведена декомпозиция, является сквозной

В случае моего примера выше мы в явном виде провели декомпозицию.

>В случае моего примера выше мы в явном виде провели декомпозицию.
В случае примера выше вы написали три бессмысленных названия.
И будьте скромнее, ваше вашество, не стоит себя на «мы» величать. Если вы, конечно, не коллективный автор.

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

Затем, что именно принцип Лисков позволяет обеспечить полиморфизм "в рамках спецификации класса-предка".

НЛО прилетело и опубликовало эту надпись здесь
Да, но наследование ведь изначально предполагает, что потомок реализует спецификацию предка?

Это и есть принцип Лисков, соблюсти который при наследовании реализации нетривиально.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Я не автор статьи и не эксперт, но это могло бы выглядеть примерно так
> Нетривиально, означает, что потребуются не только руки + клавиатура, но еще и мозг.

Нет. Есть принципиальная проблема, что «спецификация предка» — это не интерефейс, и формально нигде не описана. То есть меняя поведение в наследнике вы реализуете «спецификацию», которая есть только у вас в голове. А формально, скорее всего (если вы не наследуете абстрактный класс — точно), нарушаете LSP.

Почему вы наследование интерфейсов называете кастрацией?

НЛО прилетело и опубликовало эту надпись здесь
Как китайский "iPhone". Выглядит так же, а что внутри неизвестно.

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


Но почему я должен отказываться от наследования в собственном коде?

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

НЛО прилетело и опубликовало эту надпись здесь
Дело в том, что одинаковая функциональность оказывается реализована во множестве мест.

Не надо путать композицию с копипастой — одинаковая функциональность реализуется ровно один раз.


Скажем полезно будет указать, что вычислительная сложность метода justSolveTheProblemHereAndNow(int n) составляет O(n!)

Это не нарушение инкапсуляции, а уточнение публичного контракта.

НЛО прилетело и опубликовало эту надпись здесь

"Вызывает add" в исходном публичном контракте нет.

НЛО прилетело и опубликовало эту надпись здесь
> Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта

Нонсенс. Если внешний код должен иметь доступ к полю — это не приватное поле.

Если вы делаете такое поле protected только для того, что бы оправдать появление наследника — это вы зря. «Друзья» — это вообще костыль и инверсия абстракции.
Вы пропустили тот момент, что это наследование. А наследование расширяет функционал предка. Когда (точнее, «если») наследование заменяется стратегией поведения, безболезненно можно менять логику только в тех местах, где это предусмотрено. Если же появляется необходимость расширить функциональность в том месте, где это не предусмотрено, тогда жопа.

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

Если вам надо UndoStack, а он sealed, или буфер не protected, и исходников нет — то жопа.
Если исходники есть — вытащите стратегию, это типа в 3 клика делается. Код с вашим наследником совпадёт чуть менее, чем полностью.
Напомню,
> 3) Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
> 4) Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.

Такие классы, обычно, поставляются фреймворками. Очень часто фреймворк в бинариках поставляется, и далеко не факт, что его можно будет пересобрать без лишней боли. Очень часто любое изменение исходных кодов фреймворка (равно как и дублирование кода из фреймворка с последующей правкой под себя) требует изменения лицензии. Очень часто архитекторы даже не задумываются, что такая функциональность в принципе может понадобиться, или оставляют это на откуп конечным разработчикам, которые ставят sealed/final на автомате Вот и думайте, что делать, когда пойдёт волна свежих программистов, которые будут поголовно запечатывать свои классы потому, что Bonart так написал.
Так же, напомню, что враппер не обладает никакими из свойств оборачиваемого объекта, даже если их унаследовать от одного интерфейса, связи «is a» точно не получится, что, конечно, не важно, когда у тебя понятия «класс» нет и не предвидится или когда архитектор предусмотрел это и потребовал интерфейс, но так бывает далеко не всегда.

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

Наследуем реализации — скрываем слишком много, наследуем интерфейсы — не накладываем ограничений, наследуем контракты — не проверяем корректность реализации, проверяем корректность реализации — наследуем реализации…
А что может быть небезопасного в наследовании неподготовленного класса без виртуальных методов, например?
НЛО прилетело и опубликовало эту надпись здесь
Так и что здравого в пометке всего sealed?

Наличие виртуального метода подразумевает подготовку к наследованию. Если вдруг(!) у публичного класса подготовка ненадлежащая — sealed, в качестве исключения.

У публичного класса в 95 % случаев подготовка отсутствует, в 4,9% — ненадлежащая.

У публичного класса в 99% нет виртуальных методов. В 0.9% от него никому в голову не придёт наследоваться.

Пометка всего sealed сродни установке лежащих полицейских среди поля — машины там не гоняют, зато люди спотыкаются.
У публичного класса в 99% нет виртуальных методов.

У класса, спроектированного для наследования, виртуальные методы есть всегда.
От класса без виртуальных методов наследоваться бесполезно.

> От класса без виртуальных методов наследоваться бесполезно.

Ну, это вы загнули. Часто вы в C# перекрываете виртуальные члены Object? ) Если бы ToString() и GetHashCode() были в интерфейсе — вообще не перекрывали бы.
Часто вы в C# перекрываете виртуальные члены Object?

Да, хочу чтобы сравнение и ключи в dictionary работали нормально.
Еще хочу чтобы в отладчике и логах информация была читаемой.
И это ничего, что наследование от object слегка недобровольное?

А сравнение и dictionary и так, в целом, работают. ToString легко заменить не виртуальной пропертью(или даже полем).

> И это ничего, что наследование от object слегка недобровольное?

Как так? Вы же хотите, чтобы в отладчике и логах информация была читаемой?

Ну ладно, пусть не Object.
Берём официальное руководство по C#, раздел «наследование» (https://msdn.microsoft.com/ru-ru/library/ms173149.aspx) и смотрим на пример:

// ChangeRequest derives from WorkItem and adds a property (originalItemID)
// and two constructors.
public class ChangeRequest: WorkItem
А у интерфейсов они 100% хорошие, симпотичные, да в туфельках отличных, ага.
А что не так с интерфейсами?
А что не так с публичными классами?
Отличная от нуля вероятность кривой имплементации.
Отличная от нуля вероятность кривой имплементации. И не подумайте, что я вас передразниваю.
В интерфейсе, по определению, нет имплементации. Я думаю, вы обезьянничаете.
«Имплементация» интерфейса — фича весьма нехарактерная для dotNet.
НЛО прилетело и опубликовало эту надпись здесь
sealed предлагается использовать на уровне класса.

sealed на уровне метода подразумевает, что родитель был сначала virtual, потом был наследник, а потом решили, что эта иерархия плохо подготовлена. На мой взгляд, это «мы тут плохо надизайнили, но надо паблишить». Для либы — хак, для своего кода вообще непонятно кому надо.
НЛО прилетело и опубликовало эту надпись здесь
> Поэтому я и поставлю sealed на этих моих методах,

вы запечатаете не свой метод, а библиотечный, и «Хотите наследовать? Наследуйте!» станет издевательством.

В остальном — правильно: sealed позволяет публиковать недоделанную фигню, имитируя заботу о том, кто с матами будет пытаться обойти sealed.
НЛО прилетело и опубликовало эту надпись здесь
У вас одни противоречащие параграфы. Зачем мне ваша жутко полезная утилитка, если при любом чихе мне надо наследоваться от базового, теряя ваш функционал? Почему вы при этом говорите «я же не жадина, пусть люди пользуются»?

И, главное, зачем именно нужен sealed, если я могу матюгнуться, форкнуть и его удалить. Опубликовав исходники «не вводить во искушение» уже не в вашей власти.
НЛО прилетело и опубликовало эту надпись здесь
> инкорпорируйте и не теряйте.

Да проще на лету подменять код и ставить везде паблик. Вопрос пары лишних строк в билдфайле.
> Но это уже не мои проблемы.
> Я закрыл опасное место

Вы или крестик снимите, или одно из двух.
НЛО прилетело и опубликовало эту надпись здесь
Вы вообще не должны страдать, даже sealed можно не ставить

Вот только пользы от такого наследования около нуля — без перекрытия методов полиморфизма не будет.

Если бы наследование было только ради полиморфизма, то добавлять новые (публичные) свойства и методы давно запретили.
Враппер, реализующий интерфейс, обладает всеми нужными свойствами. Я бы не рекомендовал использовать фреймворк, использующий «is a» по классу, реализующему интерфейс — его проектировали люди, в принципе не понимающие что они делают. Наличие исходников без права их использования — тоже плохой знак.

Вообще, как только в голове появилось «архитекторы даже не задумываются», то наследоваться от такого — не самая светлая мысль.

Впрочем, ставить sealed на автомате — тоже не очень хорошая идея. Надо запрещать случайно прострелить себе ногу, а не осмысленно.
То есть мы выбрасываем Java, выбрасываем .Net, у них же реализация нефинальных классов сокрыта, выбрасываем Qt, исходники нельзя переиспользовать без покупки, Wx выбрасываем, к чёрту Unity и Unreal Engine. Это, естественно, не полный список, но даже его хватает для того, чтобы понять — в помойку всё.

Вы немного не понимаете, в чём дело. Порой нужно расширить функциональность для тех классов, в которых изначально такое расширение не предусматривалось не по злому умыслу, а по незнанию. Пример с UndoStack я уже приводил. Если UndoStack вне области, доступной для модификации (как минимум, по банальной причине бинарной совместимости), ваш подход мне ничего не даст. Ничего не дадут мне ни интерфейсы, ни подходы в стиле Rust, ни врапперы. Я либо смогу получить доступ к полям класса, либо мне придётся реализовывать всё самому, и ничего мне не поможет. Да, если на вход объекта передавать интерфейс, а не конкретный класс, то гораздо проще объект заменить, но мне не нужно его заменять.
НЛО прилетело и опубликовало эту надпись здесь
В статье даны выводы 3 и 4, призывающие запрещать наследование реализаций по умолчанию. Вот и скажите мне, к чему призывает статья.

Если так, тогда решений, отличных от саморучной реализации всех классов, просто не будет, и механизм коммитов не будет связан со стеком команд.
НЛО прилетело и опубликовало эту надпись здесь
И вместо рекомендации по ослаблению ограничений, вы отстаиваете рекомендации усиливать эти самые ограничения. Вместо рекомендации по предпочтению protected над private и выносу кода из виртуальных методов в обычные, оставив в виртуальных только набор вызывов невиртуальных методов, вы советуете (или отстаиваете рекомендации) отказываться от наследования в пользу реализации интерфейсов. Которые не более безопасны, чем наследование классов, только их небезопасность выводится за пределы вашей ответственности, как проектировщика. И контракты — это только попытки описать способы обезопасить себя, не более.
НЛО прилетело и опубликовало эту надпись здесь
Да только ода проблема. Никаких гарантий.
К примеру, вы сделали интерфейс потока данных. И ожидаете, что если запись не пройдёт успешно, вам возвратят ошибку. А вам, допустим, выбрасывают исключение, которое кладёт всё приложение, ведь его никто не ловит. Формально вы не виноваты.
Ладно, это пол-беды. другой пример. Вы в вашей стратегии ждёте контейнер для хранения объектов. И ожидаете, что вы сможете в него сбрасывать ссылки на временные объекты. Последнее, что вы ожидаете — это что у контейнера будет глобальное состояние, и объекты не умрут после удаления самого хранилища. В этом случае формально виноват будет пользователь, хотя он не нарушит никаких соглашений. Ведь в интерфейсе, который вы ожидали, не написано, как должны храниться объекты на протяжении жизни экземпляра приложения. С наследованием такая херня не пройдёт, контейнер выполняет свой деструктор, который выполнит деструкторы всех объектов, и даже если в глобальном пространстве сохранятся ссылки, они будут битыми и относительно безвредными.
Ладно, у нас же камень преткновения — виртуальные методы. Так и быть, пусть у нас есть класс фигуры. И у него есть виртуальный метод для вычисления центра масс. «Стратегия вычисления центра масс!» — возразите вы. И не сказать, что ошибётесь. Но есть одно но. В простейшем случае центр масс зависит от геометрической формы. Однако он может зависеть от распределения плотностей… И от скорости… И от импульса… Ускорения… Энергии… Так что будет принимать стратегия на вход? Какой у неё интерфейс? Снова перекладываем всё на шею пользователя? Самое грубое решение. В чём же преимущество виртуального метода? В том, что не нужно придумывать интерфейс стратегии, доступ ко всем нужным данным уже имеется. Почему бы нам не сделать доступ ко всем этим данным из интерфейса? Ну, можно, и даже нужно, если это какой-нибудь физический движок. А если нет? Или, если, например, мы не можем предоставить однородный доступ к данным, что тогда? Например, если у нас фракталы есть. Ведь для отрисовки фрактала не нужно знать его форму, достаточно отрендерить его в текстуру и натянуть на AABB, а центр масс вычислять по аналитической аппроксимации. Чисто теоретически, всё это можно сделать на чистых интерфейсах. Будет ли это проще? Будет ли меньше ошибок?

Конечно, в моих примерах много «но» и «если», но ведь и в ваших этих «если» не меньше. Можно придумать ещё много примеров, в которых кто-то будет допускать ошибки, в которых формально вы не виноваты. Однако ровно так же не виноваты и те, кто не закрыл свои классы для наследования. Единственная разница, ошибки произойдут в их вотчине. Вот и всё.
> То есть мы выбрасываем

Что?

> Если UndoStack вне области, доступной для модификации (как минимум, по банальной причине бинарной совместимости), ваш подход мне ничего не даст.

Мой опыт говорит, что в UndoStack сами команды всё равно приватные (в крайнем случае internal), наследованием там ничего не поправить. Если код не задуман для расширения — наследование не спасёт.

Если вы меня хотите убедить, что наследование можно использовать как грязный хак ради «паблика Морозова» — соглашусь, можно.
НЛО прилетело и опубликовало эту надпись здесь

Слышал про него и читал его блог плюс интервью здесь, но не понравилось. На мой взгляд он очень сильно гонится за популярностью в ущерб корректности (не политической).

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

Как будто что-то плохое. Кто хоть раз ковырялся в семи уровнях наследования WPF, тот меня поймет.


Внимание, вопрос: с точки зрения автора статьи, как обходиться в таких ситуациях?

По мелочам обычно выручает декоратор. Кода получается больше (правда 95% шумового кода декоратора все равно нагенерит решарпер), но зависимость ослабляется до уровня публичного интерфейса. Заодно можно бесплатно комбинировать несколько декораторов.

Как будто что-то плохое. Кто хоть раз ковырялся в семи уровнях наследования WPF, тот меня поймет.

Один пример плохой реализации не может быть доказательством несостоятельности концепции. Например, иерархия классов виджетов Qt вполне себе адекватна и красива.

По мелочам обычно выручает декоратор

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

И чем же плохо ограничение, препятствующее плохой реализации и облегчающее хорошую?


Так а в чем профит то?
Вы точно читали статью и мой предыдущий комментарий?

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


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

Почему вы проигнорировали композицию с делегированием? Вы точно знаете, что такое декоратор?

просто посчитайте объем строк кода и число программных сущностей при реализации через наследование и при реализации через декорирование/делегирование/композицию

Разница между наследованием и композицией:


  1. Минус одно наследование.
  2. Плюс одно приватное поле
  3. Плюс "шумовой" делегирующий код (можно избавиться используя динамическую типизацию или метапрограммирование).

Пункты 2 и 3 — экономия при написании кода, пункт 1 — при сопровождении. Итоговый баланс немного предсказуем.

Я вот искренне не понимаю, зачем нужен пункт 3? Зачем использовать какой-то кодогенератор, когда разработчики языка позаботились об этой ситуации заранее и предоставили тебе наследование? Ведь это упрощённый способ записи делегации. Без бойлерплейта. Из каробки. Может называется по-другому, но работает точно так. А если что-то плавает как утка и крякает как утка, то это и есть утка.
Bonart говорит о гибкости и корректности в сложном случае, а вы — о простом случае. Может быть поэтому вы друг друга не понимаете?

Наследование как "упрощенная запись делегации" — ложь.
"Без бойлерплейта" — ложь.
"Работает точно так" — снова ложь.
Контрольный вопрос — как скомбинировать функционал нескольких наследников с мелкими модификациями/дополнениями функционала?
С декораторами это делается элементарно.

``Наследование как «упрощенная запись делегации» — ложь'' — ложь
``«Без бойлерплейта» — ложь'' — ложь
``«Работает точно так» — снова ложь'' — ложь

> Контрольный вопрос — как скомбинировать функционал нескольких наследников с мелкими модификациями/дополнениями функционала?

Мультинаследование.
Мультинаследование
Недоступно в большинстве языков.
Ну а где-то не доступны декораторы, и динамическая генерация (байт)кода. Там, где уже всё есть — можно пользоваться готовым наследованием. Там, где нет — придумывать замену. Но, очевидно, не надо говорить о том, что наследование не нужно, если оно уже реализовано. Хорошо реализованное наследование решает множество проблем композиции без необходимости задействовать кодогенерацию. С другой стороны, можно как в лиспе наследование реализовать через кодогенерацию (метаобъектный протокол). Заодно получить возможность реализовывать разные механизмы наследования в зависимости от выбора метаобъектного протокола. Но какова бы не была реализация, принцип остаётся в силе — один и тот же, и для наследования, и для метагенераторов.

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

Если мне что-то из этого нужно, приходится использовать делегацию и страдать от некоторого бойлерплейта. Если не нужно — то классическое наследование справляется на ура.
Зачем использовать какой-то кодогенератор, когда разработчики языка позаботились об этой ситуации заранее и предоставили тебе наследование?

Во некоторых языках разработчики позаботились о нормальной поддержке делегирования, и оно делается одной строкой, без какого-либо бойлерплейта.

НЛО прилетело и опубликовало эту надпись здесь

Ну, полный список я Вам вряд ли предоставлю. Но для примера в Ruby есть def_delegator для этого, некоторые используют delegate из ActiveSupport, хотя это уже ближе к кодогенерации.
Кстати, аналогичную кодогенерацию можно устроить в любом языке где есть compile-time макросы. Rust, Crystal, Nim, etc. к вашим услугам. В Crystal такое есть из коробки, про остальных точно не знаю. Но сделать аналог весьма просто.
В функциональных языках делегация тоже хорошо поддерживается, только там она осуществляется от модуля к модулю, например defdelegate в Elixir.

НЛО прилетело и опубликовало эту надпись здесь

Так а в чём разница то? В языках, где есть нормальные макросы (если у Вас это слово ассоциируется с C++, то забудьте эту ассоциацию), абсолютное большинство средств языка выражается именно через макросы, включая элементы синтаксиса.
В Kotlin, насколько я понимаю, пока очень урезанная поддержка делегации — либо всё делегировать, либо ничего.

НЛО прилетело и опубликовало эту надпись здесь

В дотнете тоже можно так, через DynamicObject, ну или RealProxy.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Насколько я понял, вопрос был не про то, что на выходе, а про детали процесса компиляции… остаётся ли делегирование самостоятельной сущностью после синтаксического анализа или преобразуется в эквивалент набора методов на уровне AST?
Правда, непонятно, какая разница в плане практического применения? По таким критериям языки можно делить если только из теоретического интереса, тем более что реализация компилятора может меняться от версии к версии.

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

По вашей ссылке у меня не выдаёт никаких исключений.

НЛО прилетело и опубликовало эту надпись здесь

А что вы изменили? Я там вроде всё прокликал тогда, а стектрейс так и не появился.

Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода

Достаточно иметь контракт переопределяемого метода. А вызываться он может как угодно в соответствии с контрактом.
Вам же не требуется для реализации метода интерфейса знание где и как он будет вызываться.

Например реализовываете вы собственный AbstractSpliterator. Вы можете реализовать только 1 метод в соответствии с контрактом и вообще не разбираться как оно там работает внутри (а работает оно хитро). Можете реализовать 2 или даже 4, если хотите улучшить производительность. Но даже в этом случае знать вам надо только контракты методов, а не то как они используются.
НЛО прилетело и опубликовало эту надпись здесь
А можно не переопределять, а использовать пустые параметры и что-то вроде func_get_args() (PHP) :)
НЛО прилетело и опубликовало эту надпись здесь
Тогда скажите, что Вы понимаете под «нарушение контракта методов»? Может я Вас не понял :)
НЛО прилетело и опубликовало эту надпись здесь
Тогда я говорил об этом:
«В объектно-ориентированном программировании контракт метода обычно включает следующую информацию:
возможные типы входных данных и их значение;»

Объявив пустые параметры и использовав func_get_args() можно не править принимаемые параметры в наследнике вслед за родителем.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Достаточно иметь контракт переопределяемого метода.

В контракт виртуального метода всегда входит контекст его вызова в классе-предке, который является приватным по построению.


А вызываться он может как угодно в соответствии с контрактом.
Вам же не требуется для реализации метода интерфейса знание где и как он будет вызываться.

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


  1. У вас в базовом классе есть виртуальные методы Add (добавляет элемент) и AddRange (добавляет пачку элементов)
  2. В наследнике вам необходимо подсчитать общее количество добавленных элементов.

Как вы реализуете эти методы в классе-наследнике?

Для интерфейса не требуется как раз потому, что в предке ни один из методов интерфейса не вызывается по определению.
Верно для C#, но не для java >= 8, Rust, scala, kotlin, etc

Да, некоторые классы предназначены для расширения, некоторые — нет, а в некоторых об этом просто не подумали. То, что в некоторых случаях могут быть проблемы, еще не значит, что нет безпроблемных случаев. Если класс проектировался для расширения, то в нем ловушек быть не должно (например через private _Add), если не проектировался, то и методы не должны быть виртуальными.
Верно для C#, но не для java >= 8, Rust, scala, kotlin, etc

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

Методы расширения отсутствуют в java. И имеют существенный недостаток: невозможно перегрузить более оптимальной реализацией. Например, если в переменной типа Seq[Int] в находится Range, то метод sum на этой переменной имеет сложность O(1) в scala. Если вынести sum в метод расширения, то оптимизировать для конкретных реализаций уже не получится.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Когда вдруг наследование стало архаизмом?

Не знаю. Наследование интерфейсов до сих пор мейнстрим.


Есть несколько вариантов как переиспользовать код, и наследование один из них. Просто использовать нужно с умом.

"С умом" — априори верно, вот только неконкретно.


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

  1. Мне сложно исправлять баги в самой библиотеке, в отличие от ее автора.
  2. Новая версия может решать больше моих задач с лучшим качеством.
НЛО прилетело и опубликовало эту надпись здесь
Спрошу иначе. Когда наследование от обычных классов (имеющих реализацию) стало архаизмом?

Давным-давно. Уже "банда четырех" поминала наследование реализации как нежелательное.


Обновляю не просто потому что пофиксили баги. Плюс учитываю риски.

То есть библиотеки вы все-таки обновляете. И без наследования реализаций риски снижаются.


Потому что тут все it depends от конкретной задачи.

Что не отменяет ряда общих закономерностей и рекомендаций.

НЛО прилетело и опубликовало эту надпись здесь
Нет. В любом случае придется тестировать, исправлять и т.д. Если в либе поломали совместимось, это в любом случае дополнительная работа.

Тестирование и исправление — части нормального процесса обновления. А поломать совместимость при наследовании реализации объективно намного проще.


Если это про ваши «итоги», то я вот не согласен с пунктами 1, 2, 3 и 5. Это плохие рекомендации.

Это ваше оценочное суждение не подкреплено ни ссылкой на теорию, ни практическими контрпримерами.

НЛО прилетело и опубликовало эту надпись здесь
Я всегда считал, что интерфейсы это некий костыль

Интерфейсы — костыль ровно в той же степени что и классы.


А я считаю, что ограничения — это зло

Правда? Вы предпочитаете опасные бритвы, кабеля без изоляции, лестницы без ограждения, оживленные перекрестки без светофоров? Что же вы пишете на TypeScript вместо няшного неограниченного JavaScript?


Например в Java и C# нет множественного наследования.

Нет множественного наследования реализаций. Интерфейсов — пожалуйста.


Все что позволяют такие ограничения — это брать на работу плохих программистов

Опять оценочное суждение без малейшего обоснования.


Но бояться, что при использовании наследования что-то там сломается

Зачем бояться? Это просто факт — использование наследования реализаций ломает инкапсуляцию. Точка.


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

Что характерно, все эти техники вводят куда более дорогостоящие ограничения. Бороться с их помощью с последствиями наследования реализаций все равно что лупить из пушки по воробьям.


Вот неплохое объяснение довольно известного специалиста

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

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

Вот и с наследованием примерно так же. Я ленивый и лишних телодвижений не люблю.


А вот например TypeScript дает мне больше гибкости, чем Javascript. Можно сделать контракт модуля типизированным и получить профит от статической типизации и intellisense, а внутри например писать как на JS.

Итак, ограничения в TypeScript (статическая типизация) дают вам больше гибкости, чем куда более свободный JS. Получается, что не все ограничения зло?
Наследование интерфейсов также дает больше гибкости, чем наследование реализации, за счет ослабления зависимостей между классами.


В моем мире это по другому — наследование реализаций может сломать инкапсуляцию, но крайне редко и это можно предотвратить с помощью тестирования.

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


Collective Code Ownership, TDD, Code Review, DDD — мне казалось это просто стандартные техники и методологии. Я имею ввиду XP, Agile и т.д.

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

НЛО прилетело и опубликовало эту надпись здесь
Просмотрел статью и комментарии, но доказательств не нашел.

Посмотрите работу 1986 года по ссылке.


Очень просто, писать тесты. Тесты это инвестиции, а не затраты всетаки.

Инвестиции, но в решение иных задач, которые компилятору, статическому анализу и простым техникам кодирования не по зубам.


Как раз в примере про Add/AddRange тесты бы вам показали проблему.

Какую проблему? Базовый класс успешно проходит все тесты, код наследника мы не меняли, а наши тесты указывают на ошибку именно в нем, если вообще срабатывают. Несколько комментаторов дали решения, которые развалятся только при многопоточке (точнее, реентерабельности).
Вариант с наследованием интерфейсов заодно даст и лучшую диагностику по тестам — ошибка сразу будет локализована до конкретного класса.


Все, что я вижу, это то, что наследование реализации МОЖЕТ сломать инкапсуляцию.

Вы думаете, что сломанная инкапсуляция — это видимый сбой? Но это неправда:


  1. Сломанная инкапсуляция — это ошибка, она сразу заложена в наследование реализаций.
  2. Хрупкий код наследника, не работающий при изменениях (или просто при недостатке информации) в деталях реализации предка — это дефект, возникший вследствие ошибки.
  3. И наконец, некорректный расчет — это сбой как следствие дефекта.

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

НЛО прилетело и опубликовало эту надпись здесь
> Что же вы пишете на TypeScript вместо няшного неограниченного JavaScript?

Даже в статически типизированных языках есть dynamic cast. Так что к каждой инкапсуляции должен прилагаться способ её нарушить.

Frontend и Backend — это не языки, а предметные области. Области эти довольно большие требуют совершенно разных подходов, имеют совершенно различные требования и далеко не в каждом мозгу помещаются одновременно.

НЛО прилетело и опубликовало эту надпись здесь
А разница… если про программирование, то разница не большая, те же компоненты, классы, те же паттерны.

… реактивное программирование, инкрементальный рендеринг — всё это ой как надо бэкенду :-)


Если говорить по хайлоад например,

Разные приоритеты. Для UI важна отзывчивость, для бэкенда — пропускная способность.


то это не совсем про программирование, это скорее про архитектуру.

Архитектуры-то разные. От слова совсем.


Если про дизайн и верстку, тут да, мне например это скучно делать, но сейчас стало проще, есть bootstrap и его друзья.

Толковый фронтендер его бы ни за что не стал использовать.


Я согласен, что разница есть, но она минимальна.

Боюсь даже представить, как бы вы реализовывали SPA :-)

НЛО прилетело и опубликовало эту надпись здесь
Кстати рекактивным программированием пользуюсь и там и там.

Например?


Я не замечаю, тот же MVC или MVVM например. Теже слои.

Это вершина айсберга. Я, кстати, использую MV.


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

В частности, поэтому вы — бэкендер, а я — фронтендер ;-)

НЛО прилетело и опубликовало эту надпись здесь

Боюсь "reactive manifesto" имеет с "реактивным программированием" из общего только слово в названии.

Автор, попробуйте пожалуйста разобрать конкретный пример — как сделать редизайн какого-нибудь UI фреймворка, чтобы избежать всепроникающего базового класса Control. Я так понимаю, у вас есть опыт с .NET — предложите вариант принципиальных изменений для Windows Forms или WPF, чтобы там не было иерархий типа https://msdn.microsoft.com/en-us/library/system.windows.controls.button(v=vs.110).aspx :


System.Object
  System.Windows.Threading.DispatcherObject
    System.Windows.DependencyObject
      System.Windows.Media.Visual
        System.Windows.UIElement
          System.Windows.FrameworkElement
            System.Windows.Controls.Control
              System.Windows.Controls.ContentControl
                System.Windows.Controls.Primitives.ButtonBase
                  System.Windows.Controls.Button

Концептуально самый простой способ — замена наследования композицией:


  1. Выделяем публичный интерфейс Control в тип IControl
  2. Делаем в своем классе поле типа IControl
  3. Делегируем ему все члены IControl, которые не переопределяем сами.
  4. Передаем экземпляр Control как параметр конструктора.

Вот только надо ли вам в данном случае лопатить кучу легаси?

НЛО прилетело и опубликовало эту надпись здесь
С одной стороны, это действительно теоретическая проблема, выливающаяся в ряд практических проблем, вроде бинарной совместимости модулей и тп.
С другой стороны, это одно из лучших решений с учётом накладных расходов. Дело не только в уменьшении переписываемого кода, именно наследование позволяет делать как максимально гибкий, так и максимально быстрый в расширении код с наиболее понятными требованиями к программисту-пользователю.
Отказываться от наследования — всё равно что отказываться от промышленного оборудования из-за того, что долбарабочий может при нарушении ТБ покалечить себя до смерти — высший уровень тупизны. Да, каждый язык имеет некоторые антипаттерны и некоторые спорные технологии. Да что там, даже упомянутый Go не обошёлся без критики. Проблема не в том, что они есть и сколько их имеется, а в том, обучены ли программисты работе с этими технологиями. Чаще всего нет, и, что хуже всего, программисты крайне редко несут ответственность за собственный непрофессионализм*. Худшее, что с ними может произойти — переход в другую фирму после разорения предыдущей.

*Нет, это не призыв калечить людей.
Задачи можно эффективнее решать и без наследования :)

Отказаться от наследования — это отказаться от лошади в пользу автомобиля :)
Задачи можно эффективно решать и без языков программирования высокого уровня. xor eax, eax

Отказ от ООП в пользу ФП — это переход от автомобиля в пользу летающих тарелок. Очень крутой ход, жаль только топлива для них нет. Вообще, выбор любых техник должен быть осмыслен и логичен. Их набор должен давать максимум выгоды при минимуме затрат. А не следовать моде, вроде перевода всего и вся на микросервисы с 25% потерей производительности.
А я разве пропагандировал ФП? :)

Я тоже не ведусь на моду. :)

Собственно, поэтому выступаю против фреймворков в PHP.

ООП — это тоже мода.
Была модой до середины нулевых. Уже далеко не мейнстрим. А если почитать статьи на том же хабре за последний год, появилась мода хаять ООП и в хвост, и в гриву, особенно, классические классовые меньшинства. И уходить. Либо в монастырь, либо в Go. Кстати, не удивлюсь, если новая статья автора будет как-раз по этому языку.

И, ладно, когда поднимают какую-нибудь интересную багу реализации того или иного языка или освещают вопросы неправильного использования языка, как ребята из PVS-Studio, но ведь чаще всего появляются статьи «People are dying if they are killed», вроде текущей, которая просто приводит к очередному холивару.

з.ы. А ныть нынче не в моде? Было бы забавно оказаться в тренде.
Нет ничего проще, чем делегировать проблему «на уровень выше» тому кто использует класс, инструмент, абстракцию и т. д., а там профессионалы разберутся. Вот с null-ом, кажется большинство здравомыслящих людей ошибку поняло и согласилось, что нужно исправлять, а с наследованием, видимо, история только начинается.
В агитируете за прагматичный подход, а мне кажется, что даже в приведенной вами аналогии стабильность лучше.
Вы предлагаете наращивать квалификацию и дисциплину рабочего и это хорошо, но может быть все-таки в данном случае лучше закупить более дорогое, но безопасное оборудование? Ведь оба подхода решают проблему, но стабильность в долгосрочном периоде лучше.
ИМХО есть небольшая разница между «указать требования для реализации и предоставить реализации по умолчанию» и «делегировать проблему». В качестве примера можно взять те же Qt-шные модели данных, для начала работы с которыми нужно определить 4 метода, но которые, при этом, позволяют полностью изменить практически все аспекты класса.

> Вот с null-ом, кажется большинство здравомыслящих людей ошибку поняло и согласилось, что нужно исправлять
Вы про #define NULL 0? Или про шарповый null? Или про nul? Или про nullptr?
Конечно, каждый год появляется куча интересных техник и лучшие из них обязательно нужно внеедрять в язык и тд и тп, но не забывайте одну мелочь. Сегодня средний по размерам проект на хорошем компе с ssd и 32ГБ оперативы собирается с нуля не меньше получаса, занимая в процессе под полтишок гигабайт места различными кешами. 10-15 лет назад, когда критикуемые вами концепции только появлялись и формировались, на компе был гиг оперативы и 120Гб памяти. Всего. А то и этого не было.
Люди — ужасно прагматичные существа. Если они могли сэкономить на null'ах без особых проблем для кода — они это делали. Если они могут модернизировать свой станок так, чтобы он стал менее травмоопасным, не теряя особо в скорости и не обязывая программистов переписывать ВСЁ (что требуют новые языки), они сделают это.
Что выбрать, новый инструмент, который может стать топовым, а может и не стать, или не устаревающую классику, которая, порой, отправляет тебя на стол хирургам в коробке «Puzzle 4000pts»?.. Я предпочту при таком выборе стать ведьмаком.
Дело не только в уменьшении переписываемого кода, именно наследование позволяет делать как максимально гибкий, так и максимально быстрый в расширении код с наиболее понятными требованиями к программисту-пользователю.

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


Отказываться от наследования — всё равно что отказываться от промышленного оборудования из-за того, что долбарабочий может при нарушении ТБ покалечить себя до смерти — высший уровень тупизны.

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

> Да-да, наследование реализации очень дешево и гибко благодаря сломанной инкапсуляции
Напомните, наследование — это одностороннее отношение «является» («потомок является предком»), да? Скажите, как отношение «является» ломает инкапсуляцию? Никак. Если Вася — человек, то он человек от головы до пят и в нём всё человеческое. Инкапсуляцию ломает не наследование, а приватные поля базового класса, недоступные из потомка, и сокрытые реализации методов. И ограниченность ЯП, которые не позволяют частично модифицировать методы. Последнее, видимо, к лучшему.
Быть может, запретим приватные поля и классы с сокрытой реализацией? Было бы неплохо.

>Наследование интерфейсов в ней — рекомендуется
Я написал «Отказываться от наследования интерфейсов...»? Нет, я написал «от наследования», более общее понятие, несущее в данном контексте ровно единственный смысл. Если для вас нужно писать «от наследования частичной или полной реализации», то уж точно не вам рассуждать о заслуженных мною титулах.

>возможность наследования реализаций — допускается.
Я ещё раз перечитал ваши выводы. Особенно тот, про запечатывание. Это вы называете «допускается»?

Как уже написали ранее, принцип подстановки Лисков — это очень полезное, но, тем не менее, не обязательное для объекта ограничение. Хотя бы по той банальной причине, что порой потомок создаётся именно для нарушения этого принципа.

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

В гранит!

В парашу. Вы только что подписались под тем, что ваша статья — мусор.

1) Если проблема в сокрытии реализации методов и полей от наследников, то причина не в наследовании и инкапсуляции, а в проприетарности создаваемого кода.
Этот факт в вашей статье отмечен от слов «никак» и «нигде», а это фундаментальное заблуждение, меняющее всё остальное.
2) Если проблема в проприетарности, то лучшее её решение — опенсорс, то есть наследование от правильно построенных классов открытых библиотек безопасно с точки зрения озвученных вами проблем.
Исходный код открыт, есть история изменения версий в виде дампов изменений, позволяющие мониторить совместимость.
3) Интерфейсы НЕ решают проблемы, а лишь маскируют её, переводя на следующий уровень. Это всё равно, что решить проблему гнилой туши в доме покупкой ароматизаторов, бахил и повязок на глаза.
Проблема интерфейсов в том, что они просто накладывают более мягкие ограничения, нежели базовые классы, имеющие реализацию. Мы говорим, что нам не важно, как всё устроено внутри, пускай просто ведёт себя похоже. В итоге, в том месте, где совмещение функциональности базового класса и неправильного наследника сразу дали бы ошибку, неверные реализации интерфейсов пройдут без проверок, и ошибки придется искать уровнем выше. В интерфейсах нельзя спрятать состояние, в интерфейсах нельзя создать код для автоматической проверки объекта самим собой на корректность и непротиворечивость.

Главное преимущество интерфейсов над полноценными классами-предками — первых куда проще проектировать. Делает ли это их лучше? А делает ли человека лучше отсутствие рук?
НЛО прилетело и опубликовало эту надпись здесь
Наследование интерфейсов — это иерархическая декомпозиция архитектуры проекта на слой «принципов» (интерфейсов) и «сущностей» (объектов). Наследование «рабочих» неабстрактных классов друг от друга — это чуть более «тупой» DRY «по месту», способ избавления от копипасты путём решения головоломки «как распределить по дереву наследования код, чтобы было меньше дублирования». Как правило, программист в выборе «смысла» классов в Си++ растёт через «стадию DRY» до «стадии выделения интерфейсов», а обсуждения, подобные комментариям под статьёй — столкновения тех, кто уже абстрагировался до интерфейсов, и тех, кто пока ещё классами решает только DRY. ИМХО.
Вот расписали бы (какие-то best practices, логику-идеи, примеры) тогда уж как реализовывать DRY и предотвращать повторение логики без использования частичной имплементации внутри базовых классов. Пока мне кажется очень удобным прописывать общую для всех потомков логику в базовых классах (хотя я и понимаю обозначенную в статье проблему), а отсутствие такой возможности в том же Go несколько пугает. Срочно требуется статья о том, как полюбить interface-only подход и начать жить…
Думаю, применимы одновременно оба подхода, главное чётко себе в этом отдавать отчёт и, например, тестировать куски кода (юниты) ограниченные интерфейсами, а не классами. Раньше слово class и для нэйспейсов использовали, просто складывая туда статические функции. Си++ в этом смысле универсален, на нём можно накрутить самые наихитрейшие схемы декомпозиции кода (конечно, не без издержек) ещё до того, как их поддержку внедрят в стандарт, что и обеспечивает его эволюцию и актуальность (и постоянные терминологические споры :)).

Чем вас обычная замена наследования композицией не устраивает?

> Как известно, классическое ООП покоится на трех китах: Инкапсуляция, Наследование, Полиморфизм

Интересно, откуда эта тройка именно в таком виде пошла. Это же какое-то отечественное изобретение (Архангельский?).
НЛО прилетело и опубликовало эту надпись здесь

Наследование — это просто составление контракта между объектом-предком и объектом-наследником. Точно так же, как есть контракт с между этими объектами и кодом, эти объекты использующим.


Не касаясь теоритических проблем, скажу про практические:


Зависимость, создаваемая наследованием, чрезвычайно сильна
Да, на это диктуется самим требованием наследования. В противном случае не наследование нужно, но более слабая композия.

Наследники гиперчувствительны к любым изменениям предка
Как, внезапно, любой клиентский код. Это точно такое же изменение контракта. Нужно понимать, что меняя контракт всегда можно кого-то обидеть.

Наследование от чужого кода добавляет адскую боль при сопровождении
Сопровождение чужого кода, меняющего контракты, доставляет адскую боль и без наследования. Хотя, по хорошему, чужой код должен создавать точки расширения (если они вообще нужны) с помощью интерфейсов.
Как, внезапно, любой клиентский код. Это точно такое же изменение контракта. Нужно понимать, что меняя контракт всегда можно кого-то обидеть.

Чтобы "обидеть" наследника, может быть достаточно изменений в приватном контракте.
А все остальные смогут заметить только изменения в публичном контракте.


Сопровождение чужого кода, меняющего контракты, доставляет адскую боль и без наследования.

Не скажите. С публичными контрактами авторы библиотек обычно весьма щепетильны, а вот наследники могут пострадать через непреднамеренное ломающее изменение в приватных.

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

Возможно, вы пропустили или не поняли пять пунктов сразу после заголовка "наследование ломает инкапсуляцию"
Пожалуйста, посмотрите на простейший пример зависимости наследника от приватных соглашений в базовом классе. Никакой рефлексии.
https://habrahabr.ru/post/310314/#comment_9815832

Я говорил лишь о контрактах и его изменениях. В вашем примере с AddRange никто не менял контракт. Здесь другая проблема, связанная не с контрактами, а с необходимостью дополнительных знаний о реализации публичного/защищенного метода. Ее существование я не опровергаю. И действительно, изменение внутренней реализации может негативно сказаться на потомках.
НЛО прилетело и опубликовало эту надпись здесь
Я полностью согласен с тем, что у наследования реализаций в общем случае есть куча проблем. В целом, следует предпочитать агрегирование наследованию, особенно при использовании чужого кода (без разницы, открыт он или нет). Наследование реализаций все же можно использовать для внутренней кухни.

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

private поля не доступны в большинстве языков.


Всем остальным доступен только публичный интерфейс класса.

Уровней доступа вообще говоря больше 2: private (не доступен никому вне класса/модуля), package (не доступен за пределами пакета), protected (доступен только потомкам), public (доступен в любом месте программы), export (доступен даже из вне программы).


Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку.

Этот принцип полиморфизма касается любых типов, а не только объектов.


Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода

Это касается любых случаев "обратного вызова". Виртуальные методы — весьма частный случай.


Зависимость, создаваемая наследованием, чрезвычайно сильна. Наследники гиперчувствительны к любым изменениям предка.

Что такое "сила зависимости"? Зависимость либо есть, либо её нет. Любой способ переиспользования кода создаёт зависимость.


разработчики библиотеки рискуют получить обструкцию из-за поломанной обратной совместимости при малейшем изменении базового класса, а прикладники — регрессию при любом обновлении используемых библиотек.

Не обобщайте, не при любом изменении, а при ломающем совместимость изменении. Классы тут опять же ни при чём.


А вообще, в статье ни единой строчки кода, ни единого описания решаемой проблемы. Только типичное "я где-то слышал, что наследование — это антипаттерн".

НЛО прилетело и опубликовало эту надпись здесь
А можно наследовать реализацию без интерфейса этой реализации? :)
НЛО прилетело и опубликовало эту надпись здесь
Что такое "сила зависимости"? Зависимость либо есть, либо её нет. Любой способ переиспользования кода создаёт зависимость.

Вы правда не в курсе?


  1. Наследование реализации создает зависимость от класса-предка целиком.
  2. Композиция создает зависимость от того же класса, но куда более слабую, так как включает в себя только его публичный интерфейс. Можно спокойно менять детали реализации или использовать наследника вместо базового класса.
  3. Композиция через выделенный интерфейс делает зависимость от класса совсем слабой — одну реализацию интерфейса можно безболезненно заменить другой.

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

  1. В любом случае зависимость будем целиком от того кода, который вы не скопипастили.
  2. Композиция в данном случае — частный случай наследования без виртуальных методов и защищённых свойств. Вас никто не заставляет их использовать там, где не надо.
  3. Тут вы похоже вообще про инверсию зависимости.
НЛО прилетело и опубликовало эту надпись здесь
Задался вопросом, что означает «закопайте стюардессу»:

… Самолёт терпит крушение над океаном, в живых остаются трое: пилот, помощник пилота и стюардесса, — им удаётся выбраться на необитаемый остров. Через месяц жизни втроём пилот сказал: «Долой разврат!» — и убил стюардессу. Ещё через месяц он опять сказал: «Долой разврат!» — и закопал стюардессу. Ещё через месяц он сказал: «Долой разврат!» — и откопал стюардессу...

А по теме… Сколько времени было затрачено на статью? Где примеры? Ну вот реально, подумайте, какая польза от статьи с сухими перечислениями? Те, кто это знают — пролистнут и забудут, те, кто не знает — прочтут и тут же забудут. Для кого/чего это пишется? Для кармы?

Почему ссылка на полиморфизм взята для PHP (который я не знаю)? От фонаря (та статья еще хуже чем эта, начинается с растянутого примера, в конце сухо чуток теории)? Зачем тег C# тогда?

P.S.: справедливости ради признаюсь — про паблик Морозова не знал, рассмешило слегка.
«Inheritance is the base class of evil» © Sean Parent :)

После прочтения статьи складывается такое ощущение, что наследование — это какой-то грязный хак, за использование которого должно быть стыдно. Единственный контекст, в рамках которого можно признаться в его использовании, — это ситуация в стиле: «Здравствуйте! Меня зовут Джон. В своих программах я часто использую наследование реализации, а иногда ещё и защищённые члены.» Если базовый поинт именно такой, то тогда всё правильно. Но проблема в том, что эта базовая предпосылка неверна. Наследование — это не какой-то костыльный трюк, а феномен, реально существующий в мире. Точнее, не в самом мире, а в нашем способе описания этого мира. Да, объекты реального мира не наследуют реализацию друг у друга, даже когда они во многом схожи. Но кого волнуют реальные объекты? Ведь люди мыслят абстракциями. А абстракции очень часто весьма неплохо выстаиваются в иерархию с наследованием общих свойств. Для абстракций такое естественно. Всё, что от программиста требуется, — это правильно реализовать в программе абстракции… с учётом того, что они представляют собой иерархию. Но это его работа, чёрт возьми. Предполагается, что специалист должен уметь выполнять свою работу правильно. А если он не умеет, то значит ему нужно учиться, а не искать другой подход к задаче, который не потребует умения правильно выполнять свою работу. Вряд ли такой подход существует. Скорее всего при другом подходе тоже будут сложности. Просто они будут другими.
НЛО прилетело и опубликовало эту надпись здесь
Не могу не согласиться. Хотя я имел в виду совершенно другое наследование. Которое происходит не столько во времени, сколько в пространстве. Два палочки Твикс не имеют ни одного общего атома. Тем не менее, на абстрактном уровне их можно представить таким образом, что они будут наследовать почти все свои свойства от некоей абстрактной палочки, которая пока не инстанцирована в пространстве и, соответственно, не имеет никаких пространственных свойств, и при наследовании будет добавляться всего лишь одно свойство — правая или левая.
НЛО прилетело и опубликовало эту надпись здесь
Это было бы инстанцированием, если бы правая и левая палочки были не отдельными классами, а инстансами неабстрактной универсальной палочки, которая могла бы быть как правой, так и левой… в зависимости от параметров конструктора.
НЛО прилетело и опубликовало эту надпись здесь
Если вы захотите добавить третью палочку, то ваш бизнес вылетит в трубу. Годы непрерывных маркетинговых исследований убедительно показывают, что потребителю нужны ровно две палочки — одна правая и одна левая. А любой подход, при котором тип палочки задаётся динамически (т.е. не жёстко определяется настройками штампующего станка, а становится понятен лишь в момент упаковки), приведёт к неприемлемо высоким издержкам на стадии упаковки. Упаковщикам придётся не только упаковывать, но ещё и инициализировать палочки, делая их либо правыми, либо левыми. Потребуются либо более квалифицированные люди, либо более сложные упаковочные машины.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Серьёзно подумайте о научной карьере в области экономики. Вы только что опровергли всего Адама Смита. Я не шучу. Вообще полностью. Камня на камне не оставили от его теорий. Он всю жизнь писал по большому счету о двух вещах. Во-первых, о том, что спрос рождает предложение. А, во-вторых, о том, что узкая специализация позволяет радикально снизить издержки. На последнем, кстати, основано всё технологическое развитие последних 300-400 лет. Жаль, что всё это оказалось неверным, и что потребовалось столько времени для того, чтобы понять, что всё ровно наоборот.
Научная карьера не будет слишком успешной, по теме market fallacy не потоптался только ленивый. Адам Смит предполагал некоторую умозрительную модель, в котором все экономические агенты предельно рациональны, а настоящие люди этим свойством не обладают.
НЛО прилетело и опубликовало эту надпись здесь
К сожалению, я читал Адама Смита, поэтому вам не удастся запудрить мне мозги мутными фантазиями на тему того, что он якобы писал. Ничего подобного он не писал. Более того, его теория объясняет, как именно нереализованный спрос приводит к появлению изобретений. Не к внедрению, а именно к появлению. Из вашего изложения получается, что изобретение освещения произошло, как бы, случайно. Или если не случайно, то уж точно не из-за влияния экономических факторов. Из теории же Адама Смита следует ровно обратное. Когда Томас Эдисон изобрёл лампочку, он не был отшельником, живущим высоко в горах. Он жил среди людей, чувствовал наличие нереализованного спроса и целенаправленно искал способ его удовлетворить, заработав на этом немножко денежек. И снижение стоимости на товары происходит не само собой и не сначала, а потом. Сначала возникает спрос на более дешёвый товар, который никак не удовлетворяется. И его наличие заставляет людей целенаправленно искать способы снижения цены. Собственно говоря, этот непрерывный процесс как раз и называется «научно-технический прогресс». Об инновациях все очень много говорят, но лишь те, кто внимательно читал Адама Смита, точно понимают, что это такое и откуда берутся.
НЛО прилетело и опубликовало эту надпись здесь
Я не говорю, что Смит был во всём прав. Я лишь указываю на то, что в действительности он писал вещи, прямо противоположные тем, что вы попытались ему приписать (видимо, придумав их на ходу). Хотя и недооценивать его тоже не надо. Как-никак на его теории основана вся современная экономическая наука. Ну, по крайней мере, мейнстрим. А уникальные примеры того, как предложение случайно опередило спрос, разумеется, ничего не доказывают, поскольку их можно спокойно отнести к исключениям. Все говорят, что таких примеров «полным-полно», но всё время приводят лишь несколько одних и тех же.
НЛО прилетело и опубликовало эту надпись здесь
Конкретно в этом Смит абсолютно прав. Те экономические законы, которые реально существуют, действительно объективны. Другое дело, что часто за законы выдаются какие-то случайные совпадения, потому что это очень удобно. И объективные законы отнюдь не предполагают отсутствия исключений. В противном случае пришлось бы исключить большую часть химических законов. В химии редкий закон не имеет исключений. Но это всё вообще неважно, поскольку я вообще не понимаю ваш поинт. Что вы хотите сказать тем, что ставите под сомнение закон спроса и предложения? Сделайте позитивное утверждение. Например. Закон о том, что спрос рождает предложение неверен, но зато верен другой закон: предложение рождает спрос. Хотя я не понимаю, как это может работать. Чем мотивируется человек, который делает что-то не для себя, а заведомо для продажи, если не ощущением того, что на результат его труда уже существует спрос? Просто интересно.
НЛО прилетело и опубликовало эту надпись здесь
Теперь всё понятно. Вы просто неправильно понимаете термин «предложение». Когда товар появляется в рекламе… или делается анонс… или кто-то просто разрабатывает концепт и запускает проект на Кикстартере, это не предложение. Предлагать ещё пока нечего. Это просто наживка, которая забрасывается с целью изучить наличие спроса. На данном этапе работают исключительно маркетологи. Производственная фаза в этот момент в лучшем случае лишь спланирована в общих чертах… и то не факт. Для того, чтобы появилось предложение, нужно, во-первых, запустить производство, а, во-вторых, наладить систему сбыта. Товар должен стать реально доступен для покупки и немедленного получения. Предзаказ не считается. Это тоже маркетинговый инструмент. Кстати, маркетологи как раз и занимаются выяснением наличия спроса и изучением его структуры. В том числе и в условиях полного отсутствия предложения. Это один из кейсов, с которым они умеют работать. Так что не так уж это и невозможно, как вы думаете. Более того, в этом нет ничего сложного. Маркетологи просто делают официальные анонсы в СМИ. Или организовывают «утечки» типа «а в новом айфоне не будет миниджека», чтобы понять, как публика отреагирует на такой поворот. Причём задолго до того, как первый айфон без миниджека сойдёт с конвейера.
НЛО прилетело и опубликовало эту надпись здесь
А в чём несогласие-то? Я именно об этом и говорю. Предзаказ — это просто способ исследования уже существующего спроса. Количество предзаказов и динамика их размещения определяют итоговые параметры того предложения, которое поступит на рынок через какое-то время.

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

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

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

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

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

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


  1. У вас в базовом классе есть виртуальные методы Add (добавляет элемент) и AddRange (добавляет пачку элементов)
  2. В наследнике вам необходимо подсчитать общее количество добавленных элементов.

Как вы реализуете эти методы в классе-наследнике?


Принцип Лисков ни в коем случае не обязывает класс-потомок удовлетворять всем требованиям к классу-предку.

Да неужели? Нарушающий принцип Лисков сразу:


  1. Множит на ноль повторное использование клиентского кода (ибо то, что работало с предком, благодаря "необязательности" теперь легко может перестать работать с потомком).
  2. Подкладывает колоссальную свинью всем тем, кто полагает автора класса-наследника добросовестным разработчиком, и пытается использовать его детище обычным образом, например, доверяя безопасности преобразования ссылок вверх по иерархии наследования.

В большинстве случаев это получается само и бесплатно.
Разве что в ваших предположениях.

на всякий случай предлагает всем навсегда запретить пользоваться удобным механизмом наследования реализации

Попробуйте читать то что написано. В моих рекомендациях запрета нет, ни явного, ни неявного.


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

Очень практично:


  1. Получить пачку сбоев в промышленной эксплуатации
  2. Понять, что дело в плохо спроектированной иерархии типов.
  3. Освоить бюджет на рефакторинг.

"Абстрактные интерфейсы" повеселили отдельно.

Коллега, а где в приведенном вами же примере нарушение инкапсуляции — сокрытия данных и реализации от пользователя объекта?

Собственно, откуда вы вообще взяли, что от класса-наследника надо скрывать детали реализации или лишать его доступа к данным?
Коллега, а где в приведенном вами же примере нарушение инкапсуляции — сокрытия данных и реализации от пользователя объекта?

А вы приведите свою реализацию наследника — покажу.


Собственно, откуда вы вообще взяли, что от класса-наследника надо скрывать детали реализации или лишать его доступа к данным?

Надо полагать, из названия и назначения уровня доступа private.

Для того, чтобы правильно реализовать такого наследника (представим себе, что нам это действительно необходимо), нужно знать, как работают эти методы в классе-предке (например, не использует ли в базовом классе AddRange() вызов Add()). От этого зависит реализация наследника. Это нормально.

Из существование модификатора доступа private следует возможность скрывать члены базового класса от наследников. Но никак не следует необходимость это делать.

Корректная реализация перекрытых методов требует знания деталей реализации базового класса и часто доступа к членам базового класса (для чего и существует модификатор protected). Это нормально, потому что не раскрывает детали реализации и данные вовне иерархии наследования.
Если после такого никто не скажет про хрупкость базового класса, то я даже не знаю…

Хрупким здесь является не базовый класс, а вся иерархия наследования реализаций целиком.
При написании наследников надо изучать базовый класс целиком, а не только интерфейсы.
При изменениях базовых классов всегда есть риск сломать наследников, причем не только своих собственных.
Теперь вам понятны рекомендуемые ограничения для наследования реализаций?

Мне-то всё понятно. Но, к сожалению, я не могу засчитать хрупкость базового класса или даже всей иерархии как недостаток. В противном случае придётся признать, что строительство тоже идёт не тем путём. Потому что там имеются ровно те же проблемы. Когда 30-этажное здание уже построено, нельзя просто так взять и поменять что-то в фундаменте. Но все изначально об этом знают. И сразу проектируют фундамент таким образом, чтобы не нужно было его изменять. Казалось бы, альтернатива очевидна: можно строить 2-3 этажные дома из дерева. В случае необходимости их можно приподнять на домкратах и пофиксить фундамент. Можно даже полностью заменить на другой. Гибкость потрясающая. Этажи никак не зависят от фундамента. Могут стоять на всём, что предоставляет интерфейс фундамента. Но вот только 30-этажное здание на таких принципах не построишь. К счастью, строители смогли преодолеть в себе тягу к гибкой архитектуре и стали предварительно проектировать жесткую, сознательно отказавшись от возможности полной переделки зданий после постройки. Благодаря чему мы сегодня живём в больших городах, в комфортабельных квартирах с централизованным водо-/тепло-/энергоснабжением, а не в деревянных домах с туалетом на улице.

Случай, когда тех. задание определено и не меняется в жизненном цикле программного продукта — это скорее исключение. Так что обычно получаем 30-ти этажные дома, стоящие на домкратах, с подпорками со всех сторон.

Cмотря где. Когда вы разрабатываете на переднем крае новых технологий, когда ваш стартап триумфально ворвался на рынок, хотя сам ещё пока не знает зачем, и когда есть риск того, что разрабатываемый вами интернет-магазин в какой-то момент потребуется за неделю переделать в мессенджер, тогда да… гибкость архитектуры должна быть абсолютным приоритетом. Но не все же так работают. Большинство разработок, которые в конечном итоге всё-таки внедряются и приносят реальную прибыль, происходят в консервативных отраслях. Люди с самого начала знают, что именно они делают. Среди них есть архитекторы, которые всю жизнь только и занимаются проектированием архитектуры в определённой предметной области. Архитектура у них сразу получается довольно неплохой. И требования к программам меняются настолько медленно, что за тот срок, пока их накопится заметное количество, можно спокойно разработать новую архитектуру с нуля и реализовать её в коде.

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


Из существование модификатора доступа private следует возможность скрывать члены базового класса от наследников. Но никак не следует необходимость это делать.

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


Теперь немного изменим условия задачи. Базовый класс закрыт для наследования, но использует наследование интерфейса.


  1. Как вы реализуете те же методы в таких условиях?
  2. Будут ли они зависеть от деталей реализации базового класса?
  3. От какой опасности вас защитил разработчик, запечатавший базовый класс?
Да, при наследовании реализации необходимо опираться на детали реализации базового класса. Да, изменение этих деталей, очевидно, будет ломающим для наследника. Но это почти всегда не проблема на фоне возможности перезагрузить один единственный нужный метод.

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

В 99% случаев это совсем не так. Почти всегда вся иерархия наследования создается и поддерживается одной командой, а то и одним человеком. В любой момент можно не только подсмотреть детали реализации в базовом классе, но и исправить возникающие по мере развития проблемы. Логика базовых классов и наследников изменяется за один заход одним человеком, поэтому никакой хрупкости там нет.

Ваша озабоченность оправдана в 1% случаев: разработчикам фреймворков и популярных библиотек следует задумываться над тем, о чем вы говорите. Они не могут поменять свои контракты без нарушения работоспособности клиентского кода. В этом случае выставлять наружу точки расширения в виде интерфейсов, которые пользователь может полностью реализовать сам, не заглядывая в омут чужого кода — это, безусловно, отличное решение. И повсеместное проникновение DI поддерживает этот подход.

Но призывать отказываться от наследования реализации в одноразовых SaaSах и in-house проектах, которые обречены тихо гнить внутри какого-нибудь банка — это глупый максимализм.

То бишь вы ставите экономию в несколько строчек автоматически сгененерированного кода выше, чем ликвидацию целого класса проблем сопровождения.
Дешевое комбинирование декораторов вам тоже ни к чему.
Подход, отличный от вашего, вы называете "глупым максимализмом".
Полагаю, читатели сами смогут сделать все необходимые выводы.

Как вы реализуете эти методы в классе-наследнике?

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

Еще более внезапно — в статье об этом прямо написано.
Вот только проектировать под наследование интерфейсов проще и безопаснее. И, что характерно, без всяких наездов на ООП.

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

1.У вас в базовом классе есть виртуальные методы Add (добавляет элемент) и AddRange (добавляет пачку элементов)
2.В наследнике вам необходимо подсчитать общее количество добавленных элементов.

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

Плюс юнит тестирование сразу же укажет на проблему и заставит перепроектировать абстракцию или хотя бы вынести логику подсчета в нужное место.

Это всего лишь очень простой учебный случай.


Более того, такие члены не стоит делать виртуальными в базовом классе.

Последовательно применяя эту логику мы придем к запечатанному классу без виртуальных методов.

Это всего лишь очень простой учебный случай.
Последовательно применяя эту логику мы придем к запечатанному классу без виртуальных методов.

Да, а есть и реальный.
Например довольно часто вижу подобное, не вижу и не испытываю проблем с подобным:


class Base
{
  public void Execute()
  {
    if (!Validate())
      throw new ...    
  }

  protected virtual bool Validate() => true; // еще лучше abstract, чтобы совсем без претензий к lsp (но формальный контракт и так все объясняет нормально, в отличие например от ICollection.Add())
}

Но в статье не рекомендуется (в довольно жесткой форме) вызывать виртуальные члены в предке

Вы именно что видите как место вызова, так и реализацию в предке, потому и не испытываете проблем. А теперь представьте себе, что ни то, ни другое вам не гарантировано.

О, это же интерфейсы!
НЛО прилетело и опубликовало эту надпись здесь

Зря код методов Storage не добавлен. Что если в addRange внутри себя пользуется add? Ведь это вполне может быть. Это обязательное знание при реализации StorageWithCounter, к сожалению.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

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

А этот? http://ideone.com/7d4DDJ
Мне очень интересно, как вы при ровно одинаковых ограничениях на интерфейс?
Плаха, кусок текста профукал.
Мне интересно, как сделать точно такое же решение с такими же ограничениями без наследования? Сколько, например, будет занимать код, эквивалентный написанному мной?
НЛО прилетело и опубликовало эту надпись здесь
Учитывая, что это калька с моего решения — да. Однако это изначально игра в одни ворота, Для наследования реализаций было бы корректно ставить конкретные задачи, ведь наследуется конкретный класс. Пусть это будет Множество (Set).
НЛО прилетело и опубликовало эту надпись здесь

Для дотнета можно использовать какой-нибудь динамический прокси:
https://github.com/kswoll/sexy-proxy

И эти люди запрещают мне ковырять в носу ©
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

В D тоже есть автоделегирование: http://ideone.com/B5rgw7

Отлично — здесь даже кода по сравнению с наследованием больше ровно на одно поле.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Это работает, потому что вы знаете реализацию в предке. А если бы там add() был реализован через создание массива с одним элементом и вызовом addRange()? А если в addRange() может добавиться только половина элементов, а потом будет выкинуто исключение (a la std::bad_alloc)? К сожалению, универсального решения здесь нет.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Это устранение хрупкости за счет потери гибкости.
Решение с наследованием интерфейса и декоратором такого недостатка не имеет.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Ну вот, еще в 2001 году ява-гуру все было понятно.
Сдается мне, среди комментаторов, яростно защищавших наследование реализации, найдется максимум один, поступающий в соответствии с этими рекомендациями.
А у наследования интерфейсов появляется еще один плюс — экономия на лишней документации.

Как вы реализуете эти методы в классе-наследнике?

void add(arg_t arg) { Base::add(arg); _counter++; }

И по аналогии с addRange.
Очень практично:

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

«Абстрактные интерфейсы» повеселили отдельно.

А что, наследование как инструмент гарантирует сбои, плохо спроектированную иерархию типов и недобросовестных разработчиков?
А что, наследование как инструмент гарантирует сбои, плохо спроектированную иерархию типов и недобросовестных разработчиков?

А что, выдирание цитат из контекста гарантирует их корректность?
Ничего, что процитированное вами было ответом на фразу ниже?


Практичные же люди, когда видят практическую проблему наследования реализации, берут и прибегают к зависимостям от абстрактных интерфейсов
То есть ООП — ерунда (я не адепт ФП, если что :) ).

Весь этот пафос по поводу повторного использования кода, и 3-ех китов в жизни — ерунда. :)

Свой код можно как угодно писать. Можно все на интерфейсах.

А как с таким успехом использовать внешнюю библиотеку?
Она тоже нам должна предоставить только интерфейс, а каждый обязан сам свелосипедить реализацию? :)

Решение простое:
Никаких приватных элементов. Потом даже в наследнике будут проблемы при переопределении.
То, что хотели делать приватным, делать защищенным. При этом можно придерживаться соглашения, что элементы, начинающиеся на "_" — защищенные.

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

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

А можно использовать декоратор с __call() (PHP): можем скопом добавить поведение всем методам, можем для какого-то метода добавить другое, можно вообще переопределить.

А также вопрос:
Стоит ли использовать фреймворки, где в основном все реализовано, а не интерфейсы (PHP)? :)

Другие примеры дурного использования ООП http://blog.kpitv.net/article/ооп-может-способствовать-лапше-трудному-пониманию-кода-15417/.
Классу-потомку доступны защищенные члены класса-предка. Всем остальным доступен только публичный интерфейс класса. Предельный случай взлома — антипаттерн Паблик Морозов;
Только класс потомок это не какой-то сторонний класс, а класс который является подтипом суперкласса. Что такое тип к Бертрану Мейеру, у него хорошо описано.

Реально изменить поведение предка можно только с помощью перекрытия виртуальных методов
А что тут плохого?

Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку;
не всем, а контракту, включая public, internal, protected.

Информация из пункта 4 зависит от реализации класса-предка, включая приватные члены и их код.
Именно по этому иерархии наследования надо строить очень обдуманно и взвешенно.

Вообще исходя из предложенных автором пунктов, вы просто не понимаете где это можно использовать, а где нет. Ваши решения равносильны тому, что не запретить использовать инструмент A,B,C потому что они могут травмировать или убить — крайне глупое и необдуманное решение.
такое ощущение что текущие программисты просто не понимают полезности и опасности инструмента и как решение, сразу хотят его запретить.
С одной стороны наследование реализации нарушает инкапсуляцию и это плохо, с другой стороны наследование по своему определению способствует такому принципу как — повторное использование кода и это хорошо.
На практике я осознанно не избегаю наследования только потому, что оно нарушает какой-то теоретический принцип. Правильное использование наследования может приводить к очень красивому коду.
На деле когда наследование напрашивается — я его делаю, но слежу за тем, чтобы все не испортить. Например, стараюсь ограничивать глубину наследования 2-3 поколениями, не более.

Вообще-то повторное использование кода при наследовании имеет два варианта использования:


  1. Повторное использования кода базового класса. Обычно ограничено одним классом при наследовании реализации для большинства языков.
  2. Повторное использование клиентского кода со всеми наследниками. Возможно только при соблюдении принципа подстановки Лисков, что при наследовании реализации ломает инкапсуляцию и делает иерархию хрупкой.
Вот возьмем c++. Где ломается инкапсуляция от наследования? private члены недоступны наследникам. protected — доступны наследникам, но недоступны как публичный интерфейс.
Начнём с того, что объективность «нарушения инкапсуляции при наследовании» вами указана как аксиома. Хотя вам бы стоило это доказать. Не простым примером, а, как полагается математикам, через логические выводы.

Продолжая тем, что выдуманный вами пример не корректен в силу того, что наследование работает не так, как написали вы, «сделай мне слона из мухи», а из конкретной реализации, о которой класс-наследник обычно имеет значительный объём информации. Когда я наследуюсь от хеш-таблицы, я знаю, как ведут себя ВСЕ публичные методы. Что не скажешь об интерфейсах iContainer.

И заканчивая выдуманными проблемами на производстве, которые обязательно проявятся только из-за наследования.

Вот вам задачка. Нужно сделать хеш-таблицу, в которую нельзя было бы вставлять элемент с одним и тем же ключём дважды за всё время жизни таблицы. Мне нужно переопределить только методы вставки и удаления, примерно 15-20 строк всего. (Плюсы портят статистику из-за operator[], но не значительно) Вам?
На практике нельзя обойтись одними интерфейсами. Идеальный интерфейс состоит из одного метода. Если методов больше, то голый интерфейс не выражает отношений между этими методами. Взять хотя бы ICollection из C#: сам интерфейс не гарантирует, что если мы вызвали Add(), то Count изменился на единицу или, что Contains() найдёт добавленный элемент. Автогенерённая имплементация со всеми throw new NotImplementedException() уже удовлетворяет интерфейсу и до рантайма никаких ошибок не обнаружится.

Так мы приходим к Контрактам. Для. Net есть Code Contracts. Интерфейс и контракты — это уже лучше, чем просто интерфейс, но Контракты заставляют всех имплементаторов повторять часть кода, которая отвечает за поддержание контракта.

Тогда становится логичным вместо интерфейса дать абстрактный класс, который гарантирует выполнение контракта, где вся изменяемая часть представлена protected abstract методами, а все остальные методы sealed. Можно и интерфейс оставить, для тех, кому абстрактный класс жмёт, но в 99% случаев можно обойтись классом, а не интерфейсом, если это не так, то абстрактный класс просто плохо сделан.

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

Таким образом, на практике, я рекомендую абстрактные классы с только sealed и abstract методами и интерфейсы с одним методом.

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

При наследовании интерфейсы, в большинстве случаев, выражают всякие ability (и, зачастую, имеют суффикс able, e.g. IDrawable, IEquatable), а абстрактные классы определяют существо объекта, его реальную принадлежность к категории объектов. Интерфейс с этим не справляется т.к. если мы не обвяжем его контрактами он не гарантирует, что наследник обладает нужным поведением.

Собственно это всё было к пункту 2:
Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.


Все пункты в теоретическом решении, кроме пятого, верны, но на практике, вместо пункта 2, соблюдайте open-closed principle и тогда вам нечего боятся наследования от абстрактного класса, который содержит реализацию, отвечающую, за выполнение того, что мы априори ожидаем от этого объекта. Это зачастую лучше голого интерфейса т.к. он не гарантирует отношений между своими членами и все наследники должны повторять этот код с нуля.

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

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

Нет, лучше пусть это будет языковая конструкция Method Contracts
Или тогда уже юнит тесты.


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

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

В требовании знать, вызываются ли в предке методы Add и AddRange друг из друга.

НЛО прилетело и опубликовало эту надпись здесь
Видимо совсем тупенький — всё равно не понимаю, зачем это надо знать

Затем, что без этого знания вы рискуете учесть некоторые добавленные элементы дважды.


посчитать количество элементов перед вызовом предка

И огрести от многопоточности.


В любом случае, если мы наследуем какие-либо публичные/защищённые методы, мы ведь должны понимать, как они работают.

Это и есть поломка инкапсуляции. Используя объект я должен знать что он делает, но не должен как.

Это справедливо для инкапсуляции вовне класса. Но внутри класса вы должны знать, и что он делает, и как, то есть инкапсуляции внутри классов быть не должно.
За исключением редких случаев, когда нужно защитить некоторые поля, вроде ooid, от изменения? Не все же хотят делиться своей бизнес-логикой. Спрятал все данные класса в private структуру, её определил в cpp-файле, в классе только указатель. Всё работает, но как — совсем не понятно.

Ну вот вам и инкапсуляция внутри классов, а вернее в предке, о поломке которой и идет речь.

Если вы о «редких исключениях», то это скорее внутренняя защита от дурака. К примеру, meta-информация о классе не должна быть доступна для изменения пользователем и при этом она не обязательно константа. Технически, она не является частью класса, потому вопрос реализации их связи очень сложен и имеет разные варианты решения. В ряде случаев, приватное наследование — одно из наиболее оптимальных.

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


Но и все-таки, даже останавливаясь на плюсах, если бы это был такой уж редкий кейс, не было бы private, хватило бы ptotected и код ревью.

Во многих языках нет множественного наследования. Это ещё ничего не значит.
НЛО прилетело и опубликовало эту надпись здесь
Я пользуюсь ООП (не путать с PLO) так:

Есть определённая жёстко заданная логика — её реализуют публичные классы:

public 
function GetOne;
function GetTwo;


Есть абстрактные методы, на место которых будет подставлены методы дочерних классов:

  public 
  procedure Open; abstract;
  function TakeOne : type; abstract;
  function TakeTwo ; type; abstract;
  procedure Close; abstract;

function MyBaseClass.GetOne : type;
begin
  Open;
  result := TakeOne;
  Close
end;

function MyBaseClass.GetTwo;
begin
  Open;
  result := TakeTwo;
  Close;
end;


Каких-либо граблей в таком подходе не видно (кроме Abstract Error).Логика (открыть-взять-закрыть) прописана в базовом классе, хотя детали реализации необходимо реализовать в дочерних. Если нельзя использовать базовый класс для описания логики — то нахрена об вообще нужен?
НЛО прилетело и опубликовало эту надпись здесь

Вторая формулировка не является ужесточением первой.
А первую — очень тяжело проверить. Вызов может быть сколь угодно косвенным.

НЛО прилетело и опубликовало эту надпись здесь
Если нельзя использовать базовый класс для описания логики — то нахрена об вообще нужен?

Для описания контракта.

итого несколько контрактов и несколько исполнителей представляют собой одну программную сущность
по поводу Add и AddRange
обычно такие задачи решают так

http://ideone.com/fxPGq5

Многопоточности не бывает?

это пример, в реале если исходный объект обеспечивает потокобезопасность значит надо просто воспользоваться его механизмом в потомке, как это сделать надо по месту смотреть.
потокобезопасный вариант
http://ideone.com/VUG3Tg
зыЖ написано «отбалды» на яве не пишу.
Ты изначально принимаешь слабую позицию — пытаешься что-то реализовать над интерфейсом. В жопу интерфейс, это наследование, возьми хеш и посчитай на нём. А потом скажи им, «Мне понадобилось 30 строчек. Ваша очередь.» И посмотри, как они будут рвать свои жопы, чтобы просто сделать это.
ну дык, в рамках поставленной задачи.
удивительно что такие простые ситуации вызывают сложности.

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


Причем после первого непотокобезопасного варианта — это должно было ну просто сразу всплыть и остановить, чем продолжать издеваться над будующими разработчиками этого кода)

По задаче нельзя менять предка, очевидно что если можно то надо.

… но ведь это преподносится как пример несостоятельности ООП, тобишь неспособности решить эту задачу наследованием в принципе, хотя это не так.
По задаче нельзя менять предка, очевидно что если можно то надо.

Особенно если предок из чужой библиотеки.


… но ведь это преподносится как пример несостоятельности ООП, тобишь неспособности решить эту задачу наследованием в принципе, хотя это не так.

Вы точно читали статью? В одной процитированной фразе три ложных утверждения.


  1. В статье показывается непротиворечивость ООП — наследование интерфейсов не ломает инкапсуляцию.
  2. Задача легко решается наследованием интерфейса,
  3. Решение наследованием реализации требует знания приватных деталей реализации предка (сломана инкапсуляция).

Вы где-то нас обманываете.


msts2017 пишет:


По задаче нельзя менять предка

Здесь вы соглашаетесь


Особенно если предок из чужой библиотеки.

А следом, внезапно...


Задача легко решается наследованием интерфейса,

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

Получить гарантии.
https://play.rust-lang.org/?gist=27418cdffe1bbb45bde4c7d3058ec162

Хм. То есть код msts2017 http://ideone.com/lh56Ds, большую часть которого составляет борьба с унаследованным поведением, даёт больше гарантий? Это уже смешно.

Да нет, у него тоже бред, потому что он попытался руками сделать мутекс. Вполне подходящие экземпляры для аксиомы Эскобара.

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

Можно было и не писать реализацию по умолчанию, а реализовать add_range непосредственно для CollectionImpl. Это ничего бы не изменило. Точнее код бы стал немного менее самодокументированным. Но в документации к интерфейсу всё равно пришлось бы писать, что результат вызова add_range должен быть эквивалентен последовательному вызову add для всех элементов (от начала к концу / от конца к началу / в порядке определяемом реализацией).


Далее нигде не используется тот факт, что add_range реализован с помощью вызова add.


Но в этом же и слабость интерфейсов — поведение реализаций вообще никак не контролируется.

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


Чтобы гарантировать, что дочерние классы не сломают инварианты базового, базовый класс не должен содержать protected и public полей, и должен учитывать, что перегруженные реализации виртуальных методов могут не вызывать базовые.


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

Твою ж душу! Я вчера усирался целый час, а эта хрень не сохранила мой код в ссылке, а лишь цинирчно мне показывала его в редакторе! Я уже обожаю Rust вплоть до эрекции!

> Далее нигде не используется тот факт, что add_range реализован с помощью вызова add.
>> fn add_range(&mut self, vs: &[u32]) {
>> self.cnt.fetch_add(vs.len(), Ordering::SeqCst);
А что, если добавляются только чётные? Или если коллекция может содержать только уникальные элементы? Или в Add есть ещё какая-нибудь проверка?
Эти же вопросы относятся и к виртуальным методам при наследовании. Ничем, кроме уровня доступа ко внутренностям, они не отличаются.

Всё остальное — суета.
А что, если добавляются только чётные? Или если коллекция может содержать только уникальные элементы? Или в Add есть ещё какая-нибудь проверка?

В примере я сделал реализацию интерфейса CountedCollection для конкретной реализации интерфейса Collection. Там не может быть только четных или всего прочего.


Если нужно сделать единую реализацию CountedCollection для любой реализации Collection, то очевидно, что такая задача не решается. Но и наследование здесь ничем не поможет, если неизвестно как реализуются add и add_range в базовом классе.

Если нужно сделать единую реализацию CountedCollection для любой реализации Collection, то очевидно, что такая задача не решается

Решается через наследование интерфейсов.
"Только четные" — нарушение условий публичного контракта.

А где он прописан? Я его не вижу. Я вижу «добавляет элемент». Не «обязательно добавляет все элементы и хранит их в порядке добавления до уничтожения контейнера», не «Добавляет все элементы и хранит их отсортированными в порядке возрастания», а просто «добавляет элемент». Не более того.

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

Вооот. Наконец до вас дошло! ISerializable гарантирует только наличие методов, но не их поведение. У вас нет гарантий, что A == unserialize(serialize(A)). То же, в принципе, верно и для виртуального метода, но мы, как минимум, можем оставить тесты, которые нельзя не пройти.
Ничем, кроме уровня доступа ко внутренностям, они не отличаются.

А как же отсутствие проблем с хрупкостью базового класса?

А какая разница? Ваш код упадёт И в случае неверной стратегии, И в случае неверной перегрузки. Ваш код упадёт И в случае неверной реализации интерфейса, И в случае неверного наследования. В чём хрупкость-то?

Хрупкость базового класса, вообще-то, это — не рантаймовая проблема, а архитектурная. Небольшие и выглядящие неопасными изменения в базовом классе могут сломать поведение дочерних классов, потому что наследование создаёт множество неявных зависимостей между базовым и дочерними классами (допустимый порядок вызовов методов, ожидаемое состояние базового класса при вызове метода и т.п.).


Ладно. Пожалуй хватит, уже по пятому кругу пошли.

Эта же проблема возникает и при агригации, и при добавлении стратегий. Да что там, это абсолютно верно для всего программного кода в мире. Иначе бы Wine не был реализацией «баг в баг».

Несложно предоставить новый интерфейс в дополнение к обратно-совместимой по багам версии. Предоставить новый базовый класс в дополнение к старому как-то уже нетривиально.

А можно подробнее?
конкурентный вариант с вложенными блоками подсчета
http://ideone.com/lh56Ds
НЛО прилетело и опубликовало эту надпись здесь

Суть идеи ясна и так, посмотрите второй потокобезопасный вариант, там немного нагляднее.

По условию нет запрета на count, далее простая логика если Add и возникает потребность узнать сколько добавили значит это контейнер элементов и count скорее есть, не имеющее значение допущение.
Но это не важно в потокобезопасном варианте count не используется.

В условиях никакого count нет ни явно ни неявно.

НЛО прилетело и опубликовало эту надпись здесь
Возможно, например, что добавляются не все элементы, а только те, которые удовлетворяют некоторому сложному критерию, который возможно зависит от текущего состояния. Причем алгоритм этого выбора реализован в приватных методах базового класса.

Этого в условиях задачи нет. Если бы было — методы возвращали бы результат вставки. Или вопрос был бы про отправленные на добавление элементы. В общем — заморачиваться на случай неуспеха вставки не надо. Стандартное соглашение по умолчанию — любой метод делает все или ничего (во втором случае бросает исключение).


Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.

При использовании наследования реализации. При наследовании интерфейсов — возможно и несложно.


Как быть если он этого не сделал? Объявлять интерфейс самим и писать наследник-декоратор его реализующий?

Да, причем в своем коде вместо чужого класса использовать этот интерфейс.

НЛО прилетело и опубликовало эту надпись здесь
Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.

Вообще-то возможно. Пример, где-то тут уже проскакивал: запоминаем счётчик, вызываем метод, устанавливаем счётчику правильное значение. Тут возможно лишь незначительное пенальти по производительности в зависимости от реализации родителя, но логика не сломается.

Многопоточность уже отменили?

Сейчас в моде коммуникация через каналы, без совместного доступа к общей памяти.

Этого в условиях задачи нет.

А shared memory — есть?

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

А возможность многопоточного использования предполагается по умолчанию.

В тех языках, что использую я (JS, D) — не предполагается.


Ваше, впрочем, и с одним потоком может сломаться. Например, если реализация предка использует файберы.

Тут вы правы.

| А возможность многопоточного использования предполагается по умолчанию.
Да не, с чего это, в Java\C# не предполагается.
А в каких языках объекты сходу потокобезопасны? (я просто не в курсе)

В D, например, пишешь shared class и для объекта автоматически создаётся мьютекс и все публичные методы его захватывают. А если не напишешь — не получишь доступа из другого потока без плясок с бубном.

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

А смысл так делать? Куда лучше, когда по умолчанию к объектам нет доступа из другого потока. Хочешь передать данные — посылай сообщения.

Ну дык автор статьи считает очевидным — «возможность многопоточного использования предполагается по умолчанию».
вот и спрашиваю где такое по умолчанию. хотя, по мне, это исключение.

Публикации