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

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

Время на прочтение4 мин
Количество просмотров40K
Всего голосов 42: ↑24 и ↓18+13
Комментарии53

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

Принцип Don’t Repeat Yourself (Не повторяйтесь) может хорошо работать в коде, но в тестах его применять нет никакого смысла.

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

Если рассматривать с точки зрения этого принципа код u.getСompany.getOwner().get …, то он будет написан неверно

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

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

В целом, любой принцип можно послать лесом, если нет планов поддерживать проект либо он совсем маленький.

> если нет планов поддерживать проект
А то. Самое смешное, что прямо в определении принципов SOLID, черным по белому написано:

>five design principles intended to make software designs more understandable, flexible, and maintainable.

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

Есть небольшой нюанс с этим определением, understandable и flexible это противоречащие друг другу требования

Да, согласен, хотя я бы может чуть иначе сформулировал: они могут противоречить друг другу. Не обязательно будут, но могут.

Правда, так как это все-таки пять принципов, и они могут повышать разные показатели.

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

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

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

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

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

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

Хорошо, но как тогда правильно? Если нам нужно определить имя владельца компании, в которой работает данный пользователь, как это должно выглядеть если всё красиво? Скорее всего у нас будет следующая структура данных: таблица пользователей, в которой будет поле для идентификатора компании, в таблице компаний будет идентификатор владельца (вероятно, в таблице пользователей), и в нём будет текстовое поле с именем, ну или, допустим, возможно все данные загружены в память и там указатели на соответствующие объекты, а не поля с внешними ключами. Нам в любом случае нужно как-то пройти по цепочке. Ну или сделать откровенно неправильно: хранить данные о фамилиях владельца компании-работодателя в объектах класса User, либо в них реализовывать получение данных по этой цепочке (и по многим другим), что, понятно, неприемлемо. Как в данном случае должен выглядеть правильный API, чтобы реализация тоже могла быть правильной?

Что то Вы тут всё смешали. Речь про таблицы БД?
Цепочка u.getCompany().getOwner() в любом случае выглядит странно.

Вы выгружаете в память всю БД?

Мы же сейчас говорим не про оптимальные способы работы с базами данных. Ну, хорошо, если там база данных, цепочка тех методов может формировать SQL-запрос (каждый метод добавляет очередной INNER JOIN), или, допустим, там сделано очень неоптимально, и метод getCompany() у объекта типа User грузит из базы данные компании (в т.ч. идентификатор владельца), getOwner() у Company загружает объект типа User, соответствующий владельцу, и т.д. По виду методов примерно понятно, какие данные они получают, и как эти данные связаны между собой. И вопрос в том, как тогда пройтись по этой цепочке данных, если тот вариант неправильный? Как избежать зависимости от нескольких сущностей (вместо одной)?

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

S или Single-responsibility principle (Принцип единственной ответственности) применяется для проектирования объектов, классов и методов. Его суть в том, что каждый из перечисленных элементов должен отвечать только за что-то одно.

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

Тем не менее критика оправдана.

Определить оптимальное разделение зачастую сложно и оно может меняться с изменением функциональности компонента.

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

К этому можно отнестись скептически - как структура организации может влиять на архитектуру?

Но по факту это наблюдается регулярно и имеет под собой основания.

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

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

А можно пример с вредностью cqs не на руби? Просто совсем непонятно

bool checkWindowHidden(window) {
	system("rm -rf *");
  return window::hidden;
}

if (checkWindowHidden(window)) { return 1 };

Странный пример против cqs...

ну у функции есть побочные эффекты, она нарушает cqs. как это доказывает вредность cqs?

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

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

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

Если ты работаешь с этим, нужно понимать, что это и есть предел применимости lsp

Ну желательно не делать

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

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

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

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

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

Да, вероятно это намёк на цитату ниже не рассматривая «бесполезность» принципа KISS. ?
Большинство принципов, описанных выше, не имеют ничего общего с реальной жизнью: чаще они создают избыточную сложность, чем делают код понятнее, безопаснее или правильнее. Поэтому подходить к их применению нужно критически.

Короче говоря, сейчас в тренде IT фраппировать публику на тематических конференциях. Скоро собеседования будут выглядеть так:

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

З.Ы. Жду на Хабре статью "О недооценённости оператора GOTO". Уже пора бы.

О недооценённости оператора GOTO

Вы сделали мой день)))))

Любопытно: фразу "...чем делают код понятнее..." можно понять как "тем самым делают код понятнее" :)

S, L — важный и правильный, но бесполезный на практике принцип.

значит вы просто еще не поняли ИСТИННЫЙ смысл SOLID

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

По поводу S - тот же Мартин говорит что в реальности это не то чтобы компонент должен делать что-то одно, а то что у него должна быть единственный actor (причина) для изменения.

Практически для каждого понятия\явления\методологии в IT есть содержание и форма.

Проблема в том, что пытаться объяснить содержание Джуну+ / Мидлу- всего что ему нужно знать - без шансов. Поэтому есть дилема:
- не говорить ничего про SOLID \ Agile \ CI-CD (ой вроде во всём этом Джун+ должен уже оринетироваться) - объяснить суть так, чтобы суть была понята времени нету
- рассказать про всё это формальными словами, а потом на практике (через кушание говна ложкой на так-себе-проектах) поймёт сам что реально обозначали эти формальные понятия.

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

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

а может кто-то объяснить про принцип L и про ковариантность/контрвариантность? Не противоречит ли, например, контрвариантность принципу подстановки? Или там главное, чтобы код отрабатывал интерфейсно, а становятся методы конкретнее/шире - всё равно?

Принцип Лисков, если на пальцах и в терминах ООП, довольно простая штука. Представьте, что у вас есть базовый интерфейс и иеерархия наследников, с несколькими реализациями. И базовый интерфейс не просто задаёт сигнатуру функции, но еще и некоторый контракт, например, выраженный в виде набора пред и постусловий. К примеру, он содержит метод сравнения, bool less(a, b). Для него может быть контракт что метод никогда не кидает исключений, и что если less(a, b) && less(b, c), то всегда less(a, c).

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

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

Нет, не противоречит. Наоборот, ко/контр/инвариантность по сути и задаёт "направление", правило, что вместо чего вы можете подсавить: базовый тип вместо более специфичного, специфичный вместо базового либо ни то ни другое.

спасибо, стало понятнее

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

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

Хотел добавить, что "сложность" LSP не столько в контрактах, сколько в поведении. Если интересно, посмотрите про Behavioral subtyping.

По-быстрому:
У вас две реализации Linked List. FIFO vs LIFO. Контракты (Interface) идентичны. Но в данном случае нельзя заменить один класс другим не нарушив LSP.

https://www.youtube.com/watch?v=-Z-17h3jG0A

Автор!

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

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

Желаю Вам осознать свои заблуждения.

Согласен "фифти-фифти" — фифти с автором. (У меня опыт >30 лет, работал и по сложным системам). Разработка сложных систем имеет особую специфику. И в сравнительно несложных некоторые принципы могут не иметь большого смысла. Может автор в некорых местах слишком категоричен, однако заголовок спасает: бесполезность не значит вредность.

Ну вот у меня, например, опыт 30+. Я смотрю на принципы с чисто утилитаной точки зрения - помогают - значит уже использовал, не использовал - значит мешали. Т.е. строго постфактум. За годы сформировался (и меняется) стиль. С какими-то принцирами он совпадает, с какими-то нет. Я и расшифровку этих абревиатур-то не помню. Ибо зачем?

У меня нет 30 лет опыта, всего 14, но пока пришел к тому, что любой принцип разработки – это абстракция, которая призвана упрощать реальность. Но как мы знаем абстракции текут, а значит и принципы не имеет смысла бездумно применять в 100% случаев.

Кажется, это автор и хотел донести.

У него это получилось почти как «абстракции текут в 100% случаев», что конечно же неправда. Во всяком случае формулировки именно такие:

>Большинство принципов, описанных выше, не имеют ничего общего с реальной жизнью
Согласны, что это утверждение и ваше:

>принципы не имеет смысла бездумно применять в 100% случаев.
на самом деле — два разных?

У автора тут есть квантор всеобщности. Чтобы его доказать, мало привести пример или два. Вот автор этого не понимает — что его примеры не в кассу. Если принцип неприменим в примере — он неприменим в примере. Про остальные проекты, мои или ваши, автор ничего не знает, и ничего путного не сказал.

>которая призвана упрощать реальность
Ну, скажем так — принципы (некоторые) следует применять, если вам нужно это упрощение. Я бы сказал, что это касается почти всех принципов, кроме некоторых типа LSP.

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

Квантор всеобщности - это неявный отсыл к вере в серебряную пулю...

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

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

Я бы сказал, что у большинства «принципов программирования» есть такое свойство, что они являются необходимыми, но не достаточными признаками хорошего кода. То есть — если вы видите хороший код, то в нем внезапно обнаруживается и DRY, и SOLID, и много чего еще. А студентов учат так, как будто именно применение этих принципов ДЕЛАЕТ (!) код хорошим.

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

Правильное применение принципов — это когда вы смотрите на свой код, который, скажем, нарушает принцип DRY — и задаете себе вопрос: «А правильно ли, что вот этот участок кода дублируется? Есть ли внешняя причина ему так оставаться? Какие последствия будут если его свернуть и вынести во вспомогательный объект/метод ?». И дальше принимаете решение — потому что все принципы дополняются еще одним правилом из авиации: «Выполняй или объясняй». Принцип — это не про то, что нужно себе лоб расшибить — но чтобы двух одинаковых строк в коде не было… Принцип — это про то, что вам нужно иметь хорошую причину чтобы оставить проблемное место именно так, как оно написано.

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

Ни один великий скрипач не стал великим только потому, что прочитал несколько книг типа «Принципы игры на скрипке...».

В целом вы все верно написали, за одним исключением: принцип DRY - это не про дублирование кода.

У нас тут в laravel новички постоянно оборачивают конструкции Eloquent Builder в отдельные методы якобы следуя принципу не повторяйcя, но по сути они одну строку кода, оборачивают в метод, и якобы думают что они этим самым следуют принципу DRY. Но по факту они следуют принципу "выбесить тим-лида, 1001 способ)".

И с другой стороны, если не соблюдется DRY: это практически всегда говорит о неправильном использовании концептов архитектуры. То есть код сам по себе пахнет, и как правило это всегда сопровождается не правильным использованием жизненного цикла запроса в веб-приложении (мы используем фреймворки и CMS на основе фреймворков, и когда "забивают" на архитекрнуые принципы в рамках этого "каркаса", это очень заметно и практически сразу ведёт к проблемам: повторяющийся код, не связанная логика, разделение принципов ответственности тоже отсутствует).

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

Весьма занятно видеть, как преподаватель разработки ПО доказывает неэффективность паттернов проектирования путём критики кодстайла. Впрочем, пофигу: когда очередная волна студентов не сможет войти в IT, моя цена только повысится.

CQS

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

И эти люди поставляют нам кадры... :-))

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

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

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

По поводу тестов. Здесь есть тонкая грань. Есть 2 крайности:

  1. Создать врапперы для хелперов хелперов стабов и ассертов и да - в этом сложно разбираться и поддерживать.

  2. Убрать явно повторяющиеся строки кода в однотипных тестах в отдельные понятные функции которые находятся прямо в этом же файле например.

Я видел как игнорирование пункта 2 приводило к тестовым файлам на несколько тысяч строк и тетсам на 200-300 строк. Поэтому нужно соблюдать баланс.

P.S. Написать хорошие, работающие, надежные, краткие и понятные тесты это вообще говоря часто сложнее чем основной код.

Принципы - не субъективная вещь. SOLID объективен, и каждая буква в нем влияет непосредственно на вполне объективные метрики вашего кода.

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

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

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

Хорошо подумайте, вы уже на том уровне понимания, чтобы закрывать законы и открывать новые?

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

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий