Комментарии 349
А это общее правило параноика для любого ЯП (хотя больше всего его любят в C++): если вы до конца не определились как вам оформить класс, то делайте его приватным, делайте его конструктор приватным, делайте все методы приватными и невиртуальными, и запрещайте наследование. Чтобы изменить любой из этих пунктов нужна вполне конкретная причина.
А на мой взгляд приватный класс по умолчанию — очень некрасивый шаг по отношению к пользователям библиотеки.
Инкапсуляция на 100% — это утопия, так не бывает в жизни. ООП — лишь средство более элегантно решить некоторые (но далеко не все) проблемы, но не нужно его идеализировать и обожествлять! Если предоставить «клиенту» посмотреть на «внутренности» — это не плохо, просто «клиент» (пользователь библиотеки), принимая решения, должен осознавать последствия сделанного им выбора. Я уверен, что разумный доступ к внутренностям объекта (и одновременным пониманием, чем это чревато) позволяет писать более эффективный код, и делать это быстрее, а не тратить время на героическое преодоление правил. Правила должны помогать осуществлять задумки, а не мешать им. А вот ответственность за сильное связывание, спагетти-код и т.д. лежит не на языке, и даже не на библиотеке, а на конкретном разработчике ее использующем.
И если библиотека хорошо выполняет свою задачу, пусть она трижды вся public, я буду ее использовать, и скажу ее разработчику лишь спасибо!
Меня всегда удивляло стремление отдельных людей выдумывать «правила», а потом героически их преодолевать!
Я не пропагандирую это "правило параноика" и сам ему не следую. Не это конкретное правило, но правила вообще имеют смысл когда уровень квалификации "среднего" участника команды приводит к регулярным наступаниям на грабли. С точки зрения поставки фич бизнесу всегда проще сказать "всегда делай вот так", чем ждать пока коллега соберёт все грабли, пропустит все дедлайны, научится и станет делать "правильно" осознанно.
И если библиотека хорошо выполняет свою задачу, пусть она трижды вся public, я буду ее использовать, и скажу ее разработчику лишь спасибо!
Ну не всё так просто:
- У разработчика при таком подходе будут связаны руки: т.к. всё public и всё часть контракта библиотеки, нельзя будет просто так пойти и отрефакторить что-то. Каждый новый класс — это minor релиз, каждое переименование любого класса — это major релиз. Patch релизы будут предполагать только изменения кода реализации существующих классов и методов.
- Вы в итоге разработчику спасибо не скажете, т.к. на ранних стадиях развития библиотеки каждый второй релиз скорее всего будет major — с изменённым контрактом.
вот так копаешься в либах от Microsoft: вроде всё понятно, так и так, ща вот тут подлезу и будет шоколадно вообще… итут НННА ТЕБЕ INTERNAL.
Наследники гиперчувствительны к любым изменениям предка.
И что же теперь, ограничиваться одним уровнем наследования от абстрактного класса?
Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм
Бывают случаи, когда базовый класс сам по себе самостоятелен и реализует заложенный функционал. Но множество частных случаев требуют переопределения всего лишь одного-двух методов. Внимание, вопрос: с точки зрения автора статьи, как обходиться в таких ситуациях?
Бывают случаи, когда базовый класс сам по себе самостоятелен и реализует заложенный функционал. Но множество частных случаев требуют переопределения всего лишь одного-двух методов. Внимание, вопрос: с точки зрения автора статьи, как обходиться в таких ситуациях?
Не автор, но прокомментирую :-)
Часто забывают, что ООП — это от слова "объект", а не от слова "класс". Задача программиста — сделать так, чтобы получались объекты, обладающие требуемым поведением. В среднестатистическом мейнстримном языке типа C#/Java есть 2 подхода:
- Если мне нужно новое поведение, я наследую существующий класс и переопределяю какой-то аспект его поведения.
- Если мне нужно новое поведение, я немного иначе "строю" объект, поведение которого мне нужно изменить.
Если весь код написан таким образом, что единственный способ сконструировать объект это new ЧтоТоТам()
, конечно тут кроме наследования ЧтоТоТам нет вариантов. Но можно же изначально заложиться на new ЧтоТоТам(new КакЯДелаюВотЭто(), new ИЕщёКакЯДелаюВотЭто())
. В таком случае получается на порядок больше мелких классов, где одни классы описывают какой-то конкретный аспект поведения, а другие — просто скручивают несколько таких аспектов поведения в один "настроенный объект". Статья автора, если я правильно понял, про такой подход.
2) Это верно только для простых алгоритмов, которые будут вызываться изнутри кода. Вроде SortedVector(SortingAlgorithm); SortedVector(new Bubble()); SortedVector(new QSort());
Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта, ваш подход ещё хуже, так как требует объявить ваши КакЯДелаюВотЭто и ИЕщёКакЯДелаюВотЭто друзьями класса ЧтоТоТам. Далеко не все ЯП позволяют потомкам «друзей» оставаться «друзьями».
Это называется АОП
Это ни в коем случае не АОП. Это самое обычное ООП + делегирование.
Это верно только для простых алгоритмов… Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта, ваш подход ещё хуже
Посмотрите примеры использования паттерна "Стратегия" — это собственно делегирование в чистом виде и есть.
Это ни в коем случае не АОП
Думаете? Подумайте лучше.
Посмотрите пожалуйста внимательнее на вашу ссылку, ключевое слово — сквозная функциональность:
Для программы, написанной в парадигме ООП, любая функциональность, по которой не была проведена декомпозиция, является сквозной
В случае моего примера выше мы в явном виде провели декомпозицию.
В случае примера выше вы написали три бессмысленных названия.
И будьте скромнее, ваше вашество, не стоит себя на «мы» величать. Если вы, конечно, не коллективный автор.
Как я уже писал выше, это работает для простых алгоритмов. Как только у вас появляется сложное поведение, которое не умещается в простую стратегию, все ваши красивые приёмы с декомпозицией перерастают в монстров.
Либо это будет чудище Франкенштейна, сшитое из десятков кусков, каждый метод которых принимает по дюжине разных структур, и тогда собирание монстра превратится в настоящее испытание и станет источником багов, а интерфейсы самого чудища в поисках упрощения обрастут сотнями ненужных обывателям внутренних методов.
Либо это будут сильно связанные классы, знающие о кишках друг друга и, в итоге, приводящих к тем же проблемам, что и простое наследование, но не дающее его преимуществ.
Либо это будет очередное АОП.
Вы не можете избавиться от сложности, её можно только выдавливать из одного места в другое. Декомпозиция выглядит достаточно заманчиво и очень часто именно она должна использоваться вместо наследования. Но не всегда.
Затем, что именно принцип Лисков позволяет обеспечить полиморфизм "в рамках спецификации класса-предка".
Да, но наследование ведь изначально предполагает, что потомок реализует спецификацию предка?
Это и есть принцип Лисков, соблюсти который при наследовании реализации нетривиально.
Нет. Есть принципиальная проблема, что «спецификация предка» — это не интерефейс, и формально нигде не описана. То есть меняя поведение в наследнике вы реализуете «спецификацию», которая есть только у вас в голове. А формально, скорее всего (если вы не наследуете абстрактный класс — точно), нарушаете LSP.
Почему вы наследование интерфейсов называете кастрацией?
Как китайский "iPhone". Выглядит так же, а что внутри неизвестно.
Все с точностью до наоборот. Несмотря на то, что кода при наследовании реализации пишется меньше, соблюсти совместимость с оригинальным айфоном заведомо сложнее, чем при наследовании интерфейсов и композиции.
Неявное предположение "меньше кода — проще сопровождение" в данном случае неверно.
Но почему я должен отказываться от наследования в собственном коде?
Вы никому ничего не должны.
Цена наследования реализации, если незапечатанные классы недоступны снаружи, не выходит из-под вашего контроля, но не является нулевой. Минимальной единицей контроля аффекта от изменений становится не один класс, а вся иерархия.
Дело в том, что одинаковая функциональность оказывается реализована во множестве мест.
Не надо путать композицию с копипастой — одинаковая функциональность реализуется ровно один раз.
Скажем полезно будет указать, что вычислительная сложность метода justSolveTheProblemHereAndNow(int n) составляет O(n!)
Это не нарушение инкапсуляции, а уточнение публичного контракта.
Нонсенс. Если внешний код должен иметь доступ к полю — это не приватное поле.
Если вы делаете такое поле protected только для того, что бы оправдать появление наследника — это вы зря. «Друзья» — это вообще костыль и инверсия абстракции.
Простой пример: UndoStack. Если нам понадобится расширить его функционал возможностью коммитов на внешнюю машину (с вероятными былинными отказами), то без доступа к буферу команд ничего не получится. Никто не оставляет публичными команды манипуляций с буферами и не оставляет их для стратегий. Значит, единственная возможность — лезть в потроха. С наследованием всё просто. А без него?
Если вам надо UndoStack, а он sealed, или буфер не protected, и исходников нет — то жопа.
Если исходники есть — вытащите стратегию, это типа в 3 клика делается. Код с вашим наследником совпадёт чуть менее, чем полностью.
> 3) Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
> 4) Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.
Такие классы, обычно, поставляются фреймворками. Очень часто фреймворк в бинариках поставляется, и далеко не факт, что его можно будет пересобрать без лишней боли. Очень часто любое изменение исходных кодов фреймворка (равно как и дублирование кода из фреймворка с последующей правкой под себя) требует изменения лицензии. Очень часто архитекторы даже не задумываются, что такая функциональность в принципе может понадобиться, или оставляют это на откуп конечным разработчикам, которые ставят sealed/final на автомате Вот и думайте, что делать, когда пойдёт волна свежих программистов, которые будут поголовно запечатывать свои классы потому, что Bonart так написал.
Так же, напомню, что враппер не обладает никакими из свойств оборачиваемого объекта, даже если их унаследовать от одного интерфейса, связи «is a» точно не получится, что, конечно, не важно, когда у тебя понятия «класс» нет и не предвидится или когда архитектор предусмотрел это и потребовал интерфейс, но так бывает далеко не всегда.
А ещё мне очень интересно посмотреть на реализацию через стратегии и врапперы? Не забывайте, что команды в стеке могут быть как подлежащие коммитам, так и локальные, и портить совместимость с ними нам нельзя.
Наследуем реализации — скрываем слишком много, наследуем интерфейсы — не накладываем ограничений, наследуем контракты — не проверяем корректность реализации, проверяем корректность реализации — наследуем реализации…
Наличие виртуального метода подразумевает подготовку к наследованию. Если вдруг(!) у публичного класса подготовка ненадлежащая — sealed, в качестве исключения.
У публичного класса в 95 % случаев подготовка отсутствует, в 4,9% — ненадлежащая.
Пометка всего sealed сродни установке лежащих полицейских среди поля — машины там не гоняют, зато люди спотыкаются.
У публичного класса в 99% нет виртуальных методов.
У класса, спроектированного для наследования, виртуальные методы есть всегда.
От класса без виртуальных методов наследоваться бесполезно.
Ну, это вы загнули. Часто вы в C# перекрываете виртуальные члены Object? ) Если бы ToString() и GetHashCode() были в интерфейсе — вообще не перекрывали бы.
Часто вы в C# перекрываете виртуальные члены Object?
Да, хочу чтобы сравнение и ключи в dictionary работали нормально.
Еще хочу чтобы в отладчике и логах информация была читаемой.
И это ничего, что наследование от object слегка недобровольное?
> И это ничего, что наследование от 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
sealed на уровне метода подразумевает, что родитель был сначала virtual, потом был наследник, а потом решили, что эта иерархия плохо подготовлена. На мой взгляд, это «мы тут плохо надизайнили, но надо паблишить». Для либы — хак, для своего кода вообще непонятно кому надо.
вы запечатаете не свой метод, а библиотечный, и «Хотите наследовать? Наследуйте!» станет издевательством.
В остальном — правильно: sealed позволяет публиковать недоделанную фигню, имитируя заботу о том, кто с матами будет пытаться обойти sealed.
И, главное, зачем именно нужен sealed, если я могу матюгнуться, форкнуть и его удалить. Опубликовав исходники «не вводить во искушение» уже не в вашей власти.
Да проще на лету подменять код и ставить везде паблик. Вопрос пары лишних строк в билдфайле.
> Я закрыл опасное место
Вы или крестик снимите, или одно из двух.
Вот только пользы от такого наследования около нуля — без перекрытия методов полиморфизма не будет.
Вообще, как только в голове появилось «архитекторы даже не задумываются», то наследоваться от такого — не самая светлая мысль.
Впрочем, ставить sealed на автомате — тоже не очень хорошая идея. Надо запрещать случайно прострелить себе ногу, а не осмысленно.
Вы немного не понимаете, в чём дело. Порой нужно расширить функциональность для тех классов, в которых изначально такое расширение не предусматривалось не по злому умыслу, а по незнанию. Пример с UndoStack я уже приводил. Если UndoStack вне области, доступной для модификации (как минимум, по банальной причине бинарной совместимости), ваш подход мне ничего не даст. Ничего не дадут мне ни интерфейсы, ни подходы в стиле Rust, ни врапперы. Я либо смогу получить доступ к полям класса, либо мне придётся реализовывать всё самому, и ничего мне не поможет. Да, если на вход объекта передавать интерфейс, а не конкретный класс, то гораздо проще объект заменить, но мне не нужно его заменять.
Если так, тогда решений, отличных от саморучной реализации всех классов, просто не будет, и механизм коммитов не будет связан со стеком команд.
К примеру, вы сделали интерфейс потока данных. И ожидаете, что если запись не пройдёт успешно, вам возвратят ошибку. А вам, допустим, выбрасывают исключение, которое кладёт всё приложение, ведь его никто не ловит. Формально вы не виноваты.
Ладно, это пол-беды. другой пример. Вы в вашей стратегии ждёте контейнер для хранения объектов. И ожидаете, что вы сможете в него сбрасывать ссылки на временные объекты. Последнее, что вы ожидаете — это что у контейнера будет глобальное состояние, и объекты не умрут после удаления самого хранилища. В этом случае формально виноват будет пользователь, хотя он не нарушит никаких соглашений. Ведь в интерфейсе, который вы ожидали, не написано, как должны храниться объекты на протяжении жизни экземпляра приложения. С наследованием такая херня не пройдёт, контейнер выполняет свой деструктор, который выполнит деструкторы всех объектов, и даже если в глобальном пространстве сохранятся ссылки, они будут битыми и относительно безвредными.
Ладно, у нас же камень преткновения — виртуальные методы. Так и быть, пусть у нас есть класс фигуры. И у него есть виртуальный метод для вычисления центра масс. «Стратегия вычисления центра масс!» — возразите вы. И не сказать, что ошибётесь. Но есть одно но. В простейшем случае центр масс зависит от геометрической формы. Однако он может зависеть от распределения плотностей… И от скорости… И от импульса… Ускорения… Энергии… Так что будет принимать стратегия на вход? Какой у неё интерфейс? Снова перекладываем всё на шею пользователя? Самое грубое решение. В чём же преимущество виртуального метода? В том, что не нужно придумывать интерфейс стратегии, доступ ко всем нужным данным уже имеется. Почему бы нам не сделать доступ ко всем этим данным из интерфейса? Ну, можно, и даже нужно, если это какой-нибудь физический движок. А если нет? Или, если, например, мы не можем предоставить однородный доступ к данным, что тогда? Например, если у нас фракталы есть. Ведь для отрисовки фрактала не нужно знать его форму, достаточно отрендерить его в текстуру и натянуть на AABB, а центр масс вычислять по аналитической аппроксимации. Чисто теоретически, всё это можно сделать на чистых интерфейсах. Будет ли это проще? Будет ли меньше ошибок?
Конечно, в моих примерах много «но» и «если», но ведь и в ваших этих «если» не меньше. Можно придумать ещё много примеров, в которых кто-то будет допускать ошибки, в которых формально вы не виноваты. Однако ровно так же не виноваты и те, кто не закрыл свои классы для наследования. Единственная разница, ошибки произойдут в их вотчине. Вот и всё.
Что?
> Если UndoStack вне области, доступной для модификации (как минимум, по банальной причине бинарной совместимости), ваш подход мне ничего не даст.
Мой опыт говорит, что в UndoStack сами команды всё равно приватные (в крайнем случае internal), наследованием там ничего не поправить. Если код не задуман для расширения — наследование не спасёт.
Если вы меня хотите убедить, что наследование можно использовать как грязный хак ради «паблика Морозова» — соглашусь, можно.
И что же теперь, ограничиваться одним уровнем наследования от абстрактного класса?
Как будто что-то плохое. Кто хоть раз ковырялся в семи уровнях наследования WPF, тот меня поймет.
Внимание, вопрос: с точки зрения автора статьи, как обходиться в таких ситуациях?
По мелочам обычно выручает декоратор. Кода получается больше (правда 95% шумового кода декоратора все равно нагенерит решарпер), но зависимость ослабляется до уровня публичного интерфейса. Заодно можно бесплатно комбинировать несколько декораторов.
Как будто что-то плохое. Кто хоть раз ковырялся в семи уровнях наследования WPF, тот меня поймет.
Один пример плохой реализации не может быть доказательством несостоятельности концепции. Например, иерархия классов виджетов Qt вполне себе адекватна и красива.
По мелочам обычно выручает декоратор
Так а в чем профит то? Если мне нужно дополнить всего один метод, то в случае с наследованием я переопределю всего один метод: вызову реализацию предка и допишу пару частных операторов. В случае с декоратором мне придется писать класс-декоратор, который будет либо унаследован от базового декоратора (что следуя вашей логике плохо), либо продублирует код базовой реализации (что уж точно нехорошо).
Один пример плохой реализации не может быть доказательством несостоятельности концепции. Например, иерархия классов виджетов Qt вполне себе адекватна и красива.
И чем же плохо ограничение, препятствующее плохой реализации и облегчающее хорошую?
Так а в чем профит то?
Вы точно читали статью и мой предыдущий комментарий?
Профит банален: при наследовании от класса вы имеете зависимость от приватного интерфейса, при композиции — только от публичного.
В случае с декоратором мне придется писать класс-декоратор, который будет либо унаследован от базового декоратора (что следуя вашей логике плохо), либо продублирует код базовой реализации (что уж точно нехорошо).
Почему вы проигнорировали композицию с делегированием? Вы точно знаете, что такое декоратор?
Разница между наследованием и композицией:
- Минус одно наследование.
- Плюс одно приватное поле
- Плюс "шумовой" делегирующий код (можно избавиться используя динамическую типизацию или метапрограммирование).
Пункты 2 и 3 — экономия при написании кода, пункт 1 — при сопровождении. Итоговый баланс немного предсказуем.
Наследование как "упрощенная запись делегации" — ложь.
"Без бойлерплейта" — ложь.
"Работает точно так" — снова ложь.
Контрольный вопрос — как скомбинировать функционал нескольких наследников с мелкими модификациями/дополнениями функционала?
С декораторами это делается элементарно.
``«Без бойлерплейта» — ложь'' — ложь
``«Работает точно так» — снова ложь'' — ложь
> Контрольный вопрос — как скомбинировать функционал нескольких наследников с мелкими модификациями/дополнениями функционала?
Мультинаследование.
Мультинаследование
Недоступно в большинстве языков.
По поводу реализации — я джва году жду расширенное наследование, но в статических языках никто пока не запилил, а в динамических прототипных царит такая анархия и беспринципность, что мне страшно туда соваться. Под расширенным наследованием я понимаю возможность объявлять класс внутри класса и переопределять этот класс при наследовании (так что все упоминания, включая типизацию и инстанцирование) заменяются на переопределённый класс. Если обычное наследование — это делегация объектов типа функция, то посредством расширенного можно засахарить делегацию произвольных классов. Плюс ещё очень хочется функции 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, если хотите улучшить производительность. Но даже в этом случае знать вам надо только контракты методов, а не то как они используются.
Достаточно иметь контракт переопределяемого метода.
В контракт виртуального метода всегда входит контекст его вызова в классе-предке, который является приватным по построению.
А вызываться он может как угодно в соответствии с контрактом.
Вам же не требуется для реализации метода интерфейса знание где и как он будет вызываться.
Для интерфейса не требуется как раз потому, что в предке ни один из методов интерфейса не вызывается по определению.
А вот для класса давайте рассмотрим следующий вариант использования:
- У вас в базовом классе есть виртуальные методы Add (добавляет элемент) и AddRange (добавляет пачку элементов)
- В наследнике вам необходимо подсчитать общее количество добавленных элементов.
Как вы реализуете эти методы в классе-наследнике?
Для интерфейса не требуется как раз потому, что в предке ни один из методов интерфейса не вызывается по определению.Верно для C#, но не для java >= 8, Rust, scala, kotlin, etc
Да, некоторые классы предназначены для расширения, некоторые — нет, а в некоторых об этом просто не подумали. То, что в некоторых случаях могут быть проблемы, еще не значит, что нет безпроблемных случаев. Если класс проектировался для расширения, то в нем ловушек быть не должно (например через private _Add), если не проектировался, то и методы не должны быть виртуальными.
Верно для C#, но не для java >= 8, Rust, scala, kotlin, etc
Вы про дефолтные реализации для методов интерфейса? С одной стороны, вы не обязаны от них зависеть — достаточно просто реализовать все члены интерфейса в своем классе. С другой, код дефолтных методов — часть публичного контракта интерфейса. Лично мне более симпатично использование с аналогичными целями статических методов, включая методы расширения: результат тот же, но более явный и без модификации публичного контракта.
Когда вдруг наследование стало архаизмом?
Не знаю. Наследование интерфейсов до сих пор мейнстрим.
Есть несколько вариантов как переиспользовать код, и наследование один из них. Просто использовать нужно с умом.
"С умом" — априори верно, вот только неконкретно.
Это надуманная проблема. Если вы взяли библиотеку и она решает ваши задачи, зачем ее обновлять?
- Мне сложно исправлять баги в самой библиотеке, в отличие от ее автора.
- Новая версия может решать больше моих задач с лучшим качеством.
Спрошу иначе. Когда наследование от обычных классов (имеющих реализацию) стало архаизмом?
Давным-давно. Уже "банда четырех" поминала наследование реализации как нежелательное.
Обновляю не просто потому что пофиксили баги. Плюс учитываю риски.
То есть библиотеки вы все-таки обновляете. И без наследования реализаций риски снижаются.
Потому что тут все 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 тесты бы вам показали проблему.
Какую проблему? Базовый класс успешно проходит все тесты, код наследника мы не меняли, а наши тесты указывают на ошибку именно в нем, если вообще срабатывают. Несколько комментаторов дали решения, которые развалятся только при многопоточке (точнее, реентерабельности).
Вариант с наследованием интерфейсов заодно даст и лучшую диагностику по тестам — ошибка сразу будет локализована до конкретного класса.
Все, что я вижу, это то, что наследование реализации МОЖЕТ сломать инкапсуляцию.
Вы думаете, что сломанная инкапсуляция — это видимый сбой? Но это неправда:
- Сломанная инкапсуляция — это ошибка, она сразу заложена в наследование реализаций.
- Хрупкий код наследника, не работающий при изменениях (или просто при недостатке информации) в деталях реализации предка — это дефект, возникший вследствие ошибки.
- И наконец, некорректный расчет — это сбой как следствие дефекта.
Если вы думаете, что редкость видимых сбоев является доводом в пользу использования наследования реализации, то это также неверно: самые опасные сбои в промышленной эксплуатации как раз редкие и плавающие.
Даже в статически типизированных языках есть dynamic cast. Так что к каждой инкапсуляции должен прилагаться способ её нарушить.
Frontend и Backend — это не языки, а предметные области. Области эти довольно большие требуют совершенно разных подходов, имеют совершенно различные требования и далеко не в каждом мозгу помещаются одновременно.
А разница… если про программирование, то разница не большая, те же компоненты, классы, те же паттерны.
… реактивное программирование, инкрементальный рендеринг — всё это ой как надо бэкенду :-)
Если говорить по хайлоад например,
Разные приоритеты. Для UI важна отзывчивость, для бэкенда — пропускная способность.
то это не совсем про программирование, это скорее про архитектуру.
Архитектуры-то разные. От слова совсем.
Если про дизайн и верстку, тут да, мне например это скучно делать, но сейчас стало проще, есть bootstrap и его друзья.
Толковый фронтендер его бы ни за что не стал использовать.
Я согласен, что разница есть, но она минимальна.
Боюсь даже представить, как бы вы реализовывали SPA :-)
Кстати рекактивным программированием пользуюсь и там и там.
Например?
Я не замечаю, тот же MVC или MVVM например. Теже слои.
Это вершина айсберга. Я, кстати, использую MV.
если бы у меня было больше опыта в верстке, возможно я бы тоже не стал пользоваться фреймворком.
В частности, поэтому вы — бэкендер, а я — фронтендер ;-)
Автор, попробуйте пожалуйста разобрать конкретный пример — как сделать редизайн какого-нибудь 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
Концептуально самый простой способ — замена наследования композицией:
- Выделяем публичный интерфейс Control в тип IControl
- Делаем в своем классе поле типа IControl
- Делегируем ему все члены IControl, которые не переопределяем сами.
- Передаем экземпляр Control как параметр конструктора.
Вот только надо ли вам в данном случае лопатить кучу легаси?
С другой стороны, это одно из лучших решений с учётом накладных расходов. Дело не только в уменьшении переписываемого кода, именно наследование позволяет делать как максимально гибкий, так и максимально быстрый в расширении код с наиболее понятными требованиями к программисту-пользователю.
Отказываться от наследования — всё равно что отказываться от промышленного оборудования из-за того, что
*Нет, это не призыв калечить людей.
Отказаться от наследования — это отказаться от лошади в пользу автомобиля :)
Отказ от ООП в пользу ФП — это переход от автомобиля в пользу летающих тарелок. Очень крутой ход, жаль только топлива для них нет. Вообще, выбор любых техник должен быть осмыслен и логичен. Их набор должен давать максимум выгоды при минимуме затрат. А не следовать моде, вроде перевода всего и вся на микросервисы с 25% потерей производительности.
Я тоже не ведусь на моду. :)
Собственно, поэтому выступаю против фреймворков в PHP.
ООП — это тоже мода.
И, ладно, когда поднимают какую-нибудь интересную багу реализации того или иного языка или освещают вопросы неправильного использования языка, как ребята из PVS-Studio, но ведь чаще всего появляются статьи «People are dying if they are killed», вроде текущей, которая просто приводит к очередному холивару.
з.ы. А ныть нынче не в моде? Было бы забавно оказаться в тренде.
В агитируете за прагматичный подход, а мне кажется, что даже в приведенной вами аналогии стабильность лучше.
Вы предлагаете наращивать квалификацию и дисциплину рабочего и это хорошо, но может быть все-таки в данном случае лучше закупить более дорогое, но безопасное оборудование? Ведь оба подхода решают проблему, но стабильность в долгосрочном периоде лучше.
> Вот с null-ом, кажется большинство здравомыслящих людей ошибку поняло и согласилось, что нужно исправлять
Вы про #define NULL 0? Или про шарповый null? Или про nul? Или про nullptr?
Конечно, каждый год появляется куча интересных техник и лучшие из них обязательно нужно внеедрять в язык и тд и тп, но не забывайте одну мелочь. Сегодня средний по размерам проект на хорошем компе с ssd и 32ГБ оперативы собирается с нуля не меньше получаса, занимая в процессе под полтишок гигабайт места различными кешами. 10-15 лет назад, когда критикуемые вами концепции только появлялись и формировались, на компе был гиг оперативы и 120Гб памяти. Всего. А то и этого не было.
Люди — ужасно прагматичные существа. Если они могли сэкономить на null'ах без особых проблем для кода — они это делали. Если они могут модернизировать свой станок так, чтобы он стал менее травмоопасным, не теряя особо в скорости и не обязывая программистов переписывать ВСЁ (что требуют новые языки), они сделают это.
Что выбрать, новый инструмент, который может стать топовым, а может и не стать, или не устаревающую классику, которая, порой, отправляет тебя на стол хирургам в коробке «Puzzle 4000pts»?.. Я предпочту при таком выборе стать ведьмаком.
Дело не только в уменьшении переписываемого кода, именно наследование позволяет делать как максимально гибкий, так и максимально быстрый в расширении код с наиболее понятными требованиями к программисту-пользователю.
Да-да, наследование реализации очень дешево и гибко благодаря сломанной инкапсуляции и максимально быстро в расширении благодаря отсутствующей поддержке множественного наследования в большинстве языков.
Отказываться от наследования — всё равно что отказываться от промышленного оборудования из-за того, что долбарабочий может при нарушении ТБ покалечить себя до смерти — высший уровень тупизны.
Вы явно претендуете на все эти титулы, так как не смогли даже прочитать статью, которую взялись комментировать.
Наследование интерфейсов в ней — рекомендуется, возможность наследования реализаций — допускается.
Ну а что рекомендации по технике безопасности серьезные — так наследование реализаций против наследования интерфейсов все равно что опасная бритва против станка.
Напомните, наследование — это одностороннее отношение «является» («потомок является предком»), да? Скажите, как отношение «является» ломает инкапсуляцию? Никак. Если Вася — человек, то он человек от головы до пят и в нём всё человеческое. Инкапсуляцию ломает не наследование, а приватные поля базового класса, недоступные из потомка, и сокрытые реализации методов. И ограниченность ЯП, которые не позволяют частично модифицировать методы. Последнее, видимо, к лучшему.
Быть может, запретим приватные поля и классы с сокрытой реализацией? Было бы неплохо.
>Наследование интерфейсов в ней — рекомендуется
Я написал «Отказываться от наследования интерфейсов...»? Нет, я написал «от наследования», более общее понятие, несущее в данном контексте ровно единственный смысл. Если для вас нужно писать «от наследования частичной или полной реализации», то уж точно не вам рассуждать о заслуженных мною титулах.
>возможность наследования реализаций — допускается.
Я ещё раз перечитал ваши выводы. Особенно тот, про запечатывание. Это вы называете «допускается»?
Как уже написали ранее, принцип подстановки Лисков — это очень полезное, но, тем не менее, не обязательное для объекта ограничение. Хотя бы по той банальной причине, что порой потомок создаётся именно для нарушения этого принципа.
И, да, те же плюсы позволяют множественное наследование реализаций, и там это даже работает, хоть и требует некоторого навыка. Ах, точно, плюсы же положено гнобить за то, что они плюсы. Всё-всё, больше о них ни слова.
Инкапсуляцию ломает не наследование, а приватные поля базового класса, недоступные из потомка, и сокрытые реализации методов.
В гранит!
1) Если проблема в сокрытии реализации методов и полей от наследников, то причина не в наследовании и инкапсуляции, а в проприетарности создаваемого кода.
Этот факт в вашей статье отмечен от слов «никак» и «нигде», а это фундаментальное заблуждение, меняющее всё остальное.
2) Если проблема в проприетарности, то лучшее её решение — опенсорс, то есть наследование от правильно построенных классов открытых библиотек безопасно с точки зрения озвученных вами проблем.
Исходный код открыт, есть история изменения версий в виде дампов изменений, позволяющие мониторить совместимость.
3) Интерфейсы НЕ решают проблемы, а лишь маскируют её, переводя на следующий уровень. Это всё равно, что решить проблему гнилой туши в доме покупкой ароматизаторов, бахил и повязок на глаза.
Проблема интерфейсов в том, что они просто накладывают более мягкие ограничения, нежели базовые классы, имеющие реализацию. Мы говорим, что нам не важно, как всё устроено внутри, пускай просто ведёт себя похоже. В итоге, в том месте, где совмещение функциональности базового класса и неправильного наследника сразу дали бы ошибку, неверные реализации интерфейсов пройдут без проверок, и ошибки придется искать уровнем выше. В интерфейсах нельзя спрятать состояние, в интерфейсах нельзя создать код для автоматической проверки объекта самим собой на корректность и непротиворечивость.
Главное преимущество интерфейсов над полноценными классами-предками — первых куда проще проектировать. Делает ли это их лучше? А делает ли человека лучше отсутствие рук?
Чем вас обычная замена наследования композицией не устраивает?
Интересно, откуда эта тройка именно в таком виде пошла. Это же какое-то отечественное изобретение (Архангельский?).
Наследование — это просто составление контракта между объектом-предком и объектом-наследником. Точно так же, как есть контракт с между этими объектами и кодом, эти объекты использующим.
Не касаясь теоритических проблем, скажу про практические:
Зависимость, создаваемая наследованием, чрезвычайно сильна
Да, на это диктуется самим требованием наследования. В противном случае не наследование нужно, но более слабая композия.
Наследники гиперчувствительны к любым изменениям предка
Как, внезапно, любой клиентский код. Это точно такое же изменение контракта. Нужно понимать, что меняя контракт всегда можно кого-то обидеть.
Наследование от чужого кода добавляет адскую боль при сопровождении
Сопровождение чужого кода, меняющего контракты, доставляет адскую боль и без наследования. Хотя, по хорошему, чужой код должен создавать точки расширения (если они вообще нужны) с помощью интерфейсов.
Как, внезапно, любой клиентский код. Это точно такое же изменение контракта. Нужно понимать, что меняя контракт всегда можно кого-то обидеть.
Чтобы "обидеть" наследника, может быть достаточно изменений в приватном контракте.
А все остальные смогут заметить только изменения в публичном контракте.
Сопровождение чужого кода, меняющего контракты, доставляет адскую боль и без наследования.
Не скажите. С публичными контрактами авторы библиотек обычно весьма щепетильны, а вот наследники могут пострадать через непреднамеренное ломающее изменение в приватных.
Возможно, вы пропустили или не поняли пять пунктов сразу после заголовка "наследование ломает инкапсуляцию"
Пожалуйста, посмотрите на простейший пример зависимости наследника от приватных соглашений в базовом классе. Никакой рефлексии.
https://habrahabr.ru/post/310314/#comment_9815832
Также есть абстрактные классы, которые содержат некоторую часть реализации и предлагают реализовать некоторые недостающие методы. Разработчик абстрактного класса предлагает некий «контракт»: я тебе даю вот это, верни мне вот это, я сам тебя вызову. Класс выступает неким фреймворком, мы играем по его правилам, а не заставляем его сделать что-то, для чего его не очень-то и задумывали :) В частности, поэтому в некоторых языках виртуальными являются не все методы, а только те, которые таковыми захотел сделать автор, и поэтому просто нельзя переопределять всё, что вздумается.
Классу-потомку доступны защищенные члены класса-предка.
private поля не доступны в большинстве языков.
Всем остальным доступен только публичный интерфейс класса.
Уровней доступа вообще говоря больше 2: private (не доступен никому вне класса/модуля), package (не доступен за пределами пакета), protected (доступен только потомкам), public (доступен в любом месте программы), export (доступен даже из вне программы).
Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку.
Этот принцип полиморфизма касается любых типов, а не только объектов.
Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода
Это касается любых случаев "обратного вызова". Виртуальные методы — весьма частный случай.
Зависимость, создаваемая наследованием, чрезвычайно сильна. Наследники гиперчувствительны к любым изменениям предка.
Что такое "сила зависимости"? Зависимость либо есть, либо её нет. Любой способ переиспользования кода создаёт зависимость.
разработчики библиотеки рискуют получить обструкцию из-за поломанной обратной совместимости при малейшем изменении базового класса, а прикладники — регрессию при любом обновлении используемых библиотек.
Не обобщайте, не при любом изменении, а при ломающем совместимость изменении. Классы тут опять же ни при чём.
А вообще, в статье ни единой строчки кода, ни единого описания решаемой проблемы. Только типичное "я где-то слышал, что наследование — это антипаттерн".
Что такое "сила зависимости"? Зависимость либо есть, либо её нет. Любой способ переиспользования кода создаёт зависимость.
Вы правда не в курсе?
- Наследование реализации создает зависимость от класса-предка целиком.
- Композиция создает зависимость от того же класса, но куда более слабую, так как включает в себя только его публичный интерфейс. Можно спокойно менять детали реализации или использовать наследника вместо базового класса.
- Композиция через выделенный интерфейс делает зависимость от класса совсем слабой — одну реализацию интерфейса можно безболезненно заменить другой.
Зависимость от одного и того же класса есть во всех трех случаях, но сила ее радикально различается.
… Самолёт терпит крушение над океаном, в живых остаются трое: пилот, помощник пилота и стюардесса, — им удаётся выбраться на необитаемый остров. Через месяц жизни втроём пилот сказал: «Долой разврат!» — и убил стюардессу. Ещё через месяц он опять сказал: «Долой разврат!» — и закопал стюардессу. Ещё через месяц он сказал: «Долой разврат!» — и откопал стюардессу...
А по теме… Сколько времени было затрачено на статью? Где примеры? Ну вот реально, подумайте, какая польза от статьи с сухими перечислениями? Те, кто это знают — пролистнут и забудут, те, кто не знает — прочтут и тут же забудут. Для кого/чего это пишется? Для кармы?
Почему ссылка на полиморфизм взята для PHP (который я не знаю)? От фонаря (та статья еще хуже чем эта, начинается с растянутого примера, в конце сухо чуток теории)? Зачем тег C# тогда?
P.S.: справедливости ради признаюсь — про паблик Морозова не знал, рассмешило слегка.
После прочтения статьи складывается такое ощущение, что наследование — это какой-то грязный хак, за использование которого должно быть стыдно. Единственный контекст, в рамках которого можно признаться в его использовании, — это ситуация в стиле: «Здравствуйте! Меня зовут Джон. В своих программах я часто использую наследование реализации, а иногда ещё и защищённые члены.» Если базовый поинт именно такой, то тогда всё правильно. Но проблема в том, что эта базовая предпосылка неверна. Наследование — это не какой-то костыльный трюк, а феномен, реально существующий в мире. Точнее, не в самом мире, а в нашем способе описания этого мира. Да, объекты реального мира не наследуют реализацию друг у друга, даже когда они во многом схожи. Но кого волнуют реальные объекты? Ведь люди мыслят абстракциями. А абстракции очень часто весьма неплохо выстаиваются в иерархию с наследованием общих свойств. Для абстракций такое естественно. Всё, что от программиста требуется, — это правильно реализовать в программе абстракции… с учётом того, что они представляют собой иерархию. Но это его работа, чёрт возьми. Предполагается, что специалист должен уметь выполнять свою работу правильно. А если он не умеет, то значит ему нужно учиться, а не искать другой подход к задаче, который не потребует умения правильно выполнять свою работу. Вряд ли такой подход существует. Скорее всего при другом подходе тоже будут сложности. Просто они будут другими.
А модели покупателя нет отчасти потому, что она не нужна. Как для того, чтобы предсказать дождь, не нужно просчитывать траекторию каждой отдельной капли воды, так и для предсказания спроса не нужно моделировать каждого покупателя в отдельности. Маркетологи не ставят перед собой задачу спрогнозировать, купит ли новый бульбулятор какой-то конкретный покупатель. Они прогнозируют общие параметры спроса. В частности, как много бульбуляторов люди захотят купить, и как динамика продаж будет меняться во времени. С учётом всех известных обстоятельств. Таких как предполагаемые действия конкурентов, общая ситуация в экономике и т.д. Но, естественно, они не учитывают обстоятельства, которые неизвестны и не могут быть просчитаны. Отсюда и ошибки в прогнозах. Какие-то риски всегда есть. И они тоже как-то учитываются.
1. Наследование реализации может нарушать инкапсуляцию, но не обязывает это делать. Более того, инкапсуляция в практическом ключе — это сокрытие реализации от пользователей класса (или его наследников), а не от самих наследников.
2. Принцип Лисков ни в коем случае не обязывает класс-потомок удовлетворять всем требованиям к классу-предку. Он вообще не обязывает, а только лишь рекомендует. SOLID — это что-то вроде христианских заповедей, которые вроде бы надо выполнять, но не так уж и обязательно. И не факт, что от выполнения будет лучше. И за реализацию этих принципов нужно достаточно дорого заплатить, что в реальной жизни часто совершенно не оправдано. И далеко не всегда реализация принципа Лисков требует знания о реально используемом наследнике. В большинстве случаев это получается само и бесплатно.
Итого, автор усмотрел в устоявшейся концепции ООП потенциальную проблему, которая иногда возникает, и на всякий случай предлагает всем навсегда запретить пользоваться удобным механизмом наследования реализации, выжечь этот механизм каленым железом из языка. Чем демонстрирует изрядную долю теоретико-фанатизма.
Практичные же люди, когда видят практическую проблему наследования реализации, берут и прибегают к зависимостям от абстрактных интерфейсов :)
Наследование реализации может нарушать инкапсуляцию, но не обязывает это делать. Более того, инкапсуляция в практическом ключе — это сокрытие реализации от пользователей класса (или его наследников), а не от самих наследников.
Инкапсуляция при наследовании реализации нарушается совсем не потому, что кто-то хочет это сделать, а объективно.
Возьмем задачу, которую проигнорировал один из предыдущих комментаторов:
- У вас в базовом классе есть виртуальные методы Add (добавляет элемент) и AddRange (добавляет пачку элементов)
- В наследнике вам необходимо подсчитать общее количество добавленных элементов.
Как вы реализуете эти методы в классе-наследнике?
Принцип Лисков ни в коем случае не обязывает класс-потомок удовлетворять всем требованиям к классу-предку.
Да неужели? Нарушающий принцип Лисков сразу:
- Множит на ноль повторное использование клиентского кода (ибо то, что работало с предком, благодаря "необязательности" теперь легко может перестать работать с потомком).
- Подкладывает колоссальную свинью всем тем, кто полагает автора класса-наследника добросовестным разработчиком, и пытается использовать его детище обычным образом, например, доверяя безопасности преобразования ссылок вверх по иерархии наследования.
В большинстве случаев это получается само и бесплатно.
Разве что в ваших предположениях.
на всякий случай предлагает всем навсегда запретить пользоваться удобным механизмом наследования реализации
Попробуйте читать то что написано. В моих рекомендациях запрета нет, ни явного, ни неявного.
Практичные же люди, когда видят практическую проблему наследования реализации, берут и прибегают к зависимостям от абстрактных интерфейсов
Очень практично:
- Получить пачку сбоев в промышленной эксплуатации
- Понять, что дело в плохо спроектированной иерархии типов.
- Освоить бюджет на рефакторинг.
"Абстрактные интерфейсы" повеселили отдельно.
Собственно, откуда вы вообще взяли, что от класса-наследника надо скрывать детали реализации или лишать его доступа к данным?
Коллега, а где в приведенном вами же примере нарушение инкапсуляции — сокрытия данных и реализации от пользователя объекта?
А вы приведите свою реализацию наследника — покажу.
Собственно, откуда вы вообще взяли, что от класса-наследника надо скрывать детали реализации или лишать его доступа к данным?
Надо полагать, из названия и назначения уровня доступа private.
Из существование модификатора доступа private следует возможность скрывать члены базового класса от наследников. Но никак не следует необходимость это делать.
Корректная реализация перекрытых методов требует знания деталей реализации базового класса и часто доступа к членам базового класса (для чего и существует модификатор protected). Это нормально, потому что не раскрывает детали реализации и данные вовне иерархии наследования.
Хрупким здесь является не базовый класс, а вся иерархия наследования реализаций целиком.
При написании наследников надо изучать базовый класс целиком, а не только интерфейсы.
При изменениях базовых классов всегда есть риск сломать наследников, причем не только своих собственных.
Теперь вам понятны рекомендуемые ограничения для наследования реализаций?
Случай, когда тех. задание определено и не меняется в жизненном цикле программного продукта — это скорее исключение. Так что обычно получаем 30-ти этажные дома, стоящие на домкратах, с подпорками со всех сторон.
Итак, вы только что сами признали поломку инкапсуляции при наследовании реализаций.
Это означает, что любое изменение базового класса надо проверять на возможный аффект в наследниках, что качественно хуже ситуации при наследовании интерфейсов.
Из существование модификатора доступа private следует возможность скрывать члены базового класса от наследников. Но никак не следует необходимость это делать.
У вас прямо противоположная проблема — при наследовании реализаций вы вынуждены опираться на детали реализации базового класса, будь они хоть трижды приватными. Изменение этих деталей будет ломающим для вашего наследника.
Теперь немного изменим условия задачи. Базовый класс закрыт для наследования, но использует наследование интерфейса.
- Как вы реализуете те же методы в таких условиях?
- Будут ли они зависеть от деталей реализации базового класса?
- От какой опасности вас защитил разработчик, запечатавший базовый класс?
Ваша проблема в том, что вы, когда рассуждаете, фантазируете о подключаемом в виде бинарных зависимостей расширяемом фреймворке, которым пользуются миллионы.
В 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())
}
Но в статье не рекомендуется (в довольно жесткой форме) вызывать виртуальные члены в предке
Там выше был намек что addRange может вызывать add.
Зря код методов Storage
не добавлен. Что если в addRange
внутри себя пользуется add
? Ведь это вполне может быть. Это обязательное знание при реализации StorageWithCounter
, к сожалению.
Этот способ жестко ломается на многопоточке. Собственно, сама необходимость прибегать к подобным трюкам наглядно иллюстрирует особенности наследования реализаций.
Мне очень интересно, как вы при ровно одинаковых ограничениях на интерфейс?
Мне интересно, как сделать точно такое же решение с такими же ограничениями без наследования? Сколько, например, будет занимать код, эквивалентный написанному мной?
Для дотнета можно использовать какой-нибудь динамический прокси:
https://github.com/kswoll/sexy-proxy
В D тоже есть автоделегирование: http://ideone.com/B5rgw7
Отлично — здесь даже кода по сравнению с наследованием больше ровно на одно поле.
Это устранение хрупкости за счет потери гибкости.
Решение с наследованием интерфейса и декоратором такого недостатка не имеет.
Вызов или невызов Add из AddRange (и наоборот) является "побочным эффектом" только из-за поломки инкапсуляции при наследовании реализации.
При использовании только публичного интерфейса никаким "побочным эффектом" даже не пахнет.
Как вы реализуете эти методы в классе-наследнике?
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 поколениями, не более.
Вообще-то повторное использование кода при наследовании имеет два варианта использования:
- Повторное использования кода базового класса. Обычно ограничено одним классом при наследовании реализации для большинства языков.
- Повторное использование клиентского кода со всеми наследниками. Возможно только при соблюдении принципа подстановки Лисков, что при наследовании реализации ломает инкапсуляцию и делает иерархию хрупкой.
Почитайте комментарии, начиная со ссылки:
https://habrahabr.ru/post/310314/#comment_9815832
Продолжая тем, что выдуманный вами пример не корректен в силу того, что наследование работает не так, как написали вы, «сделай мне слона из мухи», а из конкретной реализации, о которой класс-наследник обычно имеет значительный объём информации. Когда я наследуюсь от хеш-таблицы, я знаю, как ведут себя ВСЕ публичные методы. Что не скажешь об интерфейсах iContainer.
И заканчивая выдуманными проблемами на производстве, которые обязательно проявятся только из-за наследования.
Вот вам задачка. Нужно сделать хеш-таблицу, в которую нельзя было бы вставлять элемент с одним и тем же ключём дважды за всё время жизни таблицы. Мне нужно переопределить только методы вставки и удаления, примерно 15-20 строк всего. (Плюсы портят статистику из-за operator[], но не значительно) Вам?
Так мы приходим к Контрактам. Для. 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
Или тогда уже юнит тесты.
А чтобы быть уверенным, что контракт соблюдается, и в случае метода интерфейса и в случае абстрактного метода придется изучать код. Разве что в случае абстрактного метода достаточно изучить только код класса, где он (метод) расположен (если абстракция хорошо спроектирована).
Логика разъяснена в статье, учебный пример — смотрите по ссылке ниже
https://habrahabr.ru/post/310314/#comment_9815832
В требовании знать, вызываются ли в предке методы Add и AddRange друг из друга.
Видимо совсем тупенький — всё равно не понимаю, зачем это надо знать
Затем, что без этого знания вы рискуете учесть некоторые добавленные элементы дважды.
посчитать количество элементов перед вызовом предка
И огрести от многопоточности.
В любом случае, если мы наследуем какие-либо публичные/защищённые методы, мы ведь должны понимать, как они работают.
Это и есть поломка инкапсуляции. Используя объект я должен знать что он делает, но не должен как.
Если так, то тогда зачем модификатор private
? Хватило бы protected
тогда.
Ну вот вам и инкапсуляция внутри классов, а вернее в предке, о поломке которой и идет речь.
Предлагаю мыслить все же шире, во многих языках программирования вовсе нет приватного наследования, так что это как раз-таки очень частый случай. Возможно как раз это помешало вам правильно понять автора сразу.
Но и все-таки, даже останавливаясь на плюсах, если бы это был такой уж редкий кейс, не было бы private
, хватило бы ptotected
и код ревью.
Есть определённая жёстко заданная логика — её реализуют публичные классы:
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).Логика (открыть-взять-закрыть) прописана в базовом классе, хотя детали реализации необходимо реализовать в дочерних. Если нельзя использовать базовый класс для описания логики — то нахрена об вообще нужен?
Если нельзя использовать базовый класс для описания логики — то нахрена об вообще нужен?
Для описания контракта.
обычно такие задачи решают так
http://ideone.com/fxPGq5
Многопоточности не бывает?
http://ideone.com/VUG3Tg
зыЖ написано «отбалды» на яве не пишу.
удивительно что такие простые ситуации вызывают сложности.
Ну вообще-то — это обычный костыль… Увидев в юнит тестах косяк у ошибочной реализации, я бы предпочел перепроектировать абстракцию, чем городить этого плохочитаемого костыльного уродца.
Причем после первого непотокобезопасного варианта — это должно было ну просто сразу всплыть и остановить, чем продолжать издеваться над будующими разработчиками этого кода)
… но ведь это преподносится как пример несостоятельности ООП, тобишь неспособности решить эту задачу наследованием в принципе, хотя это не так.
По задаче нельзя менять предка, очевидно что если можно то надо.
Особенно если предок из чужой библиотеки.
… но ведь это преподносится как пример несостоятельности ООП, тобишь неспособности решить эту задачу наследованием в принципе, хотя это не так.
Вы точно читали статью? В одной процитированной фразе три ложных утверждения.
- В статье показывается непротиворечивость ООП — наследование интерфейсов не ломает инкапсуляцию.
- Задача легко решается наследованием интерфейса,
- Решение наследованием реализации требует знания приватных деталей реализации предка (сломана инкапсуляция).
Вы где-то нас обманываете.
msts2017 пишет:
По задаче нельзя менять предка
Здесь вы соглашаетесь
Особенно если предок из чужой библиотеки.
А следом, внезапно...
Задача легко решается наследованием интерфейса,
Как, черт возьми, это слелать, если нет доступа к коду асбтракции и ее слоя?
А если доступ все-таки есть, то есть и другой вариант, который упоминал я.
Как, черт возьми, это слелать, если нет доступа к коду асбтракции и ее слоя?
А что тут может быть сложного? https://play.rust-lang.org/?gist=27418cdffe1bbb45bde4c7d3058ec162&version=stable&backtrace=0
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 полей, и должен учитывать, что перегруженные реализации виртуальных методов могут не вызывать базовые.
Это практически ничем не отличается от композиции. Базовый класс определяет и использует интерфейс, реализуемый дочерними классами. Единственное различие — возможность предоставления реализации по умолчанию, имеющей доступ к членам базового класса.
> Далее нигде не используется тот факт, что 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
. Даже юнит тест никак не исправит эту ситуацию. Помочь может только система типов, что в данном конкретном случае слишком мало, и контракты кода, чего во многих языках просто нет, пока.
Ничем, кроме уровня доступа ко внутренностям, они не отличаются.
А как же отсутствие проблем с хрупкостью базового класса?
Хрупкость базового класса, вообще-то, это — не рантаймовая проблема, а архитектурная. Небольшие и выглядящие неопасными изменения в базовом классе могут сломать поведение дочерних классов, потому что наследование создаёт множество неявных зависимостей между базовым и дочерними классами (допустимый порядок вызовов методов, ожидаемое состояние базового класса при вызове метода и т.п.).
Ладно. Пожалуй хватит, уже по пятому кругу пошли.
http://ideone.com/lh56Ds
Суть идеи ясна и так, посмотрите второй потокобезопасный вариант, там немного нагляднее.
Но это не важно в потокобезопасном варианте count не используется.
В условиях никакого count нет ни явно ни неявно.
Возможно, например, что добавляются не все элементы, а только те, которые удовлетворяют некоторому сложному критерию, который возможно зависит от текущего состояния. Причем алгоритм этого выбора реализован в приватных методах базового класса.
Этого в условиях задачи нет. Если бы было — методы возвращали бы результат вставки. Или вопрос был бы про отправленные на добавление элементы. В общем — заморачиваться на случай неуспеха вставки не надо. Стандартное соглашение по умолчанию — любой метод делает все или ничего (во втором случае бросает исключение).
Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.
При использовании наследования реализации. При наследовании интерфейсов — возможно и несложно.
Как быть если он этого не сделал? Объявлять интерфейс самим и писать наследник-декоратор его реализующий?
Да, причем в своем коде вместо чужого класса использовать этот интерфейс.
Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.
Вообще-то возможно. Пример, где-то тут уже проскакивал: запоминаем счётчик, вызываем метод, устанавливаем счётчику правильное значение. Тут возможно лишь незначительное пенальти по производительности в зависимости от реализации родителя, но логика не сломается.
Многопоточность уже отменили?
Сейчас в моде коммуникация через каналы, без совместного доступа к общей памяти.
Этого в условиях задачи нет.
А shared memory — есть?
А возможность многопоточного использования предполагается по умолчанию.
Как минимум в форме "мое решение (не) работает при многопоточном доступе".
Ваше, впрочем, и с одним потоком может сломаться. Например, если реализация предка использует файберы.
Ибо сломанную инкапсуляцию трюками не заткнешь — проще и надежнее просто не оставлять лишних дыр.
А возможность многопоточного использования предполагается по умолчанию.
В тех языках, что использую я (JS, D) — не предполагается.
Ваше, впрочем, и с одним потоком может сломаться. Например, если реализация предка использует файберы.
Тут вы правы.
Да не, с чего это, в Java\C# не предполагается.
А в каких языках объекты сходу потокобезопасны? (я просто не в курсе)
В D, например, пишешь shared class и для объекта автоматически создаётся мьютекс и все публичные методы его захватывают. А если не напишешь — не получишь доступа из другого потока без плясок с бубном.
А смысл так делать? Куда лучше, когда по умолчанию к объектам нет доступа из другого потока. Хочешь передать данные — посылай сообщения.
Наследование реализаций: закопайте стюардессу