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

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

Хорошо написано и однозначно в закладки :)

П.С. И к сожалению это действительно очень многие не знают или не до конца понимают.
блестящая статья.
>Это было названо «процедурным программированием».
То о чем вы говорите строкой выше — скорее структурным.
Получилось хоть и объемно, но довольно сжато, глядя на количество рассмотренных принципов. А поэтому мощно. Спасибо!
Вот уж действительно, всё что вы скажете, обязательно будет использовано, рано или поздно :)
Возвращаясь к вопросу о том, стоит ли использовать SOLID и всё остальное. Да, чёрт возьми.
зачем, если «оно тебе не понадобится»?
я не утверждаю, что не нужно, просто налицо конфликт, нужна какая-то более другая рекомендация
Как будто это денег стоит SOLID и всё остальное использовать :)

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

И с остальными вещами примерно так же. Привык и уже просто делаешь.
так где проходит граница между «Привык и уже просто делаешь.» и «YAGNI — Тебе это не понадобится»?
Если честно, то не совсем понимаю вопрос.
между «не надо это делать» и «привык и делаю» есть очевидный конфликт, можно выполнить только что-то одно. как определить, что лучше?
Это происходит в пункте:
Мне кажется что если ты это один раз хорошо понял и один раз научился это использовать


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

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


Моё "правило буравчика" — в реализации, где-нибудь внутри можно и навернуть абстракций, при условии, что в целом написание наворотов займёт, ну, ~10% от времени написания кода (если где-нибудь есть Future-задача сделать именно расширение, можно поднять планку до 15-20%). Но это в реализации. В интерфейсах всегда стараюсь следовать YAGNI, по умолчанию публикуется только абсолютный минимум. Что-то эдакое опубликовать потом™ всегда успею. Но на любую опубликованную деталь следует смотреть будто прямо в момент коммита какой-нибудь важный модуль начнёт от неё зависеть (что не всегда правда, мягко говоря), и значительно поменять её уже не выйдет. Потому лучше положить всё, что вот прямо сейчас не нужно, под подушку, вдруг во сне придумаю, как сделать лучшее.

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

Ну у меня же стоит «и всё остальное» :)

И даже в вашнм примере вы же наверняка не сидите часами раздумывая что важнее в случае каждого конкретного проперти, класса, интерфейса, ассембли и так далее.
Вы же тоже нааерняка для себя уже выбрали какой-то свой «глобальный паттерн программирования», в котором на стандартными вещами вы уже особо и не думаете. И наверняка это паттерн учитывает и SOLID и YAGNI. И бпланс между ними вы для себя уже более-менее нашли. И под сомнение этот паттерн ставится только если появляются совсем новые задачи или вдруг всплывают проблемы со старыми. Ну или после каких-нибудь конференций/курсов/ликбезов :)

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


"Глобальный паттерн программирования" есть, но обычно он называется "архитектура и соглашения конкретного приложения". Даже когда я сам в роли архитектора и/или техлида, то стараюсь анализировать требования ко всему приложению и отведенные на их реализацию ресурсы глобально и зафиксировать формально такие "паттерны" по умолчанию для всей команды (с учётом квалификации и опыта команды). И бывает, например, что выбираю какую-то реализацию ActiveRecord как основу работы с персистентными данными, несмотря на явное нарушение этим паттерном SRP. Просто заношу это в потенциальный техдолг хотя бы в голове.

А мне кажется, они скорее дополняют друг друга. SOLID — пиши с учётом тех изменений, которые скорее всего будут. YAGNI — игнорируй изменения, которых скорее всего не будет. Затык тут в «скорее всего», потому что будущее мы предсказывать не умеем.
Видел я как-то код, который подстраивается под все возможные изменения, которые только могут случиться. Поверьте, человеку, который это писал сильно повезло, что я не психопат, знающий, где он живёт.

Возможно. Только я бы переформулировал как "не надо грубо нарушать SOLID если это сильно не упростит решение задачи".


А так да, видя фабрики фабрик фабрик IoC контейнеров, понимаешь, что их автору сильно повезло, что я не знаю его автора :)

Было бы лучше если бы вы убрали код под спойлеры и немного сократили статью.
Когда так много информации то она перестает усваиваться.

Честно говоря, от NEPL из глаз выливалась кровь )
Видимо «сделать так, чтобы код читался, как предложение на английском языке» было ну такой себе идеей. К сожалению, к середине статьи меня окончательно понесло и останавливаться было уже поздно.
чтобы код читался, как предложение на английском языке

Для этого и был создан Ruby :)

Значит, ни у одного у меня ))

Спасибо за статью! Отличный материал, чтобы освежить в голове данные темы. К списку «Чё почитать» также хорошо может вписаться книга: «Adaptive Code» от Gary McLean Hall. В ней хорошо преподнесены принципы SOLID.
Хорошая статья по ООП. В ней в отличие от многих других статей и книг говорится о том, что ООП это способ структурирования программы, без в корне ошибочного «ООП — это парадигма программирования в терминах реального мира».
«Agile Principles, Patterns and Practices in C#» того же Роберта Мартина. К сожалению, не language-agnostic.

Есть более раннее издание "Agile Software Development, Principles, Patterns, and Practices" с примерами на C++ и Java. Так что можно сказать language-agnostic.

Language-agnostic как раз таки не подразумевает привязки к какому-либо языку.

Таких книг по программированию в принципе нет. Для примеров используется либо общеизвестный ЯП, либо выдуманный (но опять же похожий на общеизвестный).
А найти программиста, который не сможет прочитать примеры ни на C#, ни на Java, ни на C++… Ну, это надо постараться.
Хочется верить, что любой адекватный программист в режиме чтения способен понять любой из этих трёх. Иначе ему тупо закрывается возможность прочитать кучу must-read книг.

А найти программиста, который не сможет прочитать примеры ни на C#, ни на Java, ни на C++… Ну, это надо постараться.

1С-программист? :)

Ну блин… подловили :)

Опровергаю. Еще будучи 1сником читал книги и статьи где примеры как раз на этих языках приводились. Что Роберта Мартина того же, что других авторов. Ибо в 1с коммьюнити нормальной информации именно по программированию толком не найти.
З.Ы. Извиняюсь перед всеми за некропостинг, руки до этой статьи в покете только сейчас дошли.
>Ибо в 1с коммьюнити нормальной информации именно по программированию толком не найти.

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

Я не гуру разработки ПО...


Гуру много раз здесь пытались объяснить людям простым языком — что такое ооп. Но всё время что-то шло не так.

Open/Close это слегка про другое, не про модификацию данных и не про инкапсуляцию. А про возможность повторного использования кода с гарантиями, что производный от него код не сломается при изменениях базового.
Ну и SOLID это совсем не паттерны ООП, как вы подумали. Это сравнительно универсальные паттерны построения прогрмм, которые точно также применяются и в функциональных и в декларативных языках.

Классная статья, спасибо

Возьмём пример пожёстче для большей наглядности. Класс, который запрашивает данные из БД и выводит их в определённый формате в .txt-файл.
Сколько раз читал подобные статьи, не могу приложить все эти примеры к реальности. Допустим мы создали такой неправильный класс. Но ведь мы не обязаны писать внутри класса спагетти-код в котором чтение и записи смешаны хаотичным неделимым образом? Внутри класса будут функции и определенные концепции. Задачи чтения и записи данных так или иначе будут логически разделены. Если они разделены, то позже сделать из одного класса две иерархии классов — задача на 15 минут. Если они не разделены, то проблема не в отклонении от SOLID а в плохой принятой концепции. Плохой программист точно так же может создать два отдельных класса для чтения и записи, но концептуально так организовать их взаимодействие, что заменить класс txt на html будет невозможно без переписывания всего кода.
Т.е. в этом примере принцип единственной ответственности сводится к совету «не пишите спагетти код, который хаотично шарахается от одной задачи к другой, пишите код структурно». Эта структура не обязана выражаться в классах. Точно так же ее можно выражать в хорошем коде внутри классов.
Есть другая проблема. Учитывая, что этот класс уже используется другими модулями, возможно после того как вместо одного класса появится две иерархии, другим программистам придется вносить изменения в свои модули. Эти изменения не потребовались бы, если бы мы сразу подумали о том что БД и файлы могут быть разными. Задача совершенно тривиальная, если в проекте один разработчик, но для крупных проектов это может быть плохо — слишком много всего обновлять, тестировать, отвлекать много людей. Может быть именно об этом и говорит принцип единственной ответственности в контексте ООП? Я не понимаю этот вопрос до конца.
Эта структура не обязана выражаться в классах. Точно так же ее можно выражать в хорошем коде внутри классов.

Больше скажу, так и надо делать. Это упростит их разделение, когда классы всё же придётся разделить из-за их размера, чтобы их было проще воспринимать. И да, принцип единственной ответственности точно так же хорошо применяется и к методам. Например, метод LoadAndInit хороший кандидат на превращение в два метода Load и Init. Так как тут даже в названии намёк на два разных процесса, которые могут поменяться по разным причинам.
Учитывая, что этот класс уже используется другими модулями, возможно после того как вместо одного класса появится две иерархии, другим программистам придется вносить изменения в свои модули. Эти изменения не потребовались бы, если бы мы сразу подумали о том что БД и файлы могут быть разными.

Да, именно об этом и говорится. Смешивание разных ответственностей — ошибка проектирования. И чем раньше её починить, тем дешевле это обойдётся. Как и с любой ошибкой. Идеально — ещё до того, как возникла. Хорошо — до того, как её исправление начнёт обходиться слишком дорого.
Больше скажу, так и надо делать. Это упростит их разделение, когда классы всё же придётся разделить из-за их размера, чтобы их было проще воспринимать.
Но на собеседовании начинающему специалисту на вопрос по SOLID наверно лучше этого не говорить.
Смешивание разных ответственностей — ошибка проектирования.

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

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

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

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

Задачи чтения и записи данных так или иначе будут логически разделены.

Ох, не факт. Методы типа


while (row = table.readRow) {
  file.writef("%s %s", row['Name'], row['Amount'])
}

встречаются сплошь и рядом. Причём table и file — сущности внешних библиотек.

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

Добавлю: SRP применим на самых разных уровнях. Такие вещи как "философия Unix", "сервисы" (особенно "микросервисы") — этот тот же SRP на высоком уровне, гораздо выше чем отдельный класс. А логическое разделение по функциям/методам — тоже SRP, но на более низком уровне, чем класс. Вот SRP — уж точно принцип, которій стоит иметь в виду на всех уровнях разработки ПО. Даже предпочтение someVar = someFunc(); if (someVar) ... перед if (someVar = someFunc()) ... — это выбор в пользу SRP формально: у if только одна ответственность, проверка условия, присвоение переменной вне него.

Даже предпочтение someVar = someFunc(); if (someVar) ... перед if (someVar = someFunc()) ... — это выбор в пользу SRP формально: у if только одна ответственность, проверка условия, присвоение переменной вне него.

… Но при этом из соображений некоторой низкоуровневой инкапсуляции (когда значение someVar не должно быть видно или доступно за пределами блока If) имеет смысл предпочесть второй вариант. То есть, это вырожденный случай вот такого кода:


fn doWork(someVar) -> {
    if (someVar) {
        ...
    }
}
...
doWork(someFunc());

`

В большинстве популярных языков оно, насколько я знаю, будет доступно за пределами блока. А если нужна инкапсуляция, то и надо выделить функцию.
В общем на код ревью я таокго не пропускаю. Единственное исключение циклы типа
while (row = getNextRow()) {} и то лучше переписать на какой-то итератор или генератор

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

Да, это потому, что объявление переменных обычно либо вообще не выражение, либо не возвращает ничего полезного. Но говорил я не про популярные языки, а про принцип в целом. И в принципе такая инкапсуляция имеет смысл. А вот переписывать на какой-нибудь итератор — не всегда, так как единственная подобная абстракция, которая решает именно проблему инкапсуляции возвращаемого значения в блоке — это что-то вроде Spliterator<T> в Java, и при этом и там тоже есть "лишние" элементы.

Все по полочкам, круто. Спасибо!
Ни разу эта тема не обсуждалась на Хабре и вот опять… :)
Но за картинки и их наглядность лайк.
Бросил читать на «Потому что если спроектировать ООП-программу без всякой оглядки на инкапсуляцию и полиморфизм, то получим «гроб, гроб, кладбище, несопровождаемость»», чтобы перемотать вниз, положить в закладки и вернуться к чтению.
Отличный пост!
D — The Dependency Inversion
только IoC и DI
На почитать еще
Гради Буч объектно-ориентированное проектирование
Бертран мейер Объектно-ориентированное конструирование программных систем
Последняя отлично раскрывает тему инкапсуляции и наследования.

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


И не хватает информации по отношению между объектами: ассоциации, агрегации и т.д. (Хотя уделили времяместо под KISS, YAGNI. Которые также косвенно касаются темы статьи).


Может есть задумки сделать вторую часть?
Можно было бы туда вынести заголовок "Главное в разработке ПО" и рассказать про отношения объектов.

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

Абстракция — общепрограммистский, даже общеинженерный и общенаучный термин.

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


let createCounter = () => {
    var count = 0;
    return [
        () => {count = 0; return count},
        () => {count += 1; return count}
    ]
}

let [reset, inc] = createCounter();
inc()    // 1
inc()    // 2
inc()    // 3
reset()  // 0
inc()    // 1

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

Это не функциональный, а процедурный стиль. Скорее даже недо-ооп реализованное через процедуры и замыкание вместо более удобных инструментов. Но это 100% не функциональный стиль

Ну возможно, но в любом случае инкапсуляция старее ооп.

Ну так ООП выросло на базе этих принципов.

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

Дело обстоит немного иначе в языках реализующих metaobject protocol (Common Lisp Object System), да и вообще в языках в которых есть мультиметоды или multiple dispatch (Common Lisp, Clojure, Julia, C#). Там методы не принадлежат классам — классы это просто тип структуры. А методы реализуются над классами, и один и тот-же метод может быть объявлен для нескольких классов.


(defclass entity ()
  ((x :initarg :x :initform 0)
   (y :initarg :y :initform 0))
  (:documentation "Базовый класс сущности на карте"))

(defclass creature (entity)
  ((hp :initarg :hp :initform 100)
   (mana :initarg :mana :initform 0))
  (:documentation "Живое существо, наследник класса entity"))

Придумываем два типа существ. Обратите внимание — никаких методов внутри класса.


(defclass warrior (creature) ()
  (:default-initargs :hp 200 :mana 0)
  (:documentation "Воин"))

(defclass wizard (creature) ()
  (:default-initargs :hp 50 :mana 100)
  (:documentation "Волшебник"))

(setf conan (make-instance 'warrior))
(setf merlin (make-instance 'wizard))

(describe conan)
;#<WARRIOR {10033335F3}>
;  [standard-object]
;  X                              = 0
;  Y                              = 0
;  HP                             = 200
;  MANA                           = 0

(describe merlin)
;#<WIZARD {1002F1C9F3}>
;  [standard-object]
;  X                              = 0
;  Y                              = 0
;  HP                             = 50
;  MANA                           = 100

Научим их перемещаться.


(defgeneric walk (entity x y)
  (:documentation "Перемещает сущность на новое место"))

(defmethod walk (creature d-x d-y)
  (with-slots (x y) creature
    (setf x d-x)
    (setf y d-y)))

(walk conan 10 50)
(walk merlin 5 20)

(describe conan)
;#<WARRIOR {10033335F3}>
;  [standard-object]
;  X                              = 10
;  Y                              = 50
;  HP                             = 200
;  MANA                           = 0

(describe merlin)
;#<WIZARD {1002F1C9F3}>
;  [standard-object]
;  X                              = 5
;  Y                              = 20
;  HP                             = 50
;  MANA                           = 100

Пока все очень похоже на обычные ООП-языки. Зачем выносить метод из класса пока не очень понятно. Приведем пример еще одного метода.


(defgeneric attack (attacker target)
  (:documentation "Позволяет одному существу атаковать другое"))

(defmethod attack ((attacker warrior) (target wizard))
  (with-slots (hp) target
    (decf hp 20)))

(defmethod attack ((attacker wizard) (target warrior))
  (with-slots (mana) attacker
    (decf mana 10))
  (with-slots (hp) target
    (decf hp 40)))

(attack conan merlin)
(attack merlin conan)

(describe conan)
;#<WARRIOR {10033335F3}>
;  [standard-object]
;  X                              = 10
;  Y                              = 50
;  HP                             = 160
;  MANA                           = 0

(describe merlin)
;#<WIZARD {1002F1C9F3}>
;  [standard-object]
;  X                              = 5
;  Y                              = 20
;  HP                             = 30
;  MANA                           = 90

Метод attack диспатчится используя классы аргументов attacker и target. Когда воин атакует волшебника вызывается одна реализация метода, когда волшебник атакует воина — другая. Получается что метод attack нельзя положить ни внутрь класса warrior, ни внутрь класса wizard — он принадлежит одновременно обеим классам.


Такой вот гибкий полиморфизм.

А что если?


(attack merlin (make-instance 'wizard))
There is no applicable method for the generic function
  #<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::ATTACK (2)>
when called with arguments
  (#<WIZARD {1005F6AB23}> #<WIZARD {10060999F3}>).
   [Condition of type SB-PCL::NO-APPLICABLE-METHOD-ERROR]

Чтобы это исправить надо либо добавить конкретную реализацию для это случая


(defmethod attack ((attacker wizard) (target wizard)))

либо сделать какой-нибудь общий метод, который будет ловить все комбинации


(defmethod attack ((attacker creature) (target creature)))
Как правило, набор методов свой у каждого класса, а не у каждого объекта. Чтобы каждый объект определённого класса вёл себя как и другие объекты того же класса. Буду рад узнать из комментариев о языках, где дело обстоит иначе.


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

Ну инкапсуляцией как раз можно считать "локальное сохранение, и защита, и скрытие состояния".

НЛО прилетело и опубликовало эту надпись здесь
И решить нашу проблему с нетестируемостью ImportantClass можно через инверсию этой зависимости.

Сколько можно тиражировать этот бред, так же как и сам «термин» dependency inversion? Где инверсия зависимости, где?

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

Вот:


Одна стрелка разбилась на две, из которых одна идёт именно что в противоположном направлении.


Хотя, конечно же, картинка нарисована странно: по логике вещей, вверх должна была идти нижняя пара стрелок, а не верхняя.

Откуда она идет?

На приведенной картинке изображена зависимость интерфейса «Very important interface» от классов «Important class» и «Very important class». Не будем касаться такого нонсенса как зависимость интерфейса (абстракции в терминах DIP) от класса (деталей в терминах DIP). Видимо, автор просто не знает, как принято изображать направление связи.

Вопрос в другом. Где исходная зависимость «Important class» от «Very important class», которую якобы инвертировали. Теперь она должна идти в обратном направлении. Её нет, даже транзитивно.

Честно говоря, я поражен, что это требует детального обсуждения. Эта «инверсия» – несуществующая бессмыслица, которая очевидна при любой попытке ее формально описать.

Хотя, конечно же, картинка нарисована странно: по логике вещей, вверх должна была идти нижняя пара стрелок, а не верхняя.

Так зачем, чёрт возьми, вы ее используете?

Вот как объясняет термин сам Мартин:


Может возникнуть вопрос, почему я использовал слово "инверсия". Дело в том, что зачастую при использовании более традиционных способов разработки программного обеспечения, таких как Structured Analysis and Design получаются архитектуры, в которых высокоуровневые модули зависят от низкоуровневых, а абстракции зависят от деталей. Собственно, одна из целей при этих подходах и заключается в том, чтобы определить иерархию подпрограм, описывающую вызов низкоуровневых модулей высокоуровнеывми.… Следовательно, структура зависимостей хорошо спроектированной объектно-ориентированной программы оказывается "инвертированной" по отношению к структуре, получающейся при использовании традиционных процедурных походов.

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

Хочу заменить, что в языках в которых нет наследования напрямую (например Rust) его успешно имитируют с помощью композиции.

    public class A
    {
        public string Id { get; set; }
    }

    public class B : A
    {
        public string Name { get; set; }
    }

    public struct C
    {
        public C(string id)
        {
            Id = id;
        }

        public string Id { get; }
    }

    public struct D
    {
        public D(C @base, string name)
        {
            Base = @base;
            Name = name;
        }

        private C Base { get; }
        public string Id => Base.Id;
        public string Name { get; }

    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var b = new B() { Id = "1", Name = "B" };
            var d = new D(new C("1"), "C");

            Console.WriteLine($"{b.Id}:{b.Name}");
            Console.WriteLine($"{d.Id}:{d.Name}");
        }
    }
Можно ли передать D, где ожидается C, чтобы соблюсти LSP?
Ну можно передать интерфейс который реализует C. Конкретно в Rust похожая вешь называется трейт и у нас может быть там interface IC{} и interface ID:IC{}
А есть языки в которых внутренняя структура объекта при наследовании и при композиции отличается? Я считаю что наследование отличается от композиции только интерфейсом использования такого объекта, а не внутренней структурой. Так или иначе внутри объект это список его полей. При этом один или несколько полей от начала могут быть помечены как родители. Потом эти пометки читаются при попытке выполнения виртуальных обращений.
Это если у вас уровень наследования один, то наследование легко заменить композицией. А если у вас целая огромная иерархия, то делать её с помощью композиции будет не особо приятно.

Другое дело хорошо ли это когда у вас огромная иерархия наследования, но это совсем другой вопрос.

JavaScript?
В JavaScript предок задан во внутреннем поле [[Prototype]]
Там не «При этом один или несколько полей от начала могут быть помечены как родители. Потом эти пометки читаются при попытке выполнения виртуальных обращений». Есть список своих полей, а если оно не найдено, то идёт обращение в родительским.
Вас смутило «от начала»? Да я так написал думая о позиционных полях. Конечно поля могут быть и именованные. Сути дела это не меняет. Структурно предки это все равно поля объекта, т.е. композиция
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.