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

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

"Наследование — карта, которую можно разыграть только один раз."
(с) Та же книга

Кстати, не холивара ради. Если бы C++/Java/С# не пришли бы на смену Lisp, Haskell, Erlang, C? Было бы лучше сейчас или хуже? Не кажется ли вам, что Divide And Conquer, которое закладывали в ООП, не сработало? И что всё можно написать на JavaScript (будь он компилируем, как С, шаблоны и со статической типизацией)?

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

Код свой покажи, петух с умениями?

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

Преимущества ООП я вижу, прежде всего, при моделировании сущностей и связей между ними (с функциональными вкраплениями, прежде всего filter, map и reduce). Когда речь заходит о моделировании процессов, то ООП хорошо если не мешает.

Можно пример?


Мне на ум приходит data-oriented design ("что быстрее обработать: структуру массивов или массив структур?"), но, как мне кажется, это проблема скорее конкретной технической реализации ООП, а не парадигмы в целом. Кроме того, это может быть и проблемой дизайна: можно выделить в классы блоки сгруппированных данных, а не сущности, ими описываемые, например.
(Я веду речь вот об этом: http://www.dice.se/wp-content/uploads/2014/12/Introduction_to_Data-Oriented_Design.pdf)

Например, бизнес-процесс покупки картошки в магазине. С сущностями, участвующими в процессе, понятно: магазин, покупатель, продавец, картошка, деньги, касса (навскидку). Сам процесс покупки как представить в ООП-парадигме, чтобы это было естественно и понятно, без введения дополнительных сущностей типа PurchaseProcess, PurchaseManager и т. п.?

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


Еще один пример в голову пришел, тоже из геймдева: архитектура Entity-Component-System, которая, якобы, отвергает ООП. Но, в сущности, это просто другой взгляд на систему, с учетом требований, отличающихся от тех, к которым мы привыкли (реконфигурируемые "на лету" сущности и т.п.), и, по сути, тоже вполне в рамках ООП. Это я и имел в виду, когда говорил, что в ООП все же навыки проектировщика играют ключевую роль (это, впрочем, верно и для остальных парадигм).

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

Да что там не удобного-то?

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

Подробнее про это можно почитать в главе 20 книги Роберта Мартина «Принципы, паттерны и методики гибкой разработки на языке C#» (или без языка C# в более ранней версии).

Это вы про что конкретно?

Про вашу картошку, вестимо :)

У картошки нет поведения — это объект-значение :)

Копирование сущностей реального мира и попытка приклеить

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

Без PurchaseManager непонятно куда девать методы, описывающие взаимодействие нескольких классов. В общем, это методы не классов объектов, а самой предметной области в целом. Беда в том, что в Java каждый метод должен принадлежать какому-нибудь классу, и слишком часто надо решать какому.
НЛО прилетело и опубликовало эту надпись здесь

Речь не про автоматы.

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

А они не дискретны и бесконечны по своей сути. На практике дискретны, конечно, потому что цифровые компьютеры дискретны и число комбинаций состояний памяти конечно, но приходится сводить бесконечные аналоговые величины типа времени к дискретным конечным приближениям. Но делать автомат с числом состояний типа 2^64 как-то не хочется.

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

Функтокид бомбанул, найс.
Десятки лет объектно-ориентированных притеснений дают о себе знать :) Бедненький.
покормил

Если бы C++/Java/С# не пришли бы на смену Lisp, Haskell, Erlang, C? Было бы лучше сейчас или хуже?

Haskell и Erlang не были настолько распространены, чтобы что-либо пришло им на смену.
На смену редко приходит что-то такое, что от него хуже — плохим мало кто будет пользоваться.


Не кажется ли вам, что Divide And Conquer, которое закладывали в ООП, не сработало?

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


И что всё можно написать на JavaScript (будь он компилируем, как С, шаблоны и со статической типизацией)?

JavaScript с шаблонами и статической типизацией — это не JavaScript. Технически реализовать компиляцию чего-либо в бинарник — несложно.

Если бы C++/Java/С# не пришли бы на смену Lisp, Haskell, Erlang, C? Было бы лучше сейчас или хуже?

Они и не пришли им на смены, а создали новые ниши. Lisp, Haskell, Erlang и C живут в своих.

Всё-таки C++ заменил C во многих нишах, а потом Java/C# заменили его частично.

Это typescript :-) у меня есть мысль компилировать его как раз в бинарник..

Если насчёт Lisp, то CLOS не имеет «методов» вообще, там вместо этого хитрая система множественного диспетчинга во время исполнения (классическое ООП уже для двойного диспетчинга вынуждено использовать шаблон проектирования «визитёр», а уж с множественным — это вообще ой-ой-ой, я сам такое писал, потом баги вылавливал два года).
слишком легко поддаться соблазну и бездумно следовать лозунгу, не понимая, что за ним скрывается

Золотые слова.

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

Тоже золотые слова.
А многие ленуются думать своей головой.

Дальше не по теме:
Покуда не появился графический интерфейс2, которому, как выяснилось, очень-очень не хватало ООП.

То есть для программирования сайта на PHP в общем-то ООП не особо-то и нужно? :)

И вот тут ООП взлетел.

Может еще и компы стали мощнее и ООП стало не таким дорогим? :)

Поиск в гугле по фразе «объектно-ориентированное программирование» дает 8 млн результатов.

Ну это такое.
Покажет-то он меньше. :)
И на последних страницах будет шлак :)
То есть для программирования сайта на PHP в общем-то ООП не особо-то и нужно? :)

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

Этот интерфейс написан на PHP программистом, писавшим сайт? :)

Заметная его часть — html минимум :)

Вы хоть верите (понимаете) в то, что пишете, или просто аццки гоните? :)

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

То есть для программирования сайта на PHP в общем-то ООП не особо-то и нужно? :)

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


Покуда не появился графический интерфейс2, которому, как выяснилось, очень-очень не хватало ООП
Может еще и компы стали мощнее и ООП стало не таким дорогим? :)

Вы про какое время? ооп-ная графика в яве была когда php ещё назывался шаблонизатором персональных страниц.

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

Копипаста была еще когда не то что программирование, письменность не изобрели.
Причем буфер обмена продвинутый имелся, с управлением множественными копиями.
Copy text from external sound stream to brain clipboard.
Paste text to output sound stream.
И текст программы в машинных кодах копипастили друг у друга через листок бумаги.в

Знаете я вообщето-то работал в те годы

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

НЛО прилетело и опубликовало эту надпись здесь
ООП в базовом виде состоит всего лишь из двух постулатов:
  1. всё есть объект
  2. объекты взаимодействуют через посылку сообщений

Всё прочее, включая классы, не более чем приятное дополнение. Увы, C++ слишком исказил восприятие многих людей.

Существует две главные парадигмы в ООП: основанная на наследовании (class based) и прототипах (prototype based). В первой к общей идее добавили третий пункт:
  1. всё есть объект
  2. объекты взаимодействуют через посылку сообщений
  3. объекты являются экземплярами классов

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

Хотел ответить, промахнулся, написал чуть ниже...

Спасибо, нашел очередное подтверждение того, о чем сам всё время думаю (ну не всю жизнь, а только сейчас, пока вникаю в азы проектирования, почитывая Макконнелла (гл. 6 стр. 140-145))

Я, наверное, испорчен Википедией, но после слова "считается" я автоматически вижу вопрос:"кем?"


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


Я, к примеру, очарован хипповым раздолбайством js, в частности, полифиллы приводят меня в экстаз, но… Со стороны, только со стороны. И мне строгое наследование кажется гораздо более чистым. Потому, что чистым можно быть от самых разных вещей :)

кем?

In short: авторами ООП концепции.

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

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

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


И с этой точки зрения мне стало вдруг страшно интересно: есть ли прецеденты удачного сочетания прототипного наследования (без классов) со строгой типизацией? Звучит (для меня) как нонсенс, но вдруг есть?

Спасибо за качественный перевод. На хабре это, увы, редкость.

Спасибо. Значит мой замысел удался.
Немного не по теме, но зацепило одно замечание — сразу вспомнился GTK, который на не-ООП языке попытался создать ООП просто потому, что ООП якобы необходим для UI.

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

Я в своё время сталкивался с совершенно мозговыносящей (для человека, знакомого преимущественно с Win32 и Qt) архитектурой UI в UnityEngine, где элементы интерфейса выражены одной функцией, принимающей аргументы (например, текст на кнопке) и возвращающей какое-либо значение пользователю (true, если кнопка была нажата в результате события), а за хранение данных отвечает функция, вызывающая кнопку — таким образом, всё окно, например, при желании может находиться на стеке).

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

Однако система есть и пользуется определённой популярностью.
Можно ли говорить о том, что в JavaScript наследование реализовано в виде композиции (у «наследника» просто есть ссылка на «предка»(prototype))? Я правильно понял?

Не совсем. Разница в том, где предок хранит данные. В яваскрипте все данные хранятся в одном и том же обыекте. А вот в каком-нибудь c++ приватные данные каждого класса хранятся в отдельных областях памяти. И хоть там нет "ссылки на родителя", но это самая натуральная композиция.

В яваскрипте все данные хранятся в одном и том же обыекте.

Што?! В JS данные, к которым можно получить доступ по this.property размазаны по цепочке прототипов.

Дефолтные значения хранятся в в прототипах, да. Актуальные значения пишутся в объект на конце цепочки.

Не дефолтные, а определенные не в самом объекте.

Дефолтные для данного объекта.

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

А в каком вы его понимаете?

Как значение, скажем, для числового свойства дефолтным значением может быть 5.

Которое, если не установлено, будет взято из прототипа.

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

Что выглядит как утка и крякает как утка — то и называют уткой.

Именно. если я пишу console.log(this.a) и вижу 5, то для меня 5 обычное значение свойства a объекта, а не какое-то дефолтное. Что оно не собственное, а одного из объектов цепочки прототипов — нюанс.

Если вы явно не устанавливали значение, а оно есть — это значение по умолчанию. Вот зачем вы с определениями спорите?

Я как раз его явно устанавливаю:


const parent = {a: 5};
const child = Object.create(parent);
console.log(child.a);

В объект child вы его не устанавливаете, для него это значение дефолтное.

А если сделаю


const parent = {a: 5};
const child = Object.create(parent);
console.log(child.a);
parent.a = 10;
console.log(child.a);

то, что, второе дефолтное значение создаю, два дефолтных значения?
Спецификация JS явно говорит, что a в таком случае — свойство объекта, унаследованное, но свойство этого объекта: inherited property — property of an object that is not an own property but is a property (either own or inherited) of the object’s prototype/

… то вы поменяете дефолтное значение. Вас смущает, что это не константа? Ну так это и не статический язык. Сравните с:


const parent = {a: 5};
const child = Object.create(parent);
child.a = 666;
console.log(child.a);
parent.a = 10;
console.log(child.a);

"С помощью", а не "в виде". Кроме собственно композиции есть ещё механизм доступа с данными по цепочке прототипов.

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


  • кнопка, нажимаемая один раз: нарушает контракт обычной кнопки (только первое нажатие генерирует ожидаемое событие), а не "дополняет" его — тесты обычной кнопки не пройдут для одноразовой


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

UI ничем принципиально не отличается он не-UI. Компоненты точно также наследуются друг от друга.

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

Наследование, композиция — ха! Сейчас все помешаны на DI и на DI контейнерах. Через DI реализуют и наследование, и композицию, и перегрузку, и виртуальные методы, и даже доступ к членам класса осуществляют через DI.

А не имеет ли смысл рассматривать наследование в первую очередь как subtyping? То есть в переменную типа A мы при этом можем поместить значение любого типа-наследника. Имея subtyping и инкапсуляцию, нам неизбежно придётся «использовать по умолчанию поля и методы своего предка». А поскольку преемственность полей и методов следует из subtyping и инкапсуляции, то использовать эту преемственность для определения наследования немного странно. Таким образом получаем, что наследование это по определению subtyping, а преемственность методов при наследовании — механизм обусловленный наличием subtyping и инкапсуляции в языке.

Применяя это рассуждение к теме статьи можно сформулировать следующие рекомендации по вопросу composition vs inheritance:

— если инстансы класса B необходимо хранить в переменных типа A, то B должен быть наследником (подтипом), прямым или опосредованным, класса A;
— если в первом нет необходимости, имеет смысл проектировать код используя композицию.

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

имеет смысл

Неравноценные инструкции. Вторая имеет нюансы. А в целом "необходимо хранить в переменных типа A" — редкое требование

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

Допустим, если бы можно было сделать так:

class Square variant of Rectangle
{
    public __match()
    {
        return ($this->width === $this->height);
    }
}

function someActionWithSquare(Square $s)
{
    ...
}

$r1 = new Rectangle(10, 20);
$r2 = new Rectangle(10, 10);

someActionWithSquare($r1);
// throw new Exception('Rectangle does not match Square')

someActionWithSquare($r2);
// success call
function someActionWithRectangle(Rectangle $r)
{
    $r->width = 2 * $r->height
}

someActionWithRectangle(new Square(10))

Что должно произойти?

Здесь ничего. Это императивный язык, если мы сделали возможность менять состояние объекта снаружи, значит согласны, что какое-то время он будет в неконсистентном состоянии. Переменная просто не будет Square после вызова, хотя будет Rectangle, и в следующем действии со Square будет ошибка. Можно сделать возможность проверять вручную в контрольных точках через кастинг.

function someActionWithRectangle(Rectangle $r)
{
    $r->width = 2 * $r->height
}

$s = new Square(10);
someActionWithRectangle($s);
someActionWithSquare($s);  // exception

(Square)$s; // exception

Объект, который в любой момент может поменять свой тип — так себе концепция.

Вас же не смущает, когда вы передаете Child extends Parent в функцию принимающую Parent, и там переменная считается типом Parent.
Технически переменная все еще будет иметь тип Square, просто при проверках __match() будет возвращать false, и специфичных для Square действий с ней нельзя будет сделать. То есть, до изменений $s instanceof Square == true и $s instanceof Rectanlge == true, а после $s instanceof Rectanlge == true а $s instanceof Square == false.

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

Другой пример, более практический:

class Order
{
    private $productList;
    private $deliveryAddress;
}

class OrderForCheckout variant of Order
{
    public function __match()
    {
        return (count($this->productList) > 0 && !empty($this->deliveryAddress));
    }
}

function checkout(OrderForCheckout $order)
{
    ...
}

$order = Order::findOne($id);
checkout($order);
Вас же не смущает, когда вы передаете Child extends Parent в функцию принимающую Parent, и там переменная считается типом Parent.

Не смущает, потому что при таком определении Child является Parent (если, конечно, корректно задействован принцип подстановки Лисков, а не построена иерархия, где например треугольник наследуется от линии).

Ну а Square является Rectangle. Любой Square это Rectangle, но не любой Rectangle это Square.

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

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


С точки зрения геометрии да, Square является Rectangle. В ООП это не обязательно так. Например, у прямоугольника при изменении Width не должно меняться значение Height. У квадрата же Height тоже изменится, что нарушает LSP. Поэтому такое наследование недопустимо. Допустимо оно только тогда, когда Width/Height неизменяемы.

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


У квадрата при изменении Height необязательно должна меняться Width, он просто перестанет быть квадратом.


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

Наследник естественным образом является производным типом.


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

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

> ArrayList это уже потомок ArrayList

на этом читать закончил. Нет, не потомок. К переводчику претензии вряд ли есть, а вот автор рассуждает о том, чего не знает достаточно хорошо.
А вот именно к переводчику-то и надо предъявлять. У меня там явно ошибка перевода. Исправил эту фразу.

Да, стало лучше, но по-моему на себя вы это зря.


Все же у автора есть подозрение на непонимание одной (ради объективности — довольно-таки сложной) вещи — List это не наследник List

И вот так:
ArrayList is a subclass of list already, a utility collection — an implementation class.

просто писать не стоило бы. Правда, он не написал List, а просто "наследник списка". Я не хочу сказать, что тут все неправильно, но смысл слегка туманный.

Уф. Всю разметку слопал проклятый долгоносик...


В общем, речь была о том, что параметризованный List из String это не наследник List из Object (по крайней мере в Java). У параметризованных generic типов вообще все сложнее, чему впрочем хороших объяснений в сети навалом (скажем, вот: https://briangordon.github.io/2014/09/covariance-and-contravariance.html). А особенно когда wildcards имеют место.

Про зловредность использования наследования для пересечения иерархии предметной области и иерархии реализации (а в более широком смысле — пересечения разных иерархий) в частности говорит GoF в паттерне Bridge, он же иногда называется pimpl.
Как по мне наследование — это ЯВЛЯЕТСЯ, собака является животным значит наследуемся. Собака СОДЕРЖИТ блох значит применяем композицию, конечно примитивно, но логика в этом очевидна

На практике лучше "собака является животным — можем наследовать", особенно если одновременно есть и отношение "собака является другом человека" :)

Собака с блохами все еще является собакой? :)
Тут как бэ получилась новая «собака с блохами» — наследница просто собаки, которая композирована с блохами )))

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

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


Думаю, тут лучше перевести как: «приводит к сильному связыванию классов».
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.