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

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

Вы как-то сильно категорично к этому принципу относитесь. На самом деле речь идёт лишь о том, что логика класса-наследника от какого-то родительского класса не должна ломать работу тех методов, которые обращаются к методам родительского класса. И следование этому принципу, во-первых, необременительно, во-вторых, действительно полезно.
Аккурат наоборот, и обременительна – Вы вынуждены придерживаться соглашения, и бесполезна – посмотрите хотя бы на «Закон Дырявых Абстракций» — Джоэл Сполски. Что-то не выходит каменный цветок даже когда очень стараются.
Вы немного не так воспринимаете. Понятное дело, что далеко не всегда удаётся создать долгоживущий и универсальный контракт. Вы предлагаете нарушать контракт, если он не удовлетворяет условиям, а есть другой выход из ситуации — создавать новый контракт, с новым интерфейсом. И это не нарушает LSP. Я сейчас не говорю, что это серебряная пуля, и так нужно делать вообще всегда. Но зачастую это более правильный выход. Вы же сами наверняка знаете, поиск багов в коде по колено из-за нарушения контракта — он обычно намного более болезненный, чем разработка нового интерфейса под новое поведение.
И я том же.

Кто-то создал труп, который нам не интересно препарировать. Мы создаем следующий.
Я рад за вас, за себя нет.
А у вас есть какие-то другие варианты? Ну, кроме заведомо печального «сидеть и проверять/переделывать все написанные ранее обращения и тесты, если они есть». Оно взлетит, если это какой-то узкоспециализированный класс, который используется в небольшом количестве мест в коде. Но в этом случае вы можете и не заморачиваться с большинством принципов SOLID, оно вам особой выгоды не даст. Если же это какой-то часто используемый контейнер, модель и т.д., то тут вы, вполне вероятно, повеситесь раньше, чем приведёте код в порядок после нарушения LSP.

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


Таким образом, если рассматривать этот принцип без фанатизма, то он вполне выполним. Типичный пример: а давайте создадим Stream, в котором будет Read и Write. Через полгода приходит кто-то и делает read-only-поток, в котором Write бросает исключение. Здравствуй, нарушение LSP (причем еще на фазе проектирования интерфейса). Как чинить? Или (более плохой вариант, я смотрю на тебя BCL!) заранее заложить CanRead/CanWrite, поведение которых согласовать с методами, или же (вариант получше) сделать отдельно ReadableStream и WriteableStream (но тогда нужен более мощный язык, который бы позволил сказать, что мы хотим на входе параметр, реализующий и то, и другое). Все, с точки зрения интересующего пользователя поведения LSP выполнен.


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


PS


Данный принцип не выполним никогда, простая замена Int32 на Int64 уже делает его не выполнимым

Ну так одно и не подтип другого, вообще-то.

По поводу Stream примера — вроде еще третий путь возможен: вынести read и write методы в отдельные интерфейсы и сделать Stream расширяющим их? В этом случае LSP не ломается.

А вообще очень хорошо сказано, дополню только, что выполнение LSP зависит напрямую от опыта. На моей практике первые пять лет разработчики о нем в лучшем случае не думают, в худшем — попросту не знают. И работают отлично, создают кучу кода, считая себя опытными зубрами. Вот только в последствии всем их наследникам приходится разгребать недостатки LSP с помощью шлака наподобие
if (o instanceof SomeChild) { 
// do some implementation specific processing
}


На моей практике именно парадигма «if-instanceof» чаще всего служит признаком сломанных LSP принципов. Если их придерживаться при проектировании/рефакторинге иерархий классов, то с абстракцией всегда можно работать напрямую без ветвлений.

Собственно продолжу мысль: даже многие опытные разработчики не ведают о LSP принципе. Но большая часть старших программистов все-таки начинает мыслить именно в этом русле — как только встает необходимость отнаследовать класс или реализовать интерфейс (наследование все-таки сложнее и чаще приводит к ошибкам) мысленно прогоняет наследник/реализацию через LSP принципы. Это как работа с массивами или ссылками — опытный программист всегда автоматически проверит индекс на попадание в диапазон или ссылку на предмет Null, тогда как начинающий непременно закодит алгоритм без нужных проверок и отхватит проблемы в продакшне
По поводу Stream примера — вроде еще третий путь возможен: вынести read и write методы в отдельные интерфейсы и сделать Stream расширяющим их?

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

Сорри, маленько не понимаю — если мой метод принимает Stream, который расширяет WriteStream и ReadStream — значит, он умеет делать обе вещи и я могу им пользоваться легко? В случае, конечно, если реализация не бросает UnsupportedOperation в одном из методов, о чем Вы упомянули прежде.

… а теперь вспомните, что у потоков еще есть третья операция — Seek. И надо вам, значит, поток, который умеет произвольный переход и чтение, а на запись вам наплевать...

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

Комбинаторный взрыв — это следствие. А причина — это интерфейсы по возможностям и слабые языковые средства.


первый признак, что наследование желательно заменить другими паттернами типа Bridge или Decorator.

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

UnsupportedOperation — это первый признак нарушения LSP.

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

public interface IReadable { }
public interface IWriteable { }

public class ReadableWriteable : IReadable, IWriteable { }

public void ReadWrite<T>(T stream) where T : IReadable, IWriteable
{
	...
}

void Call()
{
	ReadWrite(new ReadableWriteable());
}

Да, можно, и я так делал. Но есть проблема: если у вас метод дженерик не только для этого, но еще и по делу, и его параметр не выводился (т.е., было какое-нибудь Read<T>), то вам придется типы указывать явно. Что не ужасно, но раздражает.

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

Ключевое слово полезен, но не выполним. Полезно знать, что не плохо было бы если бы…, но полезно знать, что и не выйдет даже если…

Данный принцип не выполним никогда, простая замена Int32 на Int64 уже делает его не выполнимым

Ну так одно и не подтип другого, вообще-то.

Как сказать, куда смотреть. В C# оба наследники (через костыли конечно), но Оbject.
К тому же что мешает реализовать в проекте более лучшее решение, работающее быстрее, точнее, чем работа с Int32. А затем, спустя время, «наследника». Будете использовать делегирование, наследование?
Ключевое слово полезен, но не выполним.

Почему невыполним-то?


Как сказать, куда смотреть. В C# оба наследники (через костыли конечно), но Оbject.

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


К тому же что мешает реализовать в проекте более лучшее решение, работающее быстрее, точнее, чем работа с Int32. А затем, спустя время, «наследника». Будете использовать делегирование, наследование?

Я вообще не понял, что вы спрашиваете. Ничто не мешает. Буду использовать то, что подходит под задачу.

Почему невыполним-то?

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

Можете Вы создать хорошее решение без – ДА, ВОЗМОЖНО. С ним – ДА, ВОЗМОЖНО.
Мешает ли Вам принцип – ИНОГДА.
Нужно ли ему следовать – ДА, КОГДА ОН НЕ МЕШАЕТ. НЕТ, КОГДА МЕШАЕТ.
Выполним ли принцип – НЕТ. Если Вас это утешит, можно написать ДА, КОГДА ОН НЕ МЕШАЕТ.
Выполним ли принцип – НЕТ.

Нигде в вашем комментарии нет обоснования этого утверждения.


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


В последствии, даже если, у Вас появится время (не занятое ни одним другим проектом) для изменений будете ли Вы приводить своё решение к этому принципу – НЕТ (оправдание: у нас нет времени, есть более значимые задачи)

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

Ну так они оба наследники от Object, соответственно для правильного сравнения код должен работать не с Int32, а только с Object. Тогда можно свободно вместо Int32 использовать Int64 и прочие наследники от Object.

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

Согласен, что при замене Int32 на Int64 может все работать нормально, можно даже заменить Int да Double и, в некоторых случаях, будет работать нормально. Я просто хотел указать, что пример не подходит для описания принципа. LSP говорит, что если вместо базового класса, будет передан наследник. А Int32 и Int64 это две параллельные иерархии наследования от Object.

Ничего они не наследники, Int значимый тип, а значит структура, а к типу object приводится путем упаковки. Да и в С# структуры не наследуются. Так, что Int32 и Int64 разные типы, как и int и double, один можно привести ко второму, а бругой без потерь нет.
Десятки лет разработка ПО велась без принципов и методологий, путём проб и ошибок наш брат дошёл до того, что мы имеем сейчас. А вы предлагаете вернуться в каменный век, где у каждого свои методологии и архитектуры? И делаете такое предложение на основе собственного непонимания принципов SOLID.

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

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

Где сказано, что нужно вернутся, Сказано сделайте шаг вперед.

Если Вы придерживаетесь принципов SOLID у Вас не выйдет ни чего путного, но если вышло, то Вы и не придерживались. Возможно вы смотрели на них, но нарушали везде где нужно.

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

Решайтесь, Вы за или против.
Если пользователь доволен, код не поддерживает принцип, нужен ли принцип?
Нужен ли принцип, если пользователь не доволен?

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

Ну да, руки подняли, код потеряли.

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

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

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

Следовать этому принципу не всегда просто. Но выглядеть это должно так — если метод ожидает на вход List, то никакая имплементация List не должна сломать работу этого метода. ArrayList, LinkedList, CopyOnWriteList — абсолютно все равно, ваш метод должен будет выполнится правильно (если не нарушается синхронизация, т.е. этот принцип про это ничего не говорит).
Но есть множество реализаций, которые не выполняют этот принцип, они удобные и классные, но часто порождают баги (ImmutableList, UnmodifiedList, SkippedList).

Выгода от этого принципа: Вы заменяете одну имплементацию на другую и получаете прирост производительности (LinkedList -> ArrayList, или в редких случаях ArrayList -> LinkedList), Заменяете ArrayList имплементацией из throw и получаете выигрыш по потреблению оперативной памяти.
Если предыдущий можно как-то довести до возможности реального применения, то тут без вариантов. В пределе Вы не можете изменить свой код сразу после того как поднимите руки над клавиатурой.


Слабое описание, если честно. «Фыр-фыр-фыр, эмоции, фыр-фыр-фыр, еще эмоции». Можете более четко сформулировать, что вам не нравится в «open/closed principle»? Без аналогий, аллегорий, метафор, гипербол, всего вот этого?
Мне не нравиться что сложившая ситуация, когда решение после акцептирования остается «мертвым». Что часто решение сравнивают с конструкциями из стали и бетона, хотя требуют динамичности и пластичности.
OCP не об этом. OCP о том, что желательно делать такие объекты, для модификации поведения которых не надо лезть им в код. А надо параметр передать. Или свойство установить. Или конфиг файл поправить, на худой конец. Отнаследоваться от него, если уж совсем припрет.
Ну да, ну да. Делать такие объекты, которые не меняются ни когда.
Замечательно. Ползем дальше:

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


Предположим, я проектирую свои классы так, что не нужно лезть в их код, чтобы менять их поведение. Как из этого следует то, что «рано или поздно вы сядите и перепишете все» и «проект к тому времени протух»? Переписывание — это вообще естественно для разработки ПО, потому что сложность, изменяющиеся требования, колешек Миллера, вот это все. Переписывать придется в любом случае, если мы хотим что-то менять. Остается только «протух»? Это что за волшебное состояние проекта и как оно следует из OCP?
В том, что OCP дает вам надежду что этого не произойдет, поскольку вы будет придерживаться правил.
Принцип подстановки Барбары Лисков
Собственно, никаким примером неосуществимости не подтверждено. Абстрактный класс, интерфейс, он накладывает контракт выполнения, но его наполнение зависит от языка. То, что время выполнения методов не указывается в C++\Java\C#, собственно, проблема (проблема ли?) языков, но не SOLID в целом, ведь сама концепция не запрещает накладывать и такие ограничения.

Конкретный пример, метод «выбросить мусор» требует любой объект, способный его содержать. Вы привыкли к контейнеру, но и мусоропровод, и урна, и мусоровоз, и даже чистое поле без признаков цивилизации на 200км в радиусе вам подойдёт. Вам, в принципе, не важно, куда выбросить, иначе это было бы вынесено в сигнатуру. (Этот коментарий не является призывом к загрязнению окружающей среды! Берегите природу!)

Что до отсутствие взаимозаменяемости предков-потомков аля Int32-Int64, она исходит не из самой концепции, а из совмещения в одном классе диаметрально разных функций: целого числа и битовой маски, к примеру, а так же из использования эмпирических констант в коде вместо использования именованных констант. Это не проблема SOLID, это проблема программиста, нарушающего SOLID.

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

Принцип открытости/закрытости
Приведён вами в неверной трактовке. Класс/объект, после того, как его внешний интерфейс определён, не имеет права потерять часть этого интерфейса. Иначе вызов потерянного метода приведёт к… к чему, собственно? Что должно произойти, если кто-то попытается получить доступ к потеряной части?
Принцип подстановки Барбары Лисков

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

Что должно произойти, если кто-то попытается получить доступ к потеряной части?

Что произойдёт, когда вы возьмёте сломанный костыль – ни чего хорошего, Вы упадете. Но вместо того чтобы считать, что такое поведение для класса (программиста) не допустимо, подумайте вот над чем. Может решение стало лучше после декомпозиции, внедрения иного подхода. Но Вы по-прежнему требуете свой «костыль».
Какая интерсная трактовка OCP. Откуда это, если не секрет?
Из вики же! Откуда ещё? А что с ним, собственно, не так?
Класс/объект, после того, как его внешний интерфейс определён, не имеет права потерять часть этого интерфейса.


Я никогда раньше не слышал такой трактовки OCP. В русской википедии написано:

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


Я правильно понял, что вы трактовали OCP как частный случай пункта №4, про интерфейс?
Да, в том числе. Разве я не прав в этом тезисе?
Из того что я понимаю — это маленький кусочек, частный случай. Принцип-то о том, что не надо делать объекты, любой чих по отношению к которым требует лезть им в код. А про потерю части интерфейса, это больше похоже на труизм «не надо ломать код» :) К этому многие принципы можно свести.
Из того, что дано на русской вики, можно сделать очень много разных выводов. Там, например, дано определение по Мейеру, полиморфное определение, и вольная трактовка без ссылок, которой нет на других языках. Все три вместе они не позволяют вывести общее определение кроме как с помощью отрицания.
Именно эту ситуацию я и описал в #comment_9764310 как клинический дегенератизм.

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



Должен сказать, ООП как концепция — прекрасная штука. Проблема в том, что её описывают клинические дегенираты. Я не могу найти другого определения для описания людей, использующих одни и те же термины для описания разных вещей. Критически разных. Диаметрально.
Можете назвать меня ситхом, клиническим дегениратом или просто заминусить в сраное говнище, но, ИМХО, пока не найдётся того Геркулеса, который разгребёт все эти Авгиевы конюшни, ничего путного в спорах получить не удасться. Потому что мы даже в терминах не имеем согласия.
Заклюют же.

ООП очень ограниченная концепция, как ФП (функциональное программирование).

К сожалению, одно ограничение приводит к следующему и так далее. И всё множество попыток втиснуть большее в меньшее, путём создания ограничений не добавляют ни чего.
Мы создаем тоже код, такой же плохо, но теперь больше.
Мы умеем его создавать, используя шаблоны T4, но лучше он не становится от этого. В нем просто меньше «глупых» ошибок.

Может даже огорчать, что множество «безумцев» так и будет защищать свои любимые, создаваемые годами, «костыли». Поднимать на щитах призывы к светлому будущему, но делать всё тот же страшный код.
Заклюют же.
И? Я должен теперь забиться в ужасе в угол? Пфффф! Пускай клюют, если им хочется.

ООП очень ограниченная концепция, как ФП (функциональное программирование).
Жизнь ограничена законами физики. И смертью, да. Мы все умрём. Что дальше, отказываться от жизни сейчас?

И всё множество попыток втиснуть большее в меньшее, путём создания ограничений не добавляют ни чего.
ООП, оно не про впихивание слонов в холодильники, оно про то же, про что был сериал Декстер (Кстати, стоит досмотреть последние 2 сезона?) — расчленение и правильную утилизацию ошмётков.

Может даже огорчать, что множество «безумцев» так и будет защищать свои любимые, создаваемые годами, «костыли». Поднимать на щитах призывы к светлому будущему, но делать всё тот же страшный код.
Да, вы правы, мы будем защищать наши любимые до боли в зубах костыли. Знаете, почему? Альтернативы хуже. ООП и ФП — две вершины представления систем, олицетворяющие подход практика и теоретика к задаче. Дальше идут только вариации.
Всё программирование в целом — это инструмент. Инструмент для перевода модели из образно-функционального описания в термины микропроцессорной архитектуры. Как и любой инструмент, его можно использовать неправильно. Быть может, вы именно этим и занимались?

З.Ы. У вас забавные проблемы с глаголами, и не только. Вы откуда будете родом?
Поддерживаю, интересная трактовка. А можно где-то найти более полное описание мысли и желательно от автора мысли?
во-первых, принцип Барбары Лискофф, как вы сами написали в статье:
«объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы»
Только лишь правильности. От того, что программа раньше писала в текстовый лог, а теперь начала писать в бд, правильность не изменилась. Изменилось поведение. Так, как надо было разработчику.

Open/Closed Principle — то же самое, на том же примере: если раньше был класс-логгер, пишущий лог в файл, и понадобился логгер, который будет писать в файл и в бд, можно просто расширить предыдущий класс новыми функциями и подставить его экземпляр вместо старого. При таком подходе проблемы могут быть только с классами, которые в силу конкретной реализации разработчиком не пригодны для расширения.

В двух словах, вся методология SOLID сводится к: «пиши код, состоящий из расширяемых и заменяемых элементов». Если следуя методологии SOLID у вас получаются не расширяемые и не заменяемые элементы, значит, вы что-то делаете не так
...(Очень интересно услышать бы определение «правильности выполнения программы»...).

От того, что программа раньше писала в текстовый лог, а теперь начала писать в бд, правильность не изменилась. Изменилось поведение. Так, как надо было разработчику.

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

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

Перефразирую. Проблемы возникнут как только разработчик не предвидел того что понадобилось на текущем этапе развития проекта.
Определение правильности (корректности) программы существует. См. гугл.

Пишем в базу. Глотаем ошибки? Пишем их туда же в лог? Не обрабатываем?

У потомка базового класса-логгера может быть новая зависимость — какой-нибудь класс-обработчик ошибок. От этого принципы SOLID не нарушатся, а программа не потеряет в корректности.
Почему Вы считаете, что изменение поведения не меняет «правильность»?

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

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

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

Меня там не было, я не записывал ^_^’’

Бывает, сон помогает +1
Хорошо если Вы его знаете, лучше когда Вы знаете что его нарушаете, а еще лучше когда знаете зачем Вы это сделали.
В Вашем случае Вы замыкаете решение на заданные условия и придерживаетесь их до тех пор, пока они не проходят тестирование временем.
В моём случае я залезаю в исходники известного проекта и препарирую его, вырезая все части, которые, с одной стороны, достаточно интересны в контексте моих задач, а с другой стороны, не слишком заумны. Иначе очень просто использовать инструмент не к месту.
Что произойдёт, когда вы возьмёте сломанный костыль – ни чего хорошего, Вы упадете.
То есть сохранение сигнатуры, будь это интерфейс класса или алгоритма в ФП, это разумное требование?
Может решение стало лучше после декомпозиции, внедрения иного подхода. Но Вы по-прежнему требуете свой «костыль».
Может, оно и стало лучше, как я узнаю о существовании нового метода из старой программы?
Вы упадете.
У Вас не соберется проект, отвалятся тесты, позвонит клиент.
То есть… Я создам проблему из ниоткуда. Шикарно.

Само сабой, баги нужно лечить, от устаревших методов нужно отказываться, но поведение вашей программы, ваших классов должно быть предсказуемо. Если у вас изменился внешний вид класса, быть может, это уже не старый добрый SheriffWillie с его классическим shootHimWithColt, а JudgeDredd и его killHimWithFkngRocketLauncher, и на этой констатации факта оставить шерифа и его ранче в прошлой версии?
Вы уже сами дали хороший пример ответа.
Да создадите. Но создадите проблемы и придерживаясь принципа OCP.

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

Как вы это сделаете? OCP вам не поможет, он вам просто это запрещает.
Может уже есть множество решений, которые эксплуатируют ваш недочет.

Можете поломать совместимость и «накажете» еще и других разработчиков, можете замазать штукатуркой и сделаете дополнительный метод.
Только ситхи всё возводят в абсолют.

Вы предлагаете не поклоняться бездумно принципам SOLID и иметь критический взгляд на ООП, но не используете этот критический взгляд сами, трактуя принципы дословно без объектно-ориентированного мышления.

SOLID, в частности, ИМХО, рассказывает, как структурировать код таким образом, чтобы он вёл себя предсказуемо при модификации и переиспользовании, был тестируемым и, часто, простым для восприятия. OCP, например, позволит не сломать существующий код и избавит от мучительного регрессионного тестирования.

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

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

Классика жанра DoubleList.
Да, добавление двух элементов в список. Без задания на этапе проектирования пре- и пост- условия делают все ваши старания бесполезными.
Один залетевший дятел разрушит цивилизацию.
Сплошной максимализм.
Выходит, что код обязан работать только на машине программиста, а то вдруг вы поставите программу на слабый компьютер? Это же нарушение контракта на время выполнения.
И уж постарайтесь дописать класс за один заход, а то он не сможет быть изменён после подъёма рук от клавиатуры — так и останетесь с недописанным названием метода.
Ну поставите код на сервер, ну замените программиста, добавите еще полгода для верности. И что-нибудь изменится? Нет Вы просто введете еще пару тройку сущностей и усложните условия, возможные причины отказа и способы решения решения.
Может стоить почитать не википедию, а оригинальные статьи (прекрасным языком, написанные, кстати):
http://reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.pdf

Например, на 4-й странице:
«32-bit integers are not a subtype of 64-bit integers (because a user of 64-bit
integers would expect certain method calls to succeed that will fail when applied to 32-bit integers)».

А дальше есть ЧЕТКАЯ модель вычисления с ЧЕТКИМИ определениями понятий типа и субтипа и тех свойств для которых выполняется LSP.
Так что вы всего лишь не разобрались с областью применимости принципа.

UPD Ссылка на более раннюю статью — http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.12.819&rep=rep1&type=pdf
Очень категоричный взгляд на SOLID. Во-первых SOLID, это набор принципов, а не законов. Зачем они нужны, чтобы не ходить по граблям. В зависимости от размера проекта отступление от данных принципов будет вам стоить определенных денег в будущем, они же не про написание кода, а про развитие и поддержку.
От данных принципов можно отступать когда это абсолютно необходимо. Но лучше их придерживаться, чтобы не получить не модифицируемого монстра в итоге.
Придерживаясь вы тоже тратите ресурсы и не только денег и прямо сейчас.
Полагаю, что выбор «неудачной» архитектуры никак не повлияет на выгоду от следования или не следования этим принципам. Т.е. следования принципам вам ничего не дает: вы не увидите, следуя им возможные в будущем ошибки, вы не сможете исправить положение. Они просто вас сдерживают.

Уверяю вам, будет вы ходить по тем же граблям, может быть только на цыпочках и медленно.
Нет. В начале разработки у вас есть выбор, в какую сторону пойти, налево или направо. Но если вы пойдете в неправильную сторону, вам потом придется поворачивать. То есть, исправлять и переделывать. Сумма двух сторон треугольника всегда больше длины одной стороны. Лучше заранее подумать и выбрать правильное направление, или хотя бы близкое к правильному. И принципы SOLID в этом помогают.
Большинство крупных проектов состоит из взаимодействующих частей.
Чем больше проект, тем больше будет этих самых частей, иначе его будет невозможно сдать в обозримый промежуток времени и невозможно поддерживать.

Надеюсь это мы можем постулировать?

От сюда сразу несколько проблем:
1) Стабильность интерфейсов данных частей
2) Баланс между размером и сложностью данных частей.
3) Стабильность поведения данных частей.

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

Как вы предлагаете решать данные проблемы в общем случае, без велосипедостроения?
Размер потребляемых ресурсов: памяти, процессорного времени, используемых ядер, необходимость или отсутствие доп. оборудования.

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

В пределе Вы не можете изменить свой код сразу после того как поднимите руки над клавиатурой.

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

В статье очень много максимализма, как уже говорилось выше. Эти принципы намного гибче, чем вам кажется. И SOLID действительно помогает поддерживать и развивать огромные системы.
Труп млекопитающего тоже может быть большим. И что странно, требует множества «приседаний» для поддержания в нем процессов жизнедеятельности.
Ну так мир — он сложный.
А почему вы считаете трупами большие, но работающие, развивающиеся и приносящие деньги системы?
Кто же против. Парой встречаются исключения. Как в анекдоте «Главное для пациента своевременный уход врача».
Приведите пример системы, которая «развивалась» бы, приносила денег, без «врача» (хорошо если одно, а то порой целый консилиумы дежурят у койки).
Фанатично придерживаясь его, Вы создается что-то уже «мертвое», поскольку оно не «дышит» при рождении и не когда не задышит после.

Фанатичность любую идею делает «мертвой». Так что, если претензия именно к этому, то, думаю, вам стоит вместо написания статей, бить себя по голове, как предлагал проф. Преображенский.

Не возникает ли у вас когда-нибудь сомнение типа "Может быть они все-таки имеют что-нибудь полезное ввиду? Не может же столько народу пропагандировать ерунду?"

Конечно могут. Хотя…

Земля в центре вселенной.
Средние века, инквизиция.

Видимо и большинство может что-то делать иначе.

Ok


L — совершенно правильно. Будучи последовательно примененным LSP запрещает перекрытие реализации. Вы просто недодумали эту мысль и слишком категоричны, чтобы получить из него пользу, а не сходу отвергнуть


O — прочитайте статью в википедии. До конца. В каком смысле OCP соблюдается и соблюдался раньше. Вы просто в полемическое задоре придумали себе абсурдную трактовку и с ней спорите.

НЛО прилетело и опубликовало эту надпись здесь
Такая статья в духе «нельзя написать идеально, давайте писать как-нибудь». Написать идеально действительно нельзя, но если к этому стремиться — то выйдет как-то «приемлемо». А если сразу сказать «к черту все методологии и паттерны» — то уж не сомневайтесь, результат будет хуже некуда.
Не расширяете без надобности простое, до вас тут уже много мамонтов потопталось.
Статья о том, что, используя современные ООП методологии (в частности SOLID) вы создаете нечто, что не способно на самостоятельное развитие. Хорошо если это развитие вообще возможно, лучше если оно возможно без участия вас как «творца».
Да вы тешите себе мыслью, что проект пока «не смышлёный» на этапе прототипа. Да, вы тешите себе мыслью, что проект пока «не самостоятельный» на этапе внедрения. Но на этапе сопровождения вы не на минуту не спускаете глаз с проекта, а если и отвертитесь, то вам напомнят.
Допускаю что вами был изготовлен столь искусный механизм, способный работать автономно, который не требует вашего наблюдения. Но ответе сами себе, развивается ли он сам?

Оу, а вы знаете много (программных) систем, способных на самостоятельное развитие, и при этом не попадающих под расплывчатое определение "искусственный интеллект"?

И? Вся статья об этом. Вы её прочти? Это хорошо =)
Мы используем принципы, чтобы создавать «мертвые» решения. И пропагандируем, что они позволят «развить мертвеца».

Мои познания ИИ скромнее, но там тоже не все так радует, пока получаются только системы «стимул — реакция».

Так вы много знаете таких систем, или нет?


(безотносительно того, использовался при их построении SOLID, или нет)

лучше если оно возможно без участия вас как «творца»

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

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


Новичкам, не наработавшим собственный опыт, имеет смысл им следовать. Опытным программистам все эти SOLID/TDD/паттерны/антипаттерны уже не нужны — они впитали их суть и применяют автоматически и по необходимости, не задумываясь о названиях.

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


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

1. L. Тогда не стоит использовать наследование. :)
2. O. Если писать не публичную библиотеку, то можно не придерживаться этого принципа.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории