Комментарии 400
Раньше на хабре даже предъяви кидали типа "А ты используешь SOLID?"
И попробуй повыступать против догм - получи readonly
Просто большинство догм догмами не являются. Вернее, являются одними из.
Например, есть ещё KISS (Keep it simple, stupid), который даёт свою отличную от SOLID трактовку.
А ещё есть обычный "Ты делает программу для решения квадратных уравнений, нафиг тебе вообще там классы сдались"
И что? Кто из них прав? Да все правы и все неправы. Каждому подходу своё время и своё место.
Как можно найти противоречие там, где его отродясь нет и не было? KISS не противоречит SOLID, который не навязывает классы.
Я где-то писал слово "противоречие"? Вроде, нет. Но и полного сходства между ними не вижу.
Это просто 2 разных комплекса советов, которые живут сами по себе. В чём-то похожи, в чём-то нет.
Например, есть ещё KISS (Keep it simple, stupid), который даёт свою отличную от SOLID трактовку.
Это разные принципы, а не разные трактовки для одного и того же. Или я в упор не понял посыла.
Сообщения вида "делай хорошо, плохо не делай", "каждый случай индивидуален", "все люди человеки", которые при всей своей правоте несут ровно ноль информации и только забивают эфир.
Мне лень приводить примеры на все пять букв, но SRP (да и все остальные) подразумевают пять сущностей там, где KISS — одну, да и само слово «абстракция», которое повторяется в SOLID чаще вспомогательных частей речи — противоречит KISS.
Это разные принципы
Именно это я и написал.
являются одними из (подходов к программированию)
даёт свою отличную от SOLID трактовку (подходов к программированию)
Посыл в том, что я поддерживаю идею, что ни на один из них не надо молиться. Про каждый надо решить "а подходит ли он именно под эту задачу"
Я слышал в другой формулировке: если тебе нужно здесь и сейчас помыть чашку - просто помой чашку, а не городи три слоя абстракции с иерархией классов.
О! Подумалось, что это ИТ-вариант бритвы Оккама, о неумножении сущностей сверх необходимого.
Честно говоря, ещё лет 20 назад, я всё пытался понять это насаждение бэст-практикс... Но так и не пронял. Или не принял. Если я здесь и сейчас могу написать код за 10-15 минут в манере школьника и он не просто будет работать, но и полностью выполнять свою задачу, то зачем мне городить огород в течении дня чтобы следовать этим самым принципам супер-пупер практик написания кода? На сколько показывает опыт, то этот самый код школьника для постороннего чела будет гораздо более понятен, чем венигрет который готовился целый день с этими самыми "правильными" типами, классами и прочими приправами.
Если я здесь и сейчас могу написать код за 10-15 минут в манере школьника и он не просто будет работать, но и полностью выполнять свою задачу, то зачем мне городить огород в течении дня чтобы следовать этим самым принципам супер-пупер практик написания кода?
За тем, чтобы потом спустя года, после десяток дополнений от лица группы других разработчиков (у которых может быть другой опыт) код можно было: а) относительно легко понять, б) относительно легко переписать.
Если вы сами не поддерживаете свой написанный же код на долгой дистанции, то вам не понять эту боль, когда вместо простого полиморфизма для простоты понимания отдельных состояний, код - это набор if'ов и switch'ей, через который нужно пробираться каждый раз, когда нужно менять состояния.
Логика здесь простая. SOLID не из воздуха взялся. Это не какая-то снобская прихоть. Это другой взгляд на проблему долгосрочной поддержки кода. Эти подходы к архитектуре кода появились из необходимости.
И нюанс здесь в том, что с SOLID можно сделать хорошо, а можно всё испортить. Важно применять их правильно там, где они нужны, и не переусердствовать. Если у вас долгосрочный проект, код которого по мимо вас будет поддерживать ещё несколько человек, то соблюдения некоторых подходов SOLID лишним точно не будет
удалил
Я еще не очень понимаю тему с огромным количеством классов исключений на все случаи жизни, которые отличаются друг от друга ... ничем. На вопрос тимлиду, нафига это нам, он ответил что-то типа "ну вот если в будущем нам понадобится вот этот вот конь в вакууме, то...". Потом он свалил, команда перешла на другие проекты, на проекте остался только я. Спустя год конь в вакууме не понадобился. Исключения я все вырезал к чертям, и вместо 50+ классов осталось штук 5. Никакого дискомфорта не заметил.
Типизация исключений нужна для реализации различных реакций. Если реакция везде одинаковая (catch Throwable e), то нет смысла и типизировать исключения. То же самое для http кодов ответа.
Вообще смысл есть, конечно не до фанатизма, но есть. Даже если исключения просто логируются и нет никакой специальной логики (сейчас), но в логах обычно мы выводим тип исключения в отдельном поле, и в моей практике это много раз позволяло правильно и максимально быстро провести оценку влияния и даже иногда замониторить конкретные типы. Поэтому да, всё вообще можно упростить: зачем нам это, зачем нам то, давайте всё уберём. А конь в вакууме может понадобится именно тогда, когда уже написано тонна кода и дорабатать за вменяемое время уже не получится. Принцип "лучше перебздеть, чем недобздеть" работает всегда, но и нужно соблюдать меру. Если обвес приходится обслуживать и тратить на это ресурсы, надо правильно оценить его необходимость. А если он есть не просит, в чём проблема?
Лучше недобздеть. Как показывает практика, переписать тонну кода не представляет большой трудности, если ясны цели и есть работающий код. Рефакторинг - это неотъемлемый процесс. Если код не меняется, то он умирает.
Кроме того, при работе с кодом, простота чтения гораздо важнее простоты написания. Переписываем мы, условно, 1 раз, а читаем месиво из интерфейсов и безумных конструкций сотни раз. Обвес именно что просит есть.
Как показывает практика, переписать тонну кода не представляет большой трудности, если ясны цели и есть работающий код.
А вот для переписи с нуля движка одного браузера изобрели целый новый язык (!) — а потом сами же авторы языка на нём запрограммировать так, чтобы работало — не смогли. Есть мнение, что просто язык оказался так себе, но оно непопулярное.
В разумных пределах это полезно, добавляет семантики, когда, чтобы точно понять, что случилось достаточно только типа исключения, и нет необходимости анализировать текст сообщения.
В некоторых языках, например, можно алиасы на простые типы данных создавать, тоже, казалось бы, избыточность, но если присмотреться:
class Image
{
int id;
int width;
int height;
string type;
}
их использование делает код понятнее и устойчивее:
class Image
{
ImageId id;
Dimension width;
Dimension height;
ImageType type;
}
class Image
{
int imageId;
int width;
int height;
string imageType;
}
Все может быть намного проще, и не надо городить огород. Иногда возникает желание сделать что-то крутое и необычное, но это деструктивный как правило порыв.
если класс называется Image, то зачем дублировать его название в его же полях? и так вроде понятно, что Image.id - это ИД изображения. Первый вариант лучший:
class Image
{
int id;
int width;
int height;
string type;
}
Image.id = 10;
Image.type = "png";
а так, как то не очень:
Image.imageId = 10;
Image.imageType = "png";
Дело не в классе и его полях, а в остальном коде. При вашем подходе у вас будет какой-то int, и какой-то string. А при определении подтипов - сразу везде становится понятно, что эта конкретная переменная не просто некое число, а целый идентификатор картинки - а значит, по нему, скорее всего, можно поискать саму картинку где-нибудь, и он, например, уникален среди всех остальных хранящихся ImageID
, но не будет уникален среди каких-нибудь UserID
, а также что нет самостоятельного смысла в том, чтобы по ImageID
искать пользователя.
В чуть более продвинутых системах, переменная типа Dimension
,скажем, не сможет иметь отрицательное значение, и это будет вам гарантировать компилятор вашего ЯП, а не "честное слово" предыдущего разработчика.
Ну допустим сделал 20-30 подтипов на проект - как это упростит понимание? Например ImageType - это какой тип данных - string, int, enum или еще какой-то? т.е. мне нужно разобраться с произвольными типами данных и держать их всегда в своей голове?
В чуть более продвинутых системах, переменная типа
Dimension
,скажем, не сможет иметь отрицательное значение
Опять же есть int, uint.
Может это где-то и оправдано, но в большинстве проектов нет. Мы же здесь про упрощение кода говорим?
Был бы благодарен за реальный пример, где использование подтипов оправдано.
ImageType - это какой тип данных - string, int, enum или еще какой-то
ImageType - это ImageType, конечно же. Хотя это и не исключает наличия правил конвертирования его в int, string и обратно.
Мы же здесь про упрощение кода говорим?
Я не вижу здесь упрощения при любом направлении движения. Вам всё равно придётся как-то знать и помнить, что вот это конкретное значение кодирует идентификатор пользователя, а уж будет это имя переменной или тип это дело ваше. Просто хранение в виде именно типа, а не чего-то ещё добавит вашему коду невозможность использовать значение ImageType, например, для того, чтобы отправить его в поле с комментарием, без дополнительных движений кодом. Это, в своём роде, упрощает код за счёт ограничения возможного пространства применений для каждого значения.
ImageType - это ImageType, конечно же.
и что вы будете ему присваивать? т.е. это же все равно строка, число или еще что-то.
Я не вижу здесь упрощения при любом направлении движения. Вам всё равно придётся как-то знать и помнить, что вот это конкретное значение кодирует идентификатор пользователя
ну стандартные типы данных все и так знают и помнят, в отличии от подтипов и поле uint User.Id - вполне за себя говорит, что содержит идентификатор пользователя и запоминать его вовсе не нужно, а в коде подсветит, что тип данных uint, а не какой-нибудь UserIdentifier, который может быть чем угодно.
Это, в своём роде, упрощает код за счёт ограничения возможного пространства применений для каждого значения.
а зачем нужно еще ограничивать пространство применений переменной? - есть стандартные модификаторы доступа. Вот реально не придумать такую ситуацию, если только защита от не программистов - но это нонсенс.
Больше похоже на случаи из статьи, где интерфейсы и классы лепят кто во что горазд, т.к. реального преимущества и тем более упрощения так и не увидел.
это же все равно строка, число или еще что-то.
Да нет же. У него могут быть какая-то логика создания из строк или чисел, но сам по себе этот тип - не строка и не число. Если в ЯП, на котором оно пишется, enum
не относится к числам - то ImageType
в принципе можно от enum
унаследовать, но и это само по себе не обязательно.
ну стандартные типы данных все и так знают и помнят, в отличии от подтипов и поле uint User.Id - вполне за себя говорит
Это оно там за себя говорит (если рядом где-то встречается User
, и есть прямой путь от него к id
). Но не в методе pow
, куда этот uint
можно передать, и не после операции "+ 2"
.
тип данных uint, а не какой-нибудь UserIdentifier
Но с uint можно сделать множество операций, которые для настоящего UserIdentifier не имеют абсолютно никакого смысла - тот же пресловутый "+ n"
из блока чуть выше.
а зачем нужно еще ограничивать пространство применений переменной? - есть стандартные модификаторы доступа
Например, чтобы не было соблазна передать UserIdentifier в какой-нибудь
Image? findImageById(uint id)
из этого всё равно не выйдет ничего путного, только сплошные баги и расстройство.
И поиск по UserIdentifier
вернёт места, где используется такой тип значений, а не места, где разработчик соответствующим образом назвал переменную (например, UserIdentifier friendId
поиском по uint userId
найти куда сложнее).
Если в ЯП, на котором оно пишется,
enum
не относится к числам - тоImageType
в принципе можно отenum
унаследовать, но и это само по себе не обязательно.
Зачем наследовать, если можно сразу определить enum?
Но не в методе
pow
, куда этотuint
можно передать, и не после операции"+ 2"
.
не понял
Например, чтобы не было соблазна передать UserIdentifier в какой-нибудь Image? findImageById(uint id)
Так проще названия нормальные дать:
findImageById(uint Id) - поиск картинки по ID
findImageByUserId(uint Id) - поиск картинки по ID юзера.
ну а по поводу соблазна, как и сказал, это защита от совсем непрограммистов и если подходить с этой стороны, то весь код должен состоять из валидаций/проверок. И то, найдутся умельцы, что никакие подтипы и проверки не помогут.
Ну а про поиск использования любой переменной - так это есть во всех нормальных IDE (в PhpStorm & VS точно).
Ну не вижу я реального примера, чтобы прямо подтипы нужны были. И да, подтипы усложняют код и повышают безопасность (только от кого?) Неужели любой подтип нельзя заменить стандартными типами, классами, интерфейсами, структурами или перечислениями?
сразу определить enum?
В данном случае "наследовать от enum" и "определить enum" - это по сути одно и то же.
> Но не в методе
pow
, куда этотuint
можно передать, и не после операции"+ 2"
.не понял
Что такое let id = pow(user.id + 2, 4)
, и почему это должно компилироваться?
Так проще названия нормальные дать:
findImageById(uint Id) - поиск картинки по ID
findImageByUserId(uint Id) - поиск картинки по ID юзера.
А почему не так:
Image? findImageBy(ImageId id)
Image[] findImagesBy(UserId userId)
?
Бонусом пойдёт то, что findImageById(user.id)
компилироваться перестанет, в отличие от.
это защита от совсем непрограммистов и если подходить с этой стороны, то весь код должен состоять из валидаций/проверок
Во-первых, нет, это не от "непрограммистов", а в принципе от людей. Программисты не обладают каким-то особым навыком, позволяющим им не ошибаться - иначе у программ не было бы багов. Ну или мы тут все "непрограммисты".
Во-вторых, в больших системах код и так примерно наполовину из каких-то проверок состоит. И чем больше их можно будет переложить на бездушный компилятор, тем лучше.
Ну а про поиск использования любой переменной
Нет, речь не про поиск использования одной переменной - речь про поиск использования одного типа переменной. Без хотя бы алиасинга для идентификаторов, как много вы найдёте у себя в проекте случаев использования uint
, и как много из них будут иметь отношение к семантическому типу UserID
?
Неужели любой подтип нельзя заменить стандартными типами, классами, интерфейсами, структурами или перечислениями?
Можно, разумеется. Точно так же, как можно заменить большинство uint на any, выбросить тесты, сложить весь код в один файл, и тому подобное. В программировании вообще много чего можно.
В данном случае "наследовать от enum" и "определить enum" - это по сути одно и то же.
так зачем тогда наследовать?
Что такое
let id = pow(user.id + 2, 4)
, и почему это должно компилироваться?
отвечу также - а почему не должно?
А почему не так:
Image? findImageBy(ImageId id)
Image[] findImagesBy(UserId userId)
а почему не так:
Image? findImageBy(uint ImageId)
Image[] findImagesBy(uint UserId)
Нет, речь не про поиск использования одной переменной - речь про поиск использования одного типа переменной.
программирую очень давно и не разу не приходилось искать все переменные по типу - т.е. что дает знание всех переменных определенного типа?
Во-первых, нет, это не от "непрограммистов", а в принципе от людей. Программисты не обладают каким-то особым навыком, позволяющим им не ошибаться - иначе у программ не было бы багов. Ну или мы тут все "непрограммисты".
программист знает, что передает, куда передает и какой ожидается результат.
Можно, разумеется. Точно так же, как можно заменить большинство uint на any, выбросить тесты, сложить весь код в один файл, и тому подобное. В программировании вообще много чего можно.
Так об этом и статья, что код переусложняют лишними классами, интерфейсами и прочими не нужными конструкциями. Вы же предлагает использовать подтипы для упрощения, а в чем упрощения не пояснили, да и в комментарии ниже сами написали, что для сравнения н-р ширины, нужно немного клея).. И сколько клея нужно будет всего добавить в проект и как это упростит код? Вы сами-то используете подтипы? Если да, то приведите, пожалуйста, кусок кода, где оно прям оправдано и делает код простым. А вы переусложняете код просто так - как в статье, не получая никаких явных преимуществ.
Я уверен, что подтипы сделали не просто так и где-то они нужны, но точно не для упрощения кода, а скорее для безопасности.
программирую очень давно и не разу не приходилось искать все переменные по типу - т.е. что дает знание всех переменных определенного типа?
Пример. Берем номер банковской карты, который как бы строка, но делаем типом. После этого анализировать "а куда мы номер карты передаем (и где он может убежать в какие-нибудь логи, в которые не положено)" - становится куда как легче.
Нужно такое, например, когда какое-нибудь новое регулирование приняли и нужно выяснять "а мы соответствуем, или надо переделывать?"
в чем упрощения не пояснили, да и в комментарии ниже сами написали, что для сравнения н-р ширины, нужно немного клея).. И сколько клея нужно будет всего добавить в проект и как это упростит код?
Вот еще пример(теоретический, правда, потому что языки не поддерживают/не поддерживали), когда могло бы помочь:
берем всякие ошибки (иногда сильно дорогостоящие) связанные с единицами измерения. Длину, переданную в функцию, выразили в дюймах вместо сантиметров или наоборот - и получили не то, что хотели.
Если бы компиляторы различали эти типы чисел и ругались - то оно бы ловились на этапе компиляции и написания кода.
Пример. Берем номер банковской карты, который как бы строка, но делаем типом. После этого анализировать "а куда мы номер карты передаем (и где он может убежать в какие-нибудь логи, в которые не положено)" - становится куда как легче.
а просто по использованию или имени переменной искать? Вы предлагаете делать подтипы в проекте, ради иллюзорных поисков? Вам это часто требуется?
берем всякие ошибки (иногда сильно дорогостоящие) связанные с единицами измерения. Длину, переданную в функцию, выразили в дюймах вместо сантиметров или наоборот - и получили не то, что хотели.
Так здесь подтип ничего не решает - это должно обрабатываться на этапе присвоения или в самой функции.
LengthInch length = 5; // здесь значение в дюймах или сантиметрах?
Почему иллюзорных то?
Дальше вы этот номер карты маскируете и у вас два типа - один про полный набор цифр, другой про что-то типа "последние 4 цифры". И все потребители ваших данных знают, получают они полный номер или маскированный. И это контракт.
LengthInch length = 5; // здесь значение в дюймах или сантиметрах?
В языке, поддерживающем единицы измерения для числовых переменных - должно посылать с сообщением вида "Инициализация типизированного варианта значением без размерности".
Если эмулировать - то дожно быть что-то в духе
LengthInch length = LengthInch.ofUnitless(5);
Так а что это меняет? цифра 5 - может также быть как дюймами, так и сантиметрами и также без ошибок передастся в функцию.
Так а что это меняет?
Заставляет вспомнить программиста "а в чем мы тут считаем-то?"
А в процессе вычислений - мешает складывать сантиметры с дюймами, потому что типы не совпали. Ну или автоматически переводит одно в другое, если методы перевода написали.
Т.е. в языке без типов
int velocityOfCar = 1 // в метрах секунду
int velocityOfPedestrian = 2 // в футах секундв
// тут много кода и входа-выхода из функций
// а тут уже забыли, что единицы не сходятся
carIsFasterThanPedestrian = velocityOfCar > velocityOfPedestrian
В языке, что поддерживает единицы измерения - в сравнении будет ругань.
ну да, а если единиц измерения, помимо футов и метрах, еще есть в ярдах, миллиметрах, километрах, сантиметрах и т.д., то для каждой отдельную переменную и подтип делать? При добавлении новой ед. изм. менять остальной код?
разве не лучше:
enum UnitsMeasurement
{
CM,
MM,
INCH,
FEET,
METER,
// etc.
}
class MyUnit {
uint length;
UnitsMeasurement unit;
public function ToOneUnit(){
// приводим длину к любой стандартной единице
}
}
MyUnit myUnit1 = new MyUnit(5, UnitsMeasurement.INCH);
MyUnit myUnit2 = new MyUnit(5, UnitsMeasurement.CM);
if(myUnit1.ToOneUnit() > myUnit2.ToOneUnit()){
...
}
Компактно, легко понять, легко расширить и модифицировать, аккуратная архитектура. Чем хуже подтипов?
Так тоже можно, да. Тут оно работает потому что и то и то - мера расстояния. Т.е. именно различные единицы одного и того же.
А присвоение копеек к метрам как ругаться?
Почему "Так тоже можно, да.", а не "Так лучше, да."? - в случае с классом, мы имеем легко читаемый, расширяемый и модифицируемый один тип переменной, а не 150 разных типов, 150 перегрузок, переписывание кода при изменении/добавлении ед. изм. и т.д.
А присвоение копеек к метрам как ругаться?
У вас примеры, не имеющие отношения к реальности. Никто в здравом уме, не будет сравнивать теплое с мягким. Ну а если такой найдется, то никакие подтипы/проверки/валидации уже не помогут.
Никто в здравом уме, не будет сравнивать теплое с мягким.
Перепутать местами два целочисленных аргумента где первый означает что-то одно, а второй - другое, вроде бы, не такая уж редкая ошибка. То самое присвоение метров к копейкам и получается в момент передачи аргумента.
То что в существующих языка оно все порядком неудобно получается - с этим согласен. Потому и говорил, что жалко, что единицы измерения языками нормально не поддерживаются.
ну да, а если единиц измерения, помимо футов и метрах, еще есть в ярдах, миллиметрах, километрах, сантиметрах и т.д., то для каждой отдельную переменную и подтип делать?
Если нет явного требования запретить сравнения длин в разных единицах, то смысла заводить по отдельному классу для каждой единицы измерений нет - но никто такого и не предлагал делать.
Lengh{uint, LenghUnit}
будет куда удобнее, а если его чуть более развить, но и никакого ToOneUnit
не нужно, а вместо этого определить некий IsGreater
, и пусть оно внутри уже само разбирается.
Компактно, легко понять, легко расширить и модифицировать, аккуратная архитектура. Чем хуже подтипов?
Ничем не хуже, а наоборот.
Вы скорее всего смешали аргументацию про тип ImageID
с про тип Dimension
из вообще другого комментария, от того и сомнения.
Вы скорее всего смешали аргументацию
Это я немного виноват, хотел показать, что запретить совместную работу длины в футах и длины в метрах - хорошая идея.
Потому что и то и то есть нормальное число и работает как число во все местах где математика есть. Те. после компиляции информация о типе теряется и остается просто арифметика.
А так его реально и делают, только чуть иначе: через темплеты или дженерики, где что есть. В качестве готовой реализации можно посмотреть на std::chrono из C++. Ну или Length<inch> и Length<millimeter> в вашем случае. Можно добавить конверсию между ними, например
template<> double Convert<Inch, Millimeter>(double ivalue) {
return ivalue * 2.54;
}
и где запросят конверсию (явно или неявно) - она будет вызвана через такую функцию.
Немного информации к сведению. Из условно мейнстримных языков F# поддерживает единицы измерения довольно давно (лет 15 как минимум) и ругается, если их перепутать. Но вывод типов работает в этом случае с перебоями, почти всё нужно указывать явно.

отвечу также - а почему не должно?
Потому что идентификатор пользователя - это на самом деле не настоящий номер, и любые математические операции с ним бизнес-смысла не имеют.
а почему не так:
Image? findImageBy(uint ImageId)
Image[] findImagesBy(uint UserId)
Потому что можно вызвать findImagesBy(image.id), и будет баг. А ещё потому что я могу добавить ещё какой-нибудь findImagesBy(ImageGroupID id)
- а вам нужно будет в названии метода перечислять все предикаты, как это в Spring Data делается, и в противном случае диспетчеризация сломается.
программист знает, что передает, куда передает и какой ожидается результат.
Ну, так и запишем - в мире нет ни одного программиста. Потому что если бы это верно было, то в программах никогда не было бы багов.
код переусложняют лишними классами, интерфейсами и прочими не нужными конструкциями
Напротив, мы обсуждаем не "пере"-усложнение кода, а насыщение его дополнительной документацией, в которой сущности, которые имеют явный бизнес-смысл - действительно помечены соответствующим типом, и с ними можно совершать только заранее предусмотренные при разработке операции с ожидаемым результатом. Код от этого не становится ни сложнее, ни проще, но упрощается дальнейшая разработка и поддержка.
а в чем упрощения не пояснили
Как это не пояснил, когда уже третье (или четвёртое?) сообщение на разные лады повторяю одно и то же пояснение?
для сравнения н-р ширины, нужно немного клея
Не понял, что такое н-р, но клей-то действительно нужен, особенно для сравнения 200em с 1920px - в вашем языке почти гарантированно нет возможности сравнивать такие вещи из коробки, а значит кто-то их должен был написать. И клей, если читать внимательно, я предложил только для конкретного особого случая. А в другом случае, предположенном в том же комментарии - я предлагаю, что не надо никакого специального кода для написания сравнения размеров окна и размеров картинки, потому что и то и другое в принципе можно выразить и общим типом Dimension.
Я уверен, что подтипы сделали не просто так и где-то они нужны, но точно не для упрощения кода, а скорее для безопасности.
Если я где-то сказал, что такой подход упрощает код - я оговорился. Но, вроде бы, я и не говорил именно про упрощение именно кода. Разработку такой подход точно упрощает, и будущую поддержку тоже.
Перепутать местами два целочисленных аргумента где первый означает что-то одно, а второй - другое, вроде бы, не такая уж редкая ошибка. То самое присвоение метров к копейкам и получается в момент передачи аргумента.
На самом деле редкая - ошибки обычно не в передаче не того параметра в метод, а в самой реализации метода. Если вы передадите не тот параметр, то это вызовет или ошибку при запуске или результат будет сильно неожиданным и при первой же отладки, сразу станет все ясно.
Если нет явного требования запретить сравнения длин в разных единицах, то смысла заводить по отдельному классу для каждой единицы измерений нет - но никто такого и не предлагал делать.
Я и не предлагаю заводить по отдельному классу - класс один.
Lengh{uint, LenghUnit} будет куда удобнее, а если его чуть более развить, но и никакого ToOneUnit не нужно, а вместо этого определить некий IsGreater, и пусть оно внутри уже само разбирается.
Ну это уже тонкости реализации, ни на что не влияющие. Можно хоть так, хоть этак, хоть оба метода использовать когда нужно.
Потому что идентификатор пользователя - это на самом деле не настоящий номер, и любые математические операции с ним бизнес-смысла не имеют.
Ну с ИД пользователя возможно и не имеют, а с деньгами, н-р (например), вполне могут иметь.
А ещё потому что я могу добавить ещё какой-нибудь findImagesBy(ImageGroupID id) - а вам нужно будет в названии метода перечислять все предикаты, как это в Spring Data делается, и в противном случае диспетчеризация сломается.
Перезагрузка. А если это не ИД, а ед. измерения или валюты - сколько будет таких перезагрузок?
findImagesByGroupId(uint id) - ничуть не хуже, все понятно. Какие это предикаты мне придутся перечислять?
Ну, так и запишем - в мире нет ни одного программиста. Потому что если бы это верно было, то в программах никогда не было бы багов.
Т.е. вы не знаете, что передаете, куда передаете и какой ожидается результат от функции? Как написал выше, ошибки в основном в самой реализации функции, а не в передаче не тех параметров (которые отлавливаются при первом запуске).
Напротив, мы обсуждаем не "пере"-усложнение кода, а насыщение его дополнительной документацией...
Ну да, 150 перезагрузок, 150 разных типов переменных, вместо одного класса и нескольких функций очень этому способствуют и прям облегчают разработку и делают код чище и проще, особенно если что-то нужно изменить/добавить.
Как это не пояснил, когда уже третье (или четвёртое?) сообщение на разные лады повторяю одно и то же пояснение?
Так не убедительно - слишком все за уши притянуто и выглядит, как защита от дурака при куче не нужных конструкций.
Не понял, что такое н-р, но клей-то действительно нужен, особенно для сравнения 200em с 1920px - в вашем языке почти гарантированно нет возможности сравнивать такие вещи из коробки, а значит кто-то их должен был написать.
function ToOneUnit() - приведет к единым единицам.
Разработку такой подход точно упрощает, и будущую поддержку тоже.
Не соглашусь с вами - чем он упрощает разработку? перезагрузками, кучей разных типов, легкой модификацией? - нет, код становится больше, типов много, при добавлении/изменении типа, нужно менять остальной код - все это сильно повышает вероятность ошибок. А через пол года-год, не работая с проектом, все будет еще сложнее.
Ну с ИД пользователя возможно и не имеют, а с деньгами, н-р (например), вполне могут иметь.
Но при этом, деньга - это не число, а число-и-валюта. И там тоже нельзя просто взять и прибавить, если валюты отличаются.
А если это не ИД, а ед. измерения или валюты - сколько будет таких перезагрузок?
Столько, сколько потребует логика приложения.
findImagesByGroupId(uint id) - ничуть не хуже, все понятно. Какие это предикаты мне придутся перечислять?
"ByGroupId" - это как раз предикат. Упомянутый мной Spring Data из имени методов может запросы генерировать, вот.
Т.е. вы не знаете, что передаете, куда передаете и какой ожидается результат от функции?
Если это не мой код - то нет, сначала придётся всё прочитать. Код, написанный мной год назад - это, в принципе, уже тоже не мой код, при этом.
Как написал выше, ошибки в основном в самой реализации функции, а не в передаче не тех параметров (которые отлавливаются при первом запуске).
В отдельных случаях у вас в функцию передаются UserID и ImageID, которые в рамках теста обе равны единице, потому что база чистая. Первый запуск такое отлавливает не всегда. И даже если отлавливает запуск - это всё равно куда дольше, чем если оно просто сразу же в IDE не скомпилируется.
Ну да, 150 перезагрузок, 150 разных типов переменных, вместо одного класса и нескольких функций очень этому способствуют и прям облегчают разработку и делают код чище и проще, особенно если что-то нужно изменить/добавить.
Если именно в этом коде задействовано 150 разных видов сущностей/переменных, то скрыть их настоящий тип за всякими int
и string
точно разработку сделает сложнее, а не проще.
как защита от дурака при куче не нужных конструкций.
Тестирование - это так-то тоже защита от дурака, ведь у настоящих программистов всё работает и так, ну максимум после второго запуска \srcsm.
А уж сколько для тестирования конструкций всяких придумали...
function ToOneUnit() - приведет к единым единицам.
Её тоже кто-то должен написать, то есть это усложняет код.
при добавлении/изменении типа, нужно менять остальной код
Не нужно, если это новый тип - потому что существующий код о том типе ничего не знает. А при изменении то, что оно компилироваться перестанет - это куда лучше, чем если бы оно молча собралось, и проблемы проявились бы только после "первого запуска".
Но при этом, деньга - это не число, а число-и-валюта. И там тоже нельзя просто взять и прибавить, если валюты отличаются.
Зависит от ситуации: если к примеру, нужно сделать наценку в 2 раза больше, то тип валюты знать не обязательно.
"ByGroupId" - это как раз предикат. Упомянутый мной Spring Data из имени методов может запросы генерировать, вот.
Понял, но у меня здесь это просто часть названия функции, чтобы было понятно какой параметр передать.
Если это не мой код - то нет, сначала придётся всё прочитать. Код, написанный мной год назад - это, в принципе, уже тоже не мой код, при этом.
Безусловно - не зная код, вы ничего не сможете написать.
В отдельных случаях у вас в функцию передаются UserID и ImageID, которые в рамках теста обе равны единице, потому что база чистая. Первый запуск такое отлавливает не всегда. И даже если отлавливает запуск - это всё равно куда дольше, чем если оно просто сразу же в IDE не скомпилируется.
Вернет пустой результат. Но обычно все-таки база содержит необходимые записи, либо используются фикстуры, иначе как вы проверите работоспособность без базы?
Если именно в этом коде задействовано 150 разных видов сущностей/переменных, то скрыть их настоящий тип за всякими
int
иstring
точно разработку сделает сложнее, а не проще.
В том-то и дело, что используется несколько простых классов (https://habr.com/ru/articles/874584/comments/#comment_27813296), а не на каждый чих по новому типу переменной. Например класс MyUnit для ед. измерения, MyMoney для хранения всех валют и т.д.
Её тоже кто-то должен написать, то есть это усложняет код.
нет - она будет абсолютно простая (с элементарной логикой), где разберется даже ребенок (ссылка на несколько строк выше)
Не нужно, если это новый тип - потому что существующий код о том типе ничего не знает. А при изменении то, что оно компилироваться перестанет - это куда лучше, чем если бы оно молча собралось, и проблемы проявились бы только после "первого запуска".
помимо создания нового типа, вам его еще надо как-то обрабатывать, добавлять перегрузки, менять уже существующий код и т.д., а если использовать н-р класс MyUnit, то просто добавить строку в перечисление и простой расчет в функцию ToOneUnit(), ну или IsGreater() в том же классе.
И в целом, большой разницы нет - будут ли ошибки во время компиляции или во время первого запуска. Код компилируются обычно перед запуском.
Вернет пустой результат
Или не пустой, потому что для значения uint есть и пользователь, и картинка, и что-то найдётся даже если аргументы перепутать местами. Ну подумаешь, чужие картинки кому-то показали.
Например класс MyUnit для ед. измерения, MyMoney для хранения всех валют и т.д.
Откуда тогда взялось 150 классов?
нет - она будет абсолютно простая
Ну уж точно сложнее, чем реализация ImageID в лоб.
помимо создания нового типа, вам его еще надо как-то обрабатывать, добавлять перегрузки, менять уже существующий код и т.д.
Ну да, типа того. А часто у вас новая фича состоит из добавления свойства в класс и всё? Существующий код менять всё равно нужно.
большой разницы нет - будут ли ошибки во время компиляции или во время первого запуска.
О, нет - это огромная разница по времени. Особенно в случае когда первый запуск будет только с CI, потому что машина разработчика такое приложение не потянет по любой причине.
Код компилируются обычно перед запуском.
В современных IDE код компилируется инкрементально практически постоянно, параллельно с его изменением.
В современных IDE код компилируется инкрементально практически постоянно, параллельно с его изменением.
Как это от IDE-то зависит? Что вообще такое «современная IDE»? Что происходит в «современных IDE» с кодом на перле, руби, питоне, джаваскрипте? Если вы только про джаву — то код компилировался непрерывно еще в Forte (позднее — NetBeans) — 25 лет назад.
Что происходит в «современных IDE» с кодом на перле, руби, питоне, джаваскрипте?
Точно помню, что Eclipse и плагин JSDT, земля ему пухом, при написании кода иногда всё-таки показывали очевидные ошибки, которые на таком уровне были ему доступны. Сейчас там плагин WWD, и последний раз как я им пользовался, там не работали даже хоткеи для комментов, плевался очень много и сильно, особенно после того как сходил к ним в багтрекер и увидел их отношение к происходящему. Про остальные языки не в курсе, из них я только на питоне в блокнотике ещё писал, но блокнотик и не IDE, всё-таки.
Если вы только про джаву — то код компилировался непрерывно еще в Forte (позднее — NetBeans) — 25 лет назад.
Я точно помню, как на той самой Java, когда я учился на ней программировать, ошибки компиляции видел только вон как там вверху писали, перед запуском. Ещё и не все, а только одну, самую первую найденную. Но дело точно было не 25 лет назад, а меньше, и с тех пор всё точно стало значительно лучше, о чём и речь.
Вон даже Typescript появился, где реально можно отловить проблему с типизацией ДО того, как оно у пользователей взорвётся.
В отдельных случаях у вас в функцию передаются UserID и ImageID, которые в рамках теста обе равны единице, потому что база чистая.
Или не пустой, потому что для значения uint есть и пользователь, и картинка, и что-то найдётся даже если аргументы перепутать местами. Ну подумаешь, чужие картинки кому-то показали.
вы уже сами определитесь, чистая база или нет. в случае чистой базы вернет пустой результат, в случае если в функцию получения картинки по ИД, передать ИД пользователя - само собой, если ИД найден, то будет показана чужая картинка.
Откуда тогда взялось 150 классов?
я такого не говорил - наоборот, нужен всего один класс на N количество разных типов.
Ну уж точно сложнее, чем реализация ImageID в лоб.
как вы это поняли?
Ну да, типа того. А часто у вас новая фича состоит из добавления свойства в класс и всё? Существующий код менять всё равно нужно.
в данном случае еще метод ToOneUnit() поправить в том же классе и все.
О, нет - это огромная разница по времени. Особенно в случае когда первый запуск будет только с CI, потому что машина разработчика такое приложение не потянет по любой причине.
что за приложение и как делают его отладку?
что за приложение и как делают его отладку?
Любое высоконагруженное приложение довольно бессмысленно запускать на своей машине, нужно запускать на целом зоопарке CI и смотреть бенчмарки.
Любую библиотеку общего пользования довольно бессмысленно запускать на своей машине — надо запускать сто разных приложений с разными вариантами использования, можно и у себя извратиться, но CI в тысячу раз удобнее.
Любое приложение, которое зависит от нескольких сторонних сервисов: у себя в тестах это моки, но без стендового испытания в продакшн не выкатываются.
Любое кластерное приложение.
Сиречь, вообще почти любое приложение, которое чуть сложнее физзбаза (магазина на похапе).
Я уже лет 10 вообще у себя окружение для разрабатываемых приложений не разворачиваю: проперти-тесты локально, а дальше — в CI, оно железное, пусть оно мне и расскажет, что не так. Такой подход, кстати, как выяснилось, значительно повышает качество кода первой попытки и помогает не держать на своей машине всякое говнище, типа докера.
вы уже сами определитесь, чистая база или нет.
Чистая, после trunc. Потом тестом (мы же тесты пишем?) туда добавляется два пользователя, и у каждого по картинке. Ну так как у нас ID там в обоих случаях 1 и 2, и пользователю 1 принадлежит картинка 1, то сами догадаетесь, как именно перепутанные аргументы могут пройти через сито и оказаться в проде?
нужен всего один класс на N количество разных типов.
Ну вот, а я говорю, что идентификатор сущности - это не число, а на самом деле динамическое перечисление (enum), и числами его выражать хоть и проще, но на самом деле всё равно неправильно. И даже много раз объяснил, к каким проблемам выражение через числа может приводить.
как вы это поняли?
Потому что реализация класса ID - это примерно четыре ключевых слова и четыре скобки в Java, и три ключевых слова если лениться и взять язык с алиасами. А для функции-конвертера понадобится раза в два больше токенов, даже если единиц измерения всего две.
в данном случае еще метод ToOneUnit() поправить в том же классе и все
Нет, не всё. Ещё по уму нужна будет конвертация из OneUnit во все оставшиеся. Но тут как раз прояснилась интересная деталь - почему-то вы думаете, будто я зачем-то предлагал на каждую новую единицу измерения длины делать новый класс. Хотя я неоднократно уже говорил, что ничего подобного не было.
что за приложение и как делают его отладку?
Пусть это будет микросервис на C++, с покрытием тестами на уровне порядка 60% ветвлений (для виденных мной проектов - цифра космическая, на самом деле). Ну или на Scala, с теми же характеристиками. Отладка, как водится, ручками через дебаггер.
покрытием тестами на уровне порядка 60% ветвлений
Откройте для себя Property Based Testing, и цифра 60% станет синонимом «в районе нуля».
Для моего проекта вряд ли сходу подойдёт, потому что тестировать надо не каждую комбинацию возможных входных данных, а сценарии действий пользователя, в том числе с имитацией течения времени.
Плюс сама система немаленькая, и у неё куча разных крутилок, которые тоже на всё будут влиять - по моей предварительной оценке, вариантов там может быть столько, что один полный suite займёт больше, чем спринт у разработки.
Чистая, после trunc. Потом тестом (мы же тесты пишем?) туда добавляется два пользователя, и у каждого по картинке. Ну так как у нас ID там в обоих случаях 1 и 2, и пользователю 1 принадлежит картинка 1, то сами догадаетесь, как именно перепутанные аргументы могут пройти через сито и оказаться в проде?
Выше же написал.
Как же тогда web то работает? где выводятся куча изображений, данных пользователя, счетов и т.д. - и все это работает без ошибок и подтипов.
Потому что реализация класса ID - это примерно четыре ключевых слова и четыре скобки в Java, и три ключевых слова если лениться и взять язык с алиасами. А для функции-конвертера понадобится раза в два больше токенов, даже если единиц измерения всего две.
Сам класс маленький, его легко менять/изменять и все в одном месте. Какой подтип/подтипы вы сделаете, н-р, для денег (и 15 валют)?
Нет, не всё. Ещё по уму нужна будет конвертация из OneUnit во все оставшиеся. Но тут как раз прояснилась интересная деталь - почему-то вы думаете, будто я зачем-то предлагал на каждую новую единицу измерения длины делать новый класс. Хотя я неоднократно уже говорил, что ничего подобного не было.
Речь шла не про новый класс, а про новый тип, который нужен н-р для См. и Дм., а класс-то как раз один.
Пусть это будет микросервис на C++, с покрытием тестами на уровне порядка 60% ветвлений (для виденных мной проектов - цифра космическая, на самом деле). Ну или на Scala, с теми же характеристиками. Отладка, как водится, ручками через дебаггер.
ну так, дебаг-то запускается при запуске приложения/сервиса, пэтому большой разницы нет - будут ли ошибки во время компиляции или во время первого запуска.
Как же тогда web то работает?
Ну в смысле "как". На костылях, и после долгой работы над отлавливанием багов по всему стеку. Как и любое ПО. И подход с более вовлечённой типизацией полностью всех проблем не решит, только некоторые.
Какой подтип/подтипы вы сделаете, н-р, для денег (и 15 валют)?
Думаю, что-то типа Monetary{Decimal, Currency}
.
Речь шла не про новый класс, а про новый тип, который нужен н-р для См. и Дм.
И почти сразу уже решили, что ничего подобного на самом деле не нужно, а нужен чуток специализированный Quantity{Amount, Unit}
. Я правда не понимаю, зачем вы продолжаете эту нить развивать.
дебаг-то запускается при запуске приложения/сервиса, пэтому большой разницы нет - будут ли ошибки во время компиляции или во время первого запуска.
Ну что вы, разница огромна. Как я уже сказал, множество разработчиков работают в условиях инкрементальной компиляции, а вот время первого запуска конкретной функции (не всего приложения) может отличаться от времени написания этой функции на много минут или даже часов.
Ну в смысле "как". На костылях, и после долгой работы над отлавливанием багов по всему стеку.
откуда это вы такую статистику взяли?
Ну что вы, разница огромна. Как я уже сказал, множество разработчиков работают в условиях инкрементальной компиляции, а вот время первого запуска конкретной функции (не всего приложения) может отличаться от времени написания этой функции на много минут или даже часов.
т.е. вы несколько часов пишете код и только потом запускаете функцию, чтобы проверить работает она или нет? И в случае неправильного результата, начинаете отладку всего того, что написали за несколько часов? что же за функции такие? По мне, так контрпродуктивно отлаживать большое количество кода за раз, вместо отладки по частям. Я работаю так: написал часть кода, проверил: работает - забыл, не работает - исправил, забыл. Перешел к следующей части кода.
Я так понимаю, спор зашел в тупик. О чем спорить, если нет никакого реального куска кода (с F# не знаком) с реализацией и использованием подтипов.
откуда это вы такую статистику взяли?
Из базы CVE.
т.е. вы несколько часов пишете код и только потом запускаете функцию, чтобы проверить работает она или нет?
Это от кода зависит. Иногда и так тоже, да. В моём проекте, к тому же, нельзя просто взять и запустить функцию в вакууме, обвязки вокруг запуска тоже кому-то нужно писать. А иногда на обвязки как раз времени-то и нет.
Из базы CVE.
CVE (Common Vulnerabilities and Exposures) — база данных общеизвестных уязвимостей информационной безопасности.
какое отношение она имеет к вашему утверждению по поводу работы всего web'a: "На костылях, и после долгой работы над отлавливанием багов по всему стеку" и ошибками при неправильной передаче параметров? Можете носом ткнуть?
Уязвимости в том числе вызваны багами, недостаточной внимательностью при разработке, недостаточными проверками границ массивом, и всем прочим.
Проще говоря, CVE существуют из-за того, что на планете нет ни одного "настоящего програмиста".
это я и так знаю, мне интересно, как вы определили, что весь web работает "На костылях, и после долгой работы над отлавливанием багов по всему стеку " )) Я где-то говорил, что программисты не делают ошибок?
Ну а как же ещё объяснить вашу классификацию программиста (в противовес непрограммистам) как человека, который
знает, что передает, куда передает и какой ожидается результат.
?
Если с такими вводными в софте всё равно случаются баги - этому есть только два объяснения:
весь софт написан непрограммистами
программисты сознательно добавляют баги в софт
Думаю, что-то типа
Monetary{Decimal, Currency}
.
О-о-о-о :)
Люди делятся на тех, кто с деньгами работал, и тех, кто предлагает хранить суммы числами.
А как мы разрулим ситуацию: «три человека скинулись и купили 100 долларов, а потом поссорились и им надо их поделить поровну, а потом помирились — и им снова из трех слагаемых надо получить $100»?
В оманском риале — 1000 байз, а в иене — только иена, разменных денежных единиц в Японии нет. И так далее, и тому подобное :)
Люди делятся на тех, кто с деньгами работал, и тех, кто предлагает хранить суммы числами.
Я не претендую на роль истины или человека, который знает всё. Но что конкретно плохого в выборе Decimal?
А как мы разрулим ситуацию: «три человека скинулись и купили 100 долларов, а потом поссорились и им надо их поделить поровну, а потом помирились — и им снова из трех слагаемых надо получить $100»?
А как бы вы хранили в базе данных иррациональные числа? Честная запись у числа из вашего ответа вообще-то бесконечная. И решать такую проблему можно в том числе так же, как она решалась бы в жизни - кому-то может достаться на один цент больше, или, если повезёт, каждому вместо одного цента достанется экземпляр валюты, которая в моменте позволяет разделить один цент ровно на три (правда, тогда теряется гарантия, что потом это можно будет собрать обратно именно в сто долларов, а не чуть больше или меньше).
В оманском риале — 1000 байз, а в иене — только иена, разменных денежных единиц в Японии нет. И так далее, и тому подобное :)
Эта информация как-то повлияет на тип Decimal? По-моему, такие вещи нужно будет обрабатывать как-то ещё, например, в логике операций. Хотя если настаиваете - договоритесь с @Cels,что для каждой валюты нужен будет свой отдельный класс с правилами.
⅓ — не иррациональное число. Это во-первых. Во-вторых, во многих языках есть тип «рациональное число» (Rational in ruby). В-третьих, «достаться на один цент больше» нельзя, потому что потом эти трети (если они — акции) — могут вырасти в миллиард раз, сделав дележ слишком уж неравным.
Эта информация как-то повлияет на тип Decimal?
На тип нет, но на перевод из разменных единиц в неразменные — повлияет, 200 центов — это 2 бакса, а двести иен — это двести иен.
⅓ — не иррациональное число
Ваша правда, забыл уже эти классификации.
Во-вторых, во многих языках есть тип «рациональное число»
В руби-то, может, и есть - а в базе данных? Вот mysql, к примеру, как вроде одна из самых популярных, такой роскошью не обладает, да и в Postgres для этого специальные расширения нужны (и наверняка ещё своя особенная боль от отсутствующей поддержки в binding'ах).
Ну если более реально смотреть, то, конечно, можно для денег сразу выделять три поля с целочисленным типом, чтоб хотя бы рациональные числа хранились точно.
нельзя, потому что потом эти трети (если они — акции) — могут вырасти в миллиард раз, сделав дележ слишком уж неравным.
В этом случае тоже потеряется возможность слить их обратно вместе и получить ровно сто долларов. Но с другой стороны, если целью станет сохранить порядок роста - то скорее именно подход с получением одним человеком на цент больше позволит это сделать, так как не произведёт никаких дробных.
На тип нет, но на перевод из разменных единиц в неразменные — повлияет, 200 центов — это 2 бакса, а двести иен — это двести иен.
Но в том сообщении никто и не обсуждал перевод из разменных единиц в неразменные, вы были первый.
Хранить лучше всего простой строкой, которая при записи дублируется хоть вот Decimal
, — для быстрых неточных расчётов.
подход с получением одним человеком на цент больше позволит это сделать, так как не произведёт никаких дробных
Пардон? ⅓×миллиард — это треть миллиарда, после отбрасывания дробного цента — у всех поровну.
33×миллиард и 34×миллиард — дают разницу в миллиард.
Но в том сообщении никто и не обсуждал перевод из разменных единиц в неразменные, вы были первый.
Программистское мышление в себе нужно убивать. Зачем вообще хранить деньги, если с ними нельзя сделать одну из самых распространенных операций с деньгами?
Хранить лучше всего простой строкой, которая при записи дублируется хоть вот
Decimal
, — для быстрых неточных расчётов.
Строкой бесконечной длины, что ли?
Или хранить 0.(3)
? Но это требует специального парсера, который знает про все эти 22/7. И зачем заморачиваться дописыванием парсера, если то же самое решается хранением "настоящего 9¾" - в трёх связанных целочисленных полях - [9, 3, 4]?
Пардон? ⅓×миллиард — это треть миллиарда, после отбрасывания дробного цента — у всех поровну.
Но и миллиард не делится на три нацело (и олимпиард не делится). Либо у кого-то останутся "дополнительные" доллары, либо вам всё-таки надо будет хранить синтетическую треть цента, которую никак не будет возможно обналичить. Да, по отношению к двум людям из трёх это "нечестно", потому что у них будет на цент меньше на момент раздела. Зато чем позже раздел, тем меньше разница. Пусть живут дружно, как завещал Кот Леопольд, я не знаю.
Программистское мышление в себе нужно убивать. Зачем вообще хранить деньги, если с ними нельзя сделать одну из самых распространенных операций с деньгами?
Физическую банкноту в сто долларов на три части вы можете поделить либо ножницами, либо в одной кучке останется 34 цента вместо 33х. Равно как и 0.(3) цента вы никуда не денете, потому что это не настоящие деньги, а математический/программистский артефакт. Все прочие операции вполне можно кодировать либо типом Decimal, либо тем франкенштейном из трёх целых чисел.
Строкой бесконечной длины, что ли?
Я так понял, речь идет именно о хранить - если никаких операций не делаешь а просто туда-сюда передаешь, то строчкой (возможно, обернутой в какой-то тип) - вероятно лучше. Вероятность того, что где-то что-то испортится, меньше.
Я так же по поводу дат считают - пока арифметики с ними нет, нет смысла ко всяким системным и не очень типам приводить, теряя время на парсинг и рискуя нарваться на ошибки и особенности работы с датами в конкретной системе (вдруг базу данных таймзон обновить забыли? Будут странности).
Я так же по поводу дат считают - пока арифметики с ними нет, нет смысла ко всяким системным и не очень типам приводить
Смысл есть - потому что обычно от вашей системы будут ожидать, что вы таки не принимаете дат "25-15-2025", независимо от особенностей работы с датами где угодно (мы же предполагаем ISO-шную систему, а не любую из альтернатив? даже если нет, то это где-то придётся выразить, и почему не в типах данных?). И ещё что вы сортировку по датам сравнительно честную будете делать, а не лексическую в зависимости от того, как эту дату вводил пользователь. Да и просто даты пользователю смотреть будет удобнее, если они все будут в общем форматировании, даже если вводились как придётся. Ничего из перечисленного сделать нормально не получится если их как-то когда-то не парсить. Ну а если вы чаще показываете даты, чем их сохраняете в store - то эффективнее их будет парсить при сохранении, и сохранять таки в соответствующих системных типах.
вдруг базу данных таймзон обновить забыли?
Стирать из своей системы важные для пользовательского опыта метаданные только потому, что девопсы могут забыть подложить правильный tzdata - это, по-моему, покруче всякого религиозного следования SOLID.
Стирать из своей системы важные для пользовательского опыта метаданные только потому, что девопсы могут забыть подложить правильный tzdata - это, по-моему, покруче всякого религиозного следования SOLID.
Увы, мне лично пришлось ловить, в каком именно месте по цепочке обработки через несколько рабочих мест, серверов и микросервисов день рождения человека сдвигался на один день как раз из за того, что он многократно парсилось из строк (XML) в системные типы и обратно. И потом на одном из шагов как раз "полночь 12-го числа" становилась "11-ым" как раз из за различия в понимании летнего/зимнего времени.
Но зачем же проблему чинить не там, где она возникает?
День рождения человека, скажем - это не момент его рождения, так что зачем вообще в этих рассуждениях использовать слово "полночь", и давать этой дате какое-то время, да ещё и отбрасывать его потом? По-моему, проблема ваша была на самом деле в этом, а не в опоздавшем обновлении tzdata.
Красивый пример, но исходная проблема именно в том, что дату пытались обрабатывать как время. Если что-то записано в виде 2005-11-02, то читать его как 2005-11-02T00:00:00 было уже принципиальной ошибкой чтения. Да, это тоже вопрос правильной типизации. В базах данных, например, поэтому типы данных date, time и datetime попарно различны.
Если что-то записано в виде 2005-11-02, то читать его как 2005-11-02T00:00:00
Ну так. Только язык может способствовать ошибкам. 'new Date("2005-11-02")' в JS вот что делает? Или в Java тот же Date, который очень настоятельно рекомендуют использовать по минимуму, но оно иногда встречается. Поэтому, чтобы не усложнять ситуацию - лучше в тех случаях, когда обрабатывать не нужно - именно это и делать, оставляя все строкой и передавая эту строку дальше 'как есть'.
А по поводу базы таймзон, которую админы не обновили - в Андроиде, например, на момент, когда это еще актуально было, не было Самарской таймзоны. И смещение летнее/зимнее время, соответственно, работало неправильно. Был, скажем период, когда смещение совпадало с московским (и приходилось так и ставить - зоны то нет). Но это приводило к тому, что дата/время, проводимое к тому же UTC, если оно было до момента, когда это ввели -- приводилось неправильно.
Поэтому, чтобы не усложнять ситуацию - лучше в тех случаях, когда обрабатывать не нужно - именно это и делать, оставляя все строкой и передавая эту строку дальше 'как есть'.
Если она соответствует канону представления - да. Иначе - нет. Передавая как есть непроверенный внешний ввод, вы получаете другую проблему - даёте его туда, где ожидают уже проверенные корректные данные, и может случиться реально что угодно, вплоть до тяжёлых проблем секьюрити.
Вот где-то мелькал пример, как выписали ребёнку свидетельство о рождении с датой 31 ноября, и пока он не пошёл в садик, никто не заметил проблемы. В компьютер ввести такое было нельзя. Или проще: 9/11/2001 это 9 ноября или 11 сентября 2001? Сейчас все грамотные наверняка знают, что второе, но количество странных представлений дат, включая культуры, где не dd-mm-yy или dd.mm.yy, а dd/mm/yy, зашкаливает способности автоматического опознания.
Поэтому разбор с проверкой на границе зоны ответственности за корректность данных - обязателен. А дальше - если переводится обратно в строку, а не в выделенный тип Date, и в таком виде бегает между компонентами - я не против, это уже вопрос внутренней политики.
Вот где-то мелькал пример, как выписали ребёнку свидетельство о рождении с датой 31 ноября, и пока он не пошёл в садик, никто не заметил проблемы. В компьютер ввести такое было нельзя.
Это, скорее, пример в мою пользу. Хотя и экотический Потому что потом 'написано 31 ноября - значит, в документах пишем 31 ноября. А ваш комп - дурак, что этого не понимает и программисты злодеи, что этого не предусмотрели'.
И да, исправление документа -- не поможет. Потому что запись/версия устаревшего документа - в системе должна остаться же.
написано 31 ноября - значит, в документах пишем 31 ноября. А ваш комп - дурак, что этого не понимает и программисты злодеи, что этого не предусмотрели
jIMHO, значительно вероятнее "это свидетельство невалидно, идите получайте правильное".
И да, исправление документа -- не поможет. Потому что запись/версия устаревшего документа - в системе должна остаться же.
Это если его такого вообще внесут. В чём я сомневаюсь.
Но при этом в Java уже давно JSR310, и его предшественник JodaTime. А в JS - js-joda (только что нашёл). Негусто, конечно, но что поделать - видимо, разработчикам на JS и так нормально было (я сам вживую до этого видел только datejs и momentjs, но это вообще не то).
Поэтому, чтобы не усложнять ситуацию - лучше в тех случаях, когда обрабатывать не нужно
Как я уже говорил, полное отсутствие обработки - это фантастика. Должны же вы проверять эти данные, что там действительно даты? Хотя если конкретно ваше приложение нужно только как труба для значений, то там и строки не нужны - гораздо точнее будет всё принимать и отправлять в виде бит-массива. Вот уж где точно не будет никакой возможности что-то по дороге испортить неверной типизацией.
в Андроиде, например, на момент, когда это еще актуально было, не было Самарской таймзоны
Насколько я знаю, JodaTime таскает Tzdata с собой, как раз на такой случай.
из за того, что он многократно парсилось из строк (XML) в системные типы и обратно.
Выглядит так, что чинить надо сериализацию/десериализацию. "Чинить" хранение, если сломан маршаллинг - странное решение, имхо.
Но что конкретно плохого в выборе Decimal?
Непонятно, о каком языке речь, но, например, в обоих C# и Python - Decimal это плавучка, хоть и десятичная. В C# точность фиксирована на 28 значащих цифр. В Python регулируется контекстом. В обоих переполнение доступного места на цифры вызовет округление. А округление это то, что нельзя допускать в финансах кроме явных мест: все балансы должны сходиться в чистый незамутнённый ноль. Лучше переполнение и явный вылет по нему, чем тихое округление долей.
Поэтому "серьёзные" финансы все работают в фиксированной точке. Если минимальная единица представления - копейка, то будет храниться целое число копеек. Если 1/100 копейки, то соответственно 1 рубль будет представлен числом 10000. Ну и так далее.
Шаблонный тип для представления денег в этом случае получает как минимум два параметра - тип самой валюты и дополнительное количество цифр (считаем, у большинства - десятичных) от его базы. Для рубля это будет коэффициент 100.
Бывают более сложные случаи. До ≈1971 в Великобритании было, что в фунте стерлингов 20 шиллингов, в шиллинге 12 пенни (говорят также "пенсов"), в пенни 4 фартинга. Тогда 1 фунт это 960 базовых единиц размером в фартинг. (Фартинги отменили где-то раньше, но я их для полноты картины приведу таки.) В поттериане в галлеоне 17 сиклей, в сикле 29 кнатов. Видимо, Ро хотела простебать эти древние подходы. Ах да, пока были гинеи, в них был 21 колониальный шиллинг, в отличие от фунта с 20 шиллингами метрополии. Но это я ушёл в сторону.
Десятичную плавучку в аппаратуре сделала только IBM (в линиях Z и P) и до сих пор реально непонятно, накойхер она такая. Опять же, физике нужна двоичная плавучка, а финансам нужна десятичная фиксированная точка. Места для десятичной плавучки не остаётся, или я чего-то не вижу. Кто знает осмысленное применение - прошу подсказать.
Итого, возвращаясь, мы имеем инстанциации шаблонных типов как Money<Ruble,100>
, Money<Hryvnia,100>
, Money<Yen,1>
, Money<Rial,1000>
... вот где-то с этим можно уже работать.
Непонятно, о каком языке речь, но, например, в обоих C# и Python - Decimal это плавучка, хоть и десятичная.
Поэтому "серьёзные" финансы все работают в фиксированной точке.
Однако я смотрю на доку по этому самому Decimal и вижу (выделение мое) "decimal — Decimal fixed-point and floating-point arithmetic"
Я где-то термин не так понял или они врут?
Ну вот цитата оттуда же:
A. Some operations like addition, subtraction, and multiplication by an integer will automatically preserve fixed point. Others operations, like division and non-integer multiplication, will change the number of decimal places and need to be followed-up with a
quantize()
step:
И ещё чуть ниже:
In developing fixed-point applications, it is convenient to define functions to handle the
quantize()
step:>>> def mul(x, y, fp=TWOPLACES): return (x * y).quantize(fp)
То есть собственный режим это плавающая точка (определено только количество точных значащих цифр, но не квант точности), но фиксированную точность можно получить дополнительными операциями. Костыль предоставлен, пользуйтесь на здоровье.
"Чистая" fixed point выполняла бы это сама.
Про деление - это, наверное осмысленно для уменьшения сюрпризов в вычислениях. Если зафиксировать "2 знака после запятой", то 0.01 / 10 * 10 - это надо быть очень большой внимательностью или занудностью, чтобы хотеть получить исключение или 0.00. А тем, кому так и надо и они это понимают - дополнительные операции пусть делают. Чтобы те, кто не понимают, тоже поняли, что происходит.
Да, естественно, надо понимать, до каких пределов эта самая fixed point и в каком варианте работает, и что для этого нужно. Ваш пример очень простой, я предлагаю взять что-то вроде "взять 1.5% налога на малиновые штаны от суммы в копейках". При идеальной точности и округлении до копеек, от 33 копеек это будет ещё 0, от 34 - одна копейка. И это надо правильно вычислять, и тут вопрос, как именно это делать. Если мы, например, при двух цифрах после точки (запятой) умножим на 1.5 и округлим, получим 49.5 округлённое до 50. Разделив на 100 и снова округлив по стандартному _у нас_ для финансовых расчётов round-half-up, получим 1 копейку. Уже неверно, должно быть ещё 0. Привет, двойное округление и его последствия. И вот тут промежуточный результат, получается, должен быть ещё в честной плавучке и с запасом точности, а округление (например, через quantize, в этом модуле) - только финально. Это простейший случай, бывают и хитрее.
В доке по модулю умножение и деление упомянуто "в пику" сложению и вычитанию, при которых квант не меняется (по крайней мере пока не будет переполнение за пределы значащего при сложении). Это ещё одна загвоздка, ещё проще и банальнее, чем предыдущая. Пусть у нас 7 цифр:
import decimal
c = decimal.getcontext()
c.prec = 7
n1 = decimal.Decimal('9999999')
n2 = decimal.Decimal('4')
print(n1+n2)
оно печатает Decimal('1.000000E+7')... упс, потеряли цифру: 10000000 вместо правильного 10000003. Всё, ваш баланс не сошёлся.
С этим можно бороться, да. Или применить заведомо завышенное количество значащих цифр - а сколько их нужно будет на самом деле? Умолчание у этого модуля 28, это как раз по принципу "педаль в пол, чтобы не плакались даже с зимбабвийскими долларами, коих 2 триллиона на один американский". Но как определить, что и этого количества цифр перестало хватать?
Или включить Rounded trap: `decimal.getcontext().traps[decimal.Rounded] = True` - и получите честное питоновское исключение. На время таких операций он должен быть включен. На время, например, делений при вычислении тех же 1.5% - выключен, при этом имея гарантированный запас точности (проверив значения, что они не выползут за пределы). Включаем, выключаем... I like to move it, move it.
А теперь попробуем понять, какая доля тех, кто реализует эти операции на таком же Decimal, вообще хотя бы раз читала про подобные грабельки - не говоря о том, чтобы постоянно держать их в памяти на каждой операции.
Сложно...
Непонятно, о каком языке речь
Вы не привязывайтесь к языкам. Пусть это будет ваш лично Decimal, который вообще сам по себе - десятчиное рациональное число с реализацией типа "чёрный ящик". Учитывая, что ни C#, ни Python, по вашим же словам, не имеют в стандартной библиотеке типа, который подходил бы для "серъёзных" финансов - вы либо напишете все операции вокруг существующих численных типов так, чтобы они себя хорошо вели, либо введёте новый тип, который эти операции реализует как вам нужно.
Если же нужно хранить точный результат от деления $100 на три - то лучше будет вместо Decimal назвать это дело Rational, и чёрный ящик ему соответствующий подобрать.
Money<Ruble,100>
Это дублирование информации. Нет веских причин не включить число 100 куда-то внутрь определения Ruble
, и отображение настроить соответственно.
Я, правда, малость не понимаю, куда эта ветка начинает уходить.
Вы не согласны с подходом создания типов для реализации доменной логики, или вам просто сильно хотелось меня "макнуть" в пробелы в моих знаниях (о которых я и сам в курсе)?
Это дублирование информации. Нет веских причин не включить число 100 куда-то внутрь определения
Ruble
, и отображение настроить соответственно.
Это зависит. Я работал с американскими биржевиками и у них часть операций определена в центах, а часть (типа комиссии за биржевую операцию) в 1/10000 цента. Это та же валюта, но дискрет таки другой.
Вы не привязывайтесь к языкам. Пусть это будет ваш лично Decimal
Ну вы назвали его именно Decimal, а не просто Money или что-то подобное. Это уже предполагало, IMHO, привязку. Потому я и попросил уточнений.
Вы не согласны с подходом создания типов для реализации доменной логики
Согласен и привёл уточнения к нему, при подтверждении общего подхода.
или вам просто сильно хотелось меня "макнуть" в пробелы в моих знаниях (о которых я и сам в курсе)?
Непонятно, зачем вы пытаетесь защищаться. По-моему, как раз оптимальный метод дискуссии, чтобы получить какой-то конструктивный результат, это попытаться расширить и углубить полученное от другого и на этом основании уже с чем-то соглашаться, чему-то возражать - но без своего взноса оно превращается в бессмысленную полемику. Вы же, наоборот, попытку внести конструктив и уточнение - восприняли в штыки... ?
Это зависит. Я работал с американскими биржевиками и у них часть операций определена в центах, а часть (типа комиссии за биржевую операцию) в 1/10000 цента. Это та же валюта, но дискрет таки другой.
Fair. Поэтому они и доменные типы, что нужен контекст. В противовес биржевым операциям, все эти вещи могут совершенно не понадобиться при написании B2B Value Chain.
Decimal, а не просто Money или что-то подобное
Monetary{Money, Currency}
- порождает ещё один вопрос - "а что такое Money
?". Где-то эта цепь должна сходиться к чуть более абстрактным вещам, вроде какого-никакого численного типа, пусть даже и отсутствующего в стандартной библиотеке (i.e. Decimal|Rational).
Непонятно, зачем вы пытаетесь защищаться.
Просто началось всё с указания, на мою личную неопытность. У меня бывает.
По-моему, как раз оптимальный метод дискуссии, чтобы получить какой-то конструктивный результат, это попытаться расширить и углубить полученное от другого и на этом основании уже с чем-то соглашаться, чему-то возражать - но без своего взноса оно превращается в бессмысленную полемику.
Чтобы полностью в конструктивном режиме выбрать, каким именно будет отдельный заданный тип - начинать тогда следует с описания того, для каких действий он будет применяться, в том числе действий в реальном мире.
Например, в моём изначальном предположении деньги нужно хранить с точностью, которая предположила бы их итоговый вывод в реал - а потому, в принципе, можно и не заморачиваться хранением честных третей, которые можно делить и складывать в любую сторону вообще без потерь, причём на любых временных отрезках. Но следом в то же приложение была добавлена поддержка дележа акций и их владение, биржевых финансовых операций, всех валют мира с их деноминациями. А началось всё вообще с того, что UserID
- это вообще-то не число совсем даже, а enum
.
Люди делятся на тех, кто с деньгами работал, и тех, кто предлагает хранить суммы числами.
Имеется в виду, что надо хранить не просто числами, а с индикацией валюты?
А как мы разрулим ситуацию: «три человека скинулись и купили 100 долларов, а потом поссорились и им надо их поделить поровну, а потом помирились — и им снова из трех слагаемых надо получить $100»?
В финансах вообще-то никак. Двое получат по $33.33, один $33.34, а кто будет этим счастливчиком - можно определить лотереей, можно авторитетом. Иного варианта нет... Только я уже не понимаю, к чему всё это.
С подтипами как-то поорганизованнее получается ИМХО. К примеру: вот куда складывать документацию о том, какой именно формат ожидается от переменной user_phone_number? Если это новый тип вокруг строки, то всё довольно понятно - это идёт в докстринг к типу, со всеми прилагающимися удобвствами вроде работы IntelliSense с ней.
findImageByUserId вообще можно сделать методом UserId, тогда узнать о существовании этой функции как таковой будет проще.
По части подтипов в стандартных типов: файловые пути на первый взгляд просто текст, но в последнее время их делают подтипами байтовых массивов. Примеры можно найти в Python (pathlib.Path), c++ (std::filesystem::path), Rust (std::path::PathBuf).
файловые пути на первый взгляд просто текст
Что не очень верно. Один и тот же путь по дереву файловой системы A->B->C->D в виндовом представлении и юниксном выглядит по разному.
и что вы будете ему присваивать? т.е. это же все равно строка, число или еще что-то.
Что-то мне кажется, что оно называется целое со связанной размерность или единицей измерения.
Количество ящиков и количество копеек на счете могут быть целым числом, но присваивать одно другому - ну так себе идея. Печально, кстати, что в основные языки этого понятия с руганью при неправильном присваивании не завезли.
По принципам ООП вам как раз не надо разбираться в том, как работает каждый класс. Вы должны знать только их поведение и работать с ними как с "черными ящиками". Если у вас Image.type типизирован как ImageType, вас не должна волновать его реализация для работы с ним, достаточно понимания, что этот тип представляет собой валидный тип изображения. Вы же, используя фреймворки, не изучаете реализации каждого компонента. Скорее всего вы читаете документацию
А потом width будет не сравнить с шириной окна, потому что они разных типов.
потому что они разных типов
Не вижу веских причин им быть разных типов именно так, чтобы они были несовместимы для сравнений.
И даже если почему-то вам прямо нужно чтобы это было так, но у вас, как программиста, всегда остаётся возможность написать немного клея, и явным образом добавить сравнение между ними.
Это может послужить, также, хорошим напоминанием о том, что размеры картинок и окон бывают в пикселях, а бывают в em
.
написать немного клея, и явным образом добавить сравнение между ними.
И вас вот такое не фрустрирует?
По 300 раз в день запинаться об кастомную архитектуру и объяснять машине очевидные вещи (перед этим каждый раз бегая проверять, объяснены ли они уже кем-то ранее или нет). Тратить ощутимое время и усилия на то, что джун Вася уже давно сделал одной строкой на JS, не приходя в сознание, без всяких типов, в 20 раз быстрее (хорошо, что начальник про это не знает).
Ведь смысл многих программ сейчас (особенно на вебе) - в том, что все взаимодействия идут через БД или JSON/XML, и на каждое из них придётся писать тонны такого клея, потому что в исходных данных есть только стандартные типы.
И ладно ещё, если речь про какие-нибудь транзакции бирж-банков-госуслуг, там можно на всякий случай хоть по 100 проверок на каждую строку писать (и нужно). Но ведь 99.9% ПО - это вообще не про важные транзакции.
В таком виде вы можете сделать id = width или даже id *= width. Напрямую разве что из-за опечатки, а вот через несколько уровней стека - легко.
Если нужно через несколько уровней стека протащить число, а не объект целиком, это определённо не ООП.
Если вот прямо край как необходимо, то светлая мысль умножить id *= width требует отдельного изучения внутреннего мира автора. У нас такое даже стажёры не пишут
Давайте сравним:
id *= w
id *= ID(w)
Явное же лучше неявного. Есть 2 вопроса:
Зачем описывать в системе типов ограничения данных, которые в терминологии этих типов описываются (тут, вроде, есть разумная аргументация)?
Зачем НЕ описывать в системе типов ограничения данных, которые в терминологии этих типов описываются (и вот тут, кроме "мы экономим буквы" - пока аргументации не видно)
Повторю: запись id*=width вызовет большие вопросы на кодревью.
Переменная, содержащая ширину, может быть названа w только глубоко внутри функции тригонометрических расчётов, во всех остальных случаях она не может называться w.
Умножение ID на что бы то нибыло - скорее всего, опечатка или ошибка. Не должно пройти тесты.
Ответ на 2: для снижения когнитивной сложности входа в проект или рефакторинга давно не использовавшихся частей кода.
Особенно непонятно зачем это делать для программы на Go, где зачастую интерфейс можно прикрутить в любой момент позже (там "классы" не требуют указания "implements").
Для тестов. Добавляете интерфейс и вуаля вы можете подсовывать mock реализации и тестировать поведение.
На каком языке нет библиотеки, позволяющей создавать моки под что угодно?
Простите что значит моки под что угодно? Реализацию моков как подсовывать будете?
Например, в python часто используют манкипаичинг. В JavaScript создаётся специальная сборка с API, которое позволяет заменять одни объекты другими в рантайме.
Сравнили х... с пальцем 😑
В динамических языках можно что угодно делать и без библиотек.
В компилируемых, даже через рефлексию не всегда можно выкрутиться. Поэтому самое просто и в лоб это интерфейс.
В golang статическая типизация. Самый простой вариант в таких случаях это как раз интерфейс и замена реализации через него.
На каком языке нет библиотеки, позволяющей создавать моки под что угодно?
C#. Если метод не виртуальный, не уверен что мок возможен. Во всяком случае это будет нечто выходящее из ряда вон.
Возможен-возможен: Use shims to isolate your app for unit testing
Ну и иногда такое используется даже не для тестов, а просто чтобы плагин работал. Из примеров - фреймворк Harmony для моддинга игр на движке Unity, а также старая версия библиотеки Крипто Про для .NET Framework.
На Яве куча мок библиотек, но
Скорость выполнения тестов при росте проекта грустнеет
Моки пишутся/читаются гораздо сложнее чем просто реализация
Ага, встречалось такое. Люди делают десятки классов, сложносочинённые конструкции. Спрашиваешь: "нахрена? Задача ведь сформулирована была вот ровно так?". Отвечают "а вдруг придётся делать всё через жопу в неопределённом будущем...".
Заодно убивают производительность.
Я не шучу: у меня один новый сотрудник написал замечательный класс транспорта (данные по резервированному TCP-соединению). Отдельный класс формирования заголовка, отдельный класс - вычисления контрольной суммы, отдельный класс упаковки и распаковки со ссылками на два других класса. И отдельный класс для записи данных: по одному байту через вызов виртуальной функции.
На тестах всё работало (с).
Потом "неожиданно" выяснилось, что передавать надо где-то мегабайт в секунду данных.
Знаете, сколько занимает 1 миллион вызовов виртуальной функции?
Я не шучу: у меня один новый сотрудник написал замечательный класс транспорта (данные по резервированному TCP-соединению).
Это скорее традиции старого доброго Java-безумия, чем SOLID.
Даже если понимать S в описанной в статье ошибочной интерпретации, все равно непонятно почему этот код был бы "не SOLID", если каждая из этих задач упакована в отдельную функцию (по смыслу или синтаксически статическую) вместо отдельного класса.
Знаете, сколько занимает 1 миллион вызовов виртуальной функции?
Интересно, сколько? Просто сравнивал в Rust прямой вызов и вызов через dyn и вот не могу сказать, что там какая-то сильно пугающая разница.
Там главная разница - в том, что виртуальный вызов не может быть просто так заинлайнен.
Да вроде компиляторы уже лет 20 умеют строить граф наследования и инлайнить виртуальные функции, если могут доказать динамический тип
И, как правило, для этого требуется заинлайнить несколько функций так, чтобы создание объекта и его использование по интерфейсу оказались рядом. И, если слишком полагаться на эту оптимизацию - промежуточные вызовы тоже могут быть виртуальными. В итоге один раз споткнувшийся инлайнинг перестаёт работать сразу везде.
Так что я бы не полагался на способность компьютера заинлайнить виртуальные вызовы в нагруженном коде.
сколько занимает 1 миллион вызовов виртуальной функции
Если речь про вызов одной и той же виртуальной функции по сравнению с миллионом вызовов точно такой же, но невиртуальной функции - предположу, что примерно нисколько. У виртуальных функций есть оверхед, но в данном конкретном случае он ничтожен (если они реализованы через VPTR, т.е. примерно всегда)
А вот миллион сисколлов в секунду для передачи в ядро по 1 байту - вот оно будет долго и больно.
Да. Только сисколу не важно вызывали ли вы его из одной функции или раздробили ее на несколько.
Более того. Современные VM могут сделать вывод и заинлайнить функцию если это позволяет код. Что уберет оверхед на вызов виртуальной функции.
Мне бы хотелось верить, что в независимости от того, писалось по одному байту или по 3, на нижнем слое всё равно стоит буфер перед сисколом. Иначе ну совсем грустно)
Ну то есть чел вызывал по функции на 1 байт, а виноваты десятки классов и сложносочиненные конструкции?
Ну да, ну да, ох уж эти классы.
Вам полю дай, вы вообще к goto вернетесь.
Ну то есть чел вызывал по функции на 1 байт, а виноваты десятки классов и сложносочиненные конструкции?
Какие-нибудь getchar(), puchar() из C stdio это тоже по байту. Но, в отличие от Java, у них умолчательный режим как раз через буферизацию (реально внутри они обычно макры, которые прямо лезут в буфер объекта FILE).
А вот наивный кодер Java прочитает, что есть FileReader, и не прочитает, что нормально вокруг него ещё и оборачивать через BufferedReader. Аналогично для записи. И да, я бы сказал, что "виноваты" десятки классов, потому что навернуть такую сложную конструкцию вокруг простого I/O это надо было постараться. Цель понятна - обобщить - но результат пугает.
Вам полю дай, вы вообще к goto вернетесь.
В goto нет ничего плохого при использовании определённых жёстких правил, как, например, в MISRA (переходы только вперёд - уже спасают от 90% тех проблем, которые описывал Дейкстра; ещё 9% решается при переходах только на финальную зачистку). Оригинальная статья была о том, когда он применяется не к месту и создаёт плетёнку переходов, в которой фиг разберётся даже опытный - и что мешает автоматизированному анализу кода (но ему и early return мешал тогда).
Не чтобы оспорить тезис, но просто дополню: в Java 1 миллион вызовов виртуальной функции занимает нисколько, если окажется в рантайме, что имплементация всегда одна и та же - JiT увидит мономорфизм, поставит trap и заинлайнит всё под корень.
Для меня проблема в миллиардах интерфейсов чаще не столько в производительности, сколько в том, что читать становится сложнее, а модифицировать - вообще хоть вешайся, хотя автор декларировал, казалось бы, что будет наоборот)
этот парень в РКН прошивку для ТСПУ писал? тогда он всё правильно сделал...
Мне кажется проблема в том, что изначально это было обусловлено именно потребностями в организации работы в энтерпрайз-среде.
Но что-то пошло не так и принципы стали использовать ради принципов даже в прототипах и MVP
Вот! Когда Мартин еще сам писал код, а не учил этому других, многое было иначе. В частности систем контроля версий в современном понимании этого слова не было. Системы были, конечно, но были они откровенно убогие по нынешним меркам. Да и даже убогие использовались далеко не везде. А в такой ситуации одновременная работа с файлом (классом, модулем), когда несколько человек одновременно вносят правки превращается в мучение. И подход - разбейте класс/модуль на части чтобы исключить одновременные правки это реально работающая вещь.
Что касается OCP, то придуман он был Мейером и у него явно сказано что это про организацию работы. Мартин же довел принцип до абсурда - нельзя чтобы бинарники менялись при добавлении нового функционала.
Когда Мартин еще сам писал код, а не учил этому других, многое было иначе.
А есть хоть одно свидетельство того, что Мартин в своей жизни написал хоть одну завалящую строчку кода?
А то если смотреть строго по его советам, то ничего тяжелее мегафона он в руках никогда не держал.
А есть хоть одно свидетельство того, что Мартин в своей жизни написал хоть одну завалящую строчку кода?
Что в вашем представлении "завалящая"? До сих пор пишет https://github.com/unclebob
А-а-а-а! Это прекрасно. Последний Advent of Code на Кложе — это просто восторг!
Первый день: угадайте, на каком размере файла этот код сожрёт всю память и закуклится?
А есть хоть одно свидетельство того, что Мартин в своей жизни написал хоть одну завалящую строчку кода?
Ну наверное что-то он писал, вопрос что именно, ЕМНИП в какой-то из книг у него есть перечень где он работал и что делал.
А то если смотреть строго по его советам, то ничего тяжелее мегафона он в руках никогда не держал.
ИМХО это просто специфический опыт. Типа рассказов про еженедельный(!) билд.
Мартин не настоящий программист, а только лишь посредственный и жадный до внимания писатель
Наконец-то кто-то об этом заговорил
Ну почему "наконец-то". Я на сборе аргументов против SOLID не циклился, просто сваливал наиболее характерное по методологии, но вот с ходу нашлось в запасах:
Ну и вообще если по Мартину - то почему "Clean code" в его реализации это чушь, не меньше:
https://www.computerenhance.com/p/clean-code-horrible-performance (перевод: https://habr.com/ru/companies/sportmaster_lab/articles/728880/ )
http://sergeyteplyakov.blogspot.com/2015/12/review-clean-coder.html
Ну а если покопаться, ещё в несколько раз больше будет.
Ну и моё замечание в этих комментариях.
SOLID всегда напоминал мне религиозные заповеди (как я представляю себе их эволюцию).
Вначале у кого-то появляется хорошая универсальная идея. Например, «давайте будем немного добрее друг к другу». Или «классы должны представлять моделируемую область, чтобы код было легче читать». (Сейчас это называется DDD, а когда-то у нас не было такого термина, потому что мы подразумевали DDD просто говоря «ООП»).
Но донести хорошую универсальную идею до масс трудно. Ведь в каждом конкретном случае придётся ДУМАТЬ. А думать массы не любят. Они любят готовые и простые рецепты. Вот кто-то мудрый видит класс File, который выводит на экран окно, и говорит: «Ну нахрена ж ты это СЮДА запихал?! Класс должен делать что-то одно!» (имея в виду, что класс File должен хорошо воплощать наши представления о файлах, и больше ничего). Побитый ученик выходит к толпе: «Учитель сказал — класс должен делать только одну вещь!». Все дружно идут пересчитывать, сколько вещей делают их классы.
Так мы пришли к SOLID и запрету варить козлёнка в молоке матери его.
это стало возможным благодаря тому что произошел скачок в языках и С++/Java теперь при беглом взгляде не думая работает вроде тривиально, но в то же время если вернуться к истокам С окажется, что С++ стал не тривиальным, а в С вроде всё как и было
Побитый ученик выходит к толпе: «Учитель сказал — класс должен делать только одну вещь!». Все дружно идут пересчитывать, сколько вещей делают их классы.
Это было про букву S. Хочется добавить и про букву L.
Допустим, мы пишем операционную система класса UNIX. «Всё есть файл». Файлом является и документ на диске, и последовательный порт, и многое другое, что мы можем открыть по имени, считать оттуда данные и записать другие данные. И у нас есть модуль, который принимает на вход файл, и работает с ним, не разбирая, что мы ему передали — документ на диске или последовательный порт. И знаете что? В этом случае модуль должен отработать корректно, не важно, что мы ему передали: документ на диске или последовательный порт. ВЫ ЧУВСТВУЕТЕ ВСЮ ГЛУБИНУ ЭТОГО ПРИНЦИПА? Последний раз я такую видел, когда читал «Записки прапорщика»: «Боевой листок должен быть боевым листком — ведь это же, как-никак, боевой листок!».
Собственно, почему это трюизм? Потому что DDD требует, чтобы вы наследовали классы тогда, и только тогда, когда что-то является разновидностью чего-то другого. Дубинушка, просто не наследуйся от класса File, если ты хочешь описать то, что файлом не является! Но поскольку программисты забыли, для чего предки придумали наследование, им приходится учить:
Если q(x) является свойством, верным относительно объектов x некоторого типа T, тогда q (y) также должно быть верным для объектов y типа S, где S является подтипом типа T.
и
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
SOLID плох не тем, что он неправилен, а тем, что простую и ясную мысль («ваш код должен корректно отражать предметную область») подменяют пачкой бездушных следствий. При этом отдельные юмористы ещё и умудряются отрицать исходную мысль (DDD).
Было бы интересно ознакомится с правильными принципами, правилами, методологией. Если SOLID плохой, ООП плохой, DDD плохой... всё плохое. Познакомьте с хорошим. Или вообще ничего не надо, просто пиши код, как придётся? Или "как договорится команда"? А перешёл в другую команду, там свой "талмуд", при чём даже нигде не описанный, просто на словах. Зачем вообще что-то знать? Книжки читать, так там плохому научат. Консолидированный опыт в топку.
В том то и дело, что нет, вполне себе вербализуемо - SOLID, KISS и прочие прекрасные вещи ;-) То, что микроскопом можно разбить кому-нибудь окно не делает микроскоп плохим инструментом.
Ну вот есть такой принцип для здоровья, что чистить зубы надо минимум два раза в день. И тут приходит человек, смотрите говорит, знакомый взял железную щётку и чистил два раза в день, остался без зубов. А вот другой знакомый, чистил два раза в день, содой. Остался без зубов. Ещё друг знакомого, чистил два раза в день, нормальной щёткой, делал это каждый раз по полтора часа не прекращая, остался без зубов.
Возвели, понимаешь, чистку зубов в какую-то догму. Нашли себе святую корову! Выкиньте этот глупый принцип :)
если уж воруешь, то воруй как художник
Вот это прекрасно. Тут не то место, где подобное оценят, но я бы хотел, чтобы у меня в команде люди хотя бы понимали, что это значит, и как этого достичь.
Это не вербализуемо.
Я б немножко иначе выразился. Дело не вербализуемости, а в том, что DDD — эмерджентный, высокоуровневый принцип. Его невозможно свести к набору рецептов (что роднит его со вторым началом термодинамики, которое является физическим эмерджентным принципом). Чтобы раскидать предметную область по классам, надо долго думать. А кто думать не любит, тот говорит: да какая польза от этого вашего DDD. Между тем, польза огромная. Если подумать 8-)
Вот очень хороший пример: Воины и волшебники. Так облажаться при декомпозиции — это надо суметь. А ведь автор (Эрик Липперт) довольно неглуп. Я думаю, надо одновременно любить Heroes 3 и программирование, чтобы понять, что неправильно делать классы для воинов и волшебников. А он этот пример использует, чтобы показать, что DDD не работает. Переваливает со своей больной головы на здоровую.
Если SOLID плохой, ООП плохой, DDD плохой... всё плохое. Познакомьте с хорошим
Как-то странно вы читаете. То, что я не писал.
Было бы интересно ознакомится с правильными принципами, правилами, методологией.
Идеала нет и не будет. Но, например, GRASP был собран не ради красивой аббревиатуры, а ради подбора действительно важных принципов, и в формулировках, в которых его сильно сложнее неверно понять.
SOLID плох не тем, что он неправилен, а тем, что простую и ясную мысль («ваш код должен корректно отражать предметную область») подменяют пачкой бездушных следствий.
То, что SOLID плох - очень сильное утверждение.
SOLID кажется необоснованно-многословным и расплывчатым ровно по одной причине: "ваш код должен корректно отражать предметную область" - наивное определение, не отражающее реальное положение дел.
Возьмите "классику" с квадратом и прямоугольником. Каждому 3-класснику понятно, что квадрат - частный случай прямоугольника, наследующий все его свойства. Т.е. буквально класс "квадрат" должен быть наследником класса "прямоугольник". Беда в том, что, к сожалению, это невыразимо в текущей реализации ООП. Квадрат не может быть использован вместо прямоугольника, у него дополнительное ограничение. Как только вы измените длину одной из сторон, он квадратом быть перестанет.
Поэтому и описываюся принципы так, чтобы максимально корректно отражать суть происходящего. L как раз буквально говорит нам о том, что, несмотря на всю заманчивость идеи, реализовывать квадрат как наследника прямоугольника в ООП - путь в никуда.
Не совсем так. То, что квадрат является прямоугольником, в ООП вполне себе выразимо - но надо придерживаться математических квадрата и прямоугольника, которые неизменяемы, и не имеют идентичности и поведения.
Можно обыграть и изменяемость, если, например, базовым классом назначить "прямоугольник, у которого при изменении одной стороны вторая корректируется некоторым образом, виртуальными функциями correctionWidth() и correctionHeight()". Тогда у обычного прямоугольника эти функции ничего не будут делать, у квадрата просто копировать значение, ну и возможны всякие другие варианты.
В поддержку предыдущего оратора и чтобы было чуть более понятно на примере. Квадрат прекрасно сабклассирует прямоугольник если интерфейс прямоугольника не содержит инплэйс мутаторов.
class rectangle{
virtual rectangle withLength(uint newLength) const { return rectangle{newLength, height}; }
}
Тогда не составит труда корректно с точки зрения лсп реализовать перегрузку в сабклассе квадрат. При этом квадрат может иметь свои специфичные методы которые возвращают квадрат, что бы можно было и дальше сабклассировать при необходимости.
Тут конечно может возникнуть оверхед на то чтобы боксить возвращаемые значения в зависимости от языка и вашей реализации.
Но тут главное какую задачу решаем, если это векторный редактор и вы мышкой тянете сторону квадрата, то наверное вы хотите чтобы он квадратом и остался и тогда можно делать мутабельную версию которая при изменении длины и ширину поменяет, а вот версия выше юзеру не понравится т.к. квадрат мгновенно превратится в прямоугольник. Просто в мутабельной версии вы должны каким то образом выразить (хоть в документации) что изменение длины не подразумевает неизменность ширины и тогда нет никаких проблем с лсп и в мутабельной версии. Можно даже оба подхода совместить и иметь визЛенч и сэтЛенч.
Все верно, если фигура иммутабельна, то квадрат вполне может быть наследником прямоугольника. Собственно, мы с вами только что продемонстрировали полезность буквы L из SOLID.
Т.е. буквально опираясь на принцип подстановки Лисков мы проанализировали ограничения нашей предметной области и определили условия, при которых мы можем наследовать квадрат от прямоугольника.
Сам принцип как раз к этому и сводится: помни, что где-то в коде любой наследник может быть использован в качестве своего родителя! Убедись, что не поломаешь логику.
Полезная же, получается, буковка? Тупо в качестве чеклиста "о чем подумать, чтобы не поломать проект" - польза несомненная.
Каждому 3-класснику понятно, что квадрат - частный случай прямоугольника, наследующий все его свойства. Т.е. буквально класс "квадрат" должен быть наследником класса "прямоугольник".
Конечно, наивность. Только это личная наивность проектировщика, а не принципа DDD.
Допустим, вы проектируете векторный редактор. (Я их в своё время напроектировался, и нареализовывался). Разве ваш пользователь оперирует квадратами? Нет. Он хочет иметь возможность растянуть любой квадрат. (Ну, может не любой, а тот, у которого aspect ratio unlocked). Так откуда же возьмётся класс «квадрат»?
Далеко не факт, что даже класс «прямоугольник» нужен. Может, хватило бы просто класса Path. Но класс «квадрат» не нужен почти наверняка. Я просто не могу придумать, где бы он мог пригодиться. Иными словами, класс «квадрат» не нужен именно по DDD, что бы там вашему третьекласснику ни казалось.
Я рядом уже привёл пример, Воины и волшебники. Вам удалось всё то же самое выразить в одном комментарии, а там автор написал серию аж из шести статей. А суть-то одна: сначала неправильно применяем DDD (полностью оторвавшись от РЕАЛЬНОЙ предметной области и погнавшись за абстракциями), потом получаем ужасный результат, потом пишем, что DDD не работает.
Сейчас это называется DDD, а когда-то у нас не было такого термина, потому что мы подразумевали DDD просто говоря «ООП»
Бурные, продолжительные аплодисменты ©. Если ООП модель не DDD – с ней явно что-то не так. Сама концепция ООП сформировалась, как попытка представить бизнес-сущности средствами программирования. Всё остальное вторично.
Проблема в том, что наша сфера не имеет каких-то четких критериев, ГОСТов, нормативов и т.д., даже законы физики оказывают влияние только до определенного уровня. Слепо следовать принципам неэффективно, но так хотя бы можно попытаться говорить с другими разработчиками на одном языке. Я, возможно в силу небольшого опыта, особо не вижу других вариантов.
IT - благодатная почва для карго-культа.

О, картинка в точности с моей прошлой галеры. "Давайте оценивать задачи в сторипоинтах", "Давайте проводить рефайнмент по вторникам после начала спринта"... "Может давайте сначала познакомимся друг с другому и разберемся что от нашей команды вообще ожидают и потом решим какие процессы нам нужны" - и тут я понимаю, что лечу в окно.
по пунктам SOLID
1. SRP не SRP
Окей, здесь скорее согласен, бессмысленные перестановки кода без цели вряд ли будут успешными. Если вы хорошо не можете объяснить, зачем вы что-то переставляете, не делайте этого
2. Принцип открытости/закрытости
> Тут всегда вопрос в том, а появится ли еще один тип заказа в будущем? Или мы просто так добавили 2 класса и интерфейс
Здесь вопрос сразу: если это никто и не будет в будущем трогать, то почему не использовать эту известную всем идиому с имплементацией интерфейса, а не запихивать if..else в методы. А если у калькулятора будет не один calculateTotal, а 4-5 методов?
3. Liskov Substitution Principle
> В данном случае строгое следование LSP привело к: Увеличению количества кода; Дублированию логики чтения файла; Усложнению структуры проекта
На мой взгляд стало понятнее и проще. Снова мы не лезем с непонятной логикой в тела методов, а четко разграничили разные сущности с общим интерфейсом. Вроде бы win-win. Кода стало больше: у вас есть оптимизация чанков сборщиком и всякие tree-shaking'и в бандлере
4. Interface Segregation Principle: Размер имеет значение
Не совсем ведь. Это похоже на SRP, но диктует уже то, что не нужно классу давать методы, которые ему не нужны. То, что в результате получаются маленькие интерфейсы, это следствие, а не причина
> no code should be forced to depend on methods it does not use
Иначе вы либо игнорируете некорректное наследование, что рано или поздно выстрелит в ногу, либо делаете странные вещи и даете классам ненужные методы, которые по идее не должны вообще работать, если даты под это нет у класса, а ее не должно быть, иначе разделять не нужно и ISP не подходит
5. Dependency Inversion Principle: Инверсия ради инверсии
> DIP часто превращается в догму "всегда используйте интерфейсы", что приводит к созданию ненужных абстракций
Либо я не понял, либо я не видел, чтобы так происходило. DIP это возможно, один из наиболее важных принципов в SOLID, он чаще всего как раз полезный, чем не полезный: легче тестирование, а уже этого много стоит, меньше классы, легче прийти к адаптерам, легче формируется точки входа в модулях
DIP один из важнейших и полезных принципов. Отрицание, банально, один из признаков не понимания.
DIP один из важнейших и полезных принципов.
Это если хорошенько натянуть сову на глобус. Что-то типа - используйте интерфейсы/абстрактные классы там где надо, а где не надо, не используйте. Хороший принцип? Хороший. Знание настолько общего принципа как-то помогает писать хороший код? Нет.
Это не говоря о том, что изначально Мартин писал исключительно про плюсы и сам писал что в других языках проблемы нет. В интернетах есть его оригинальная статья, абзац " Separating Interface from Implementation in C++".
Не очень понял про сову и глобус. В инверсии зависимостей абстракция это решения основной задачи: инвертировать зависимость. Принцип замечательный, и очень многое расставляет по местам, и снимает кучу проблем. Какая сова?
Не очень понял про сову и глобус.
Сова это когда любое использование интерфейса (интерфейса в смысле абстрактного класса) называется инверсией зависимостей. Сам же Мартин пишет вот что
Notice that the inversion here is not just one of dependencies, it is also one of interface ownership. We often think of utility libraries as owning their own interfaces. But when the DIP is applied, we find that the clients tend to own the abstract interfaces and that their servers derive from them.
А это уже достаточно спорно. И если в плюсах (по крайней мере в плюсах времен Мартина) у этого есть смысл, то в тех же шарпах смысла в этом нет.
Любое использование интерфейса всегда разворачивает зависимость на 180 градусов. Требуется этот инструмент для выстраивания взаимоотношений не уровня класс-класс, а управлением зависимостями между модулями/библиотеками. Если 2 модуля дергают методы конкретных классов друг друга, то это приведет к куче проблем во время любых манипуляций с ними. Поэтому в хорошей архитектуре всегда зависимости между модулями выстраиваются однонаправлено.
Ну а когда и на каком языке это написано, значения не имееет для данного принципа.
Я чего-то не понимаю, хочу разобраться. Можете перечислить эту кучу проблем?
Ну вот, к примеру, пусть в моем приложении есть класс Shop, который дёргает напрямую метод pay(amount ...) конкретного класса IndiaPayment из стороннего пакета india-payment.
Если этот пакет решит что надо менять аргументы метода pay, то и интерфейсы не помогут. А если не решит, то и без интерфейсов всё будет работать.
А зачем вы в своем приложении полагаетесь на интерфейсы которые вами не управляются? Если я подключаю внешнюю зависимость и не уверен в ее стабильности (а это чаще всего так), то отделяю эту зависимость своим интерфейсом. Далее если IndiaPayment поменяет метод, то мне нужно будет это править ровно в одном месте который осуществляет связывание с IndiaPayment и не заниматься рефакторингом всего проекта.
Ну окей, класс-адаптер - это хорошо, причем тут абстракции, интерфейсы и DIP
А интерфейс потребуется чтобы заменять класс-адаптер на тестовую тупиковую реализацию. Чисто практический смысл.
А интерфейс потребуется чтобы заменять класс-адаптер на тестовую тупиковую реализацию. Чисто практический смысл.
Это, вообще говоря, свойства рантайма и линковки в конкретном языке. Если можно в какой-то момент как то выбрать, какую из одинаково названных реализаций использовать (поместить, скажем, в путь поиска библиотеку тестовых вариантов раньше чем продуктовую) - то можно и без интерфейсов же?
Да можно. Только это не упрощает, а усложняет жизнь программисту. Вместо явного указания тут будет интерфейс и тут мы ожидаем что будет подмена реализации, это выносится на уровень рантайма и линковки. А это надо будет еще раз где-то описывать и показывать. В чем смысл? Чтобы просто кушать не могу убрать интерфейс? Ну дак лучше то не становится.
Только это не упрощает, а усложняет жизнь программисту.
Зависит от точки зрения. Если среда позволяет - то можно будет вообще не думать нужна тут когда-нибудь подмена реализации или нет. А просто тестовой среде объяснять "сейчас подмени то", когда захотелось.
Интерфейс же и замена через него реализаций, универсально и используется во многих языках. Это дешевле так-как вероятность того что программист уже пользовался таким выше. Особенно это актуально в компилируемых языках.
Подход замены же который предлагается это больше про динамические языки. И там вот интерфейсы для тестирования можно и не использовать так-как подмена делается существенно проще.
Подход замены же который предлагается это больше про динамические языки.
В очень даже компилируемом C стандартная библиотека(да и не только она) поставляется в двух вариантах. В продуктовом и версии 'для отладки', где в реализации больше напихано и не выкинуто всяких полезностей именно для разработки. И в зависимости от режима компиляции/среды линкуется нужная.
Так что способ с явной подменой - на самом деле весьма древний. Просто эволюция инструментов пошла по пути использования интерфейсов.
В очень даже компилируемом C стандартная библиотека(да и не только она) поставляется в двух вариантах. В продуктовом и версии 'для отладки', где в реализации больше напихано и не выкинуто всяких полезностей именно для разработки. И в зависимости от режима компиляции/среды линкуется нужная.
Ну вообще там просто debug символы при линковке режутся и все. А сами по себе библиотеки одинаковые.
Проблема подмены в ее неявности.
Любое использование интерфейса всегда разворачивает зависимость на 180 градусов.
Мартин считает иначе, цитату я привел. Я не к тому, что мнение Мартина о том как делать правильно автоматически верное. Но если мы обсуждаем принципы Мартина мы должны обсуждать его точку зрения.
Иначе получается совсем смешно. Взят банальный прием ООП, назван непонятным именем и теперь надо заплатить денег дяде Бобу чтобы он объяснил что имеется в виду.
Поэтому в хорошей архитектуре всегда зависимости между модулями выстраиваются однонаправлено.
Это "слоистая" архитектура и в ней инверсия зависимостей и/или использование абстрактного класса/интерфейса прямого отношения не имеют. Можно разделить код на слои, выстроить зависимости однонаправленно и все это без абстрактных классов.
Ну а когда и на каком языке это написано, значения не имееет для данного принципа.
One might complain that the structure in Figure 3 does not exhibit the dependency, and transitive dependency problems that I claimed. After all, Policy Layer depends only upon the interface of Mechanism Layer. Why would a change to the implementation of Mechanism Layer have any affect at all upon Policy Layer?
In some object oriented language, this would be true. In such languages, interface is separated from implementation automatically. In C++ however, there is no separation between interface and implementation. Rather, in C++, the separation is between the definition of the class and the definition of its member functions
Если контрактом (интерфейсом) владеет исполнитель (сервер), то вы (клиент) вынуждены играть по чужим правилам (vendor lock). Это как в ситуации, когда сотовый оператор меняет условия "вашего" тарифа (ведь на самом деле это его тариф) в одностороннем порядке и вам остаётся лишь утереться и подстроиться (сделать рефакторинг). Это пример обычной "прямой" зависимости, где стрелки использования и зависимости при изменении сонаправлены - от клиента к серверу.
Когда же контракт находится на стороне клиента (вашей), то уже он диктует правила для сервера (исполнителя). Здесь как в случае контракта организации с наёмными сотрудниками - имея свой стандартный договор она может менять их потом как перчатки, в любой момент, без особых изменений на своей стороне. Подстраиваться под ваши условия это задача исполнителей. Именно в этом случае стрелки зависимости оказываются развернутыми в отношении стрелок использования, что и описывает DIP.
На уровне кода эта разница может быть выражена, например, на уровне пакетов: в чьём пакете находится определение интерфейса - клиента или сервера.
Здесь вопрос сразу: если это никто и не будет в будущем трогать, то почему не использовать эту известную всем идиому с имплементацией интерфейса, а не запихивать if..else в методы.
Здесь речь в том числе и о том, что есть религиозное "тут нарушен SOLID, надо потратить ресурсы и переделать", а есть "да, SOLID тут нарушен, но пофиг, так как никакого развития кода тут не будет, конечная остановка".
А если у калькулятора будет не один calculateTotal, а 4-5 методов?
Так об этом и речь дальше: если сложность перешагнула некоторую точку, то нужно рефакторить. Но не нужно заранее усложнять архитектуру проекта с мыслью "а вдруг?". Нужно понимать, когда выделение интерфейсов, разделение типов исключений, DIP идёт на пользу продукту, а когда это FizzBuzz Enterprise Edition.
У правила есть такое свойство, если правило не соблюдается, значит это не правило. Принципы это правила, но достаточно абстрактные и общие, поэтому принципы. Они позволяют не только писать хороший код, но и оценивать его качество в сообществе. У каждого может быть своё мнение и взгляды, кто-то может считать, что забивать шурупы молотком это нормально, и вообще чего вы лезете со своей отвёрткой, и возводите это в религию? :)
Сишник тут спросит: "Чем вам не нравится GOTO на конец функции, где обрабатываются ошибки и высвобождается память?"
У хороших правил бывают исключения. В некоторых редких случаях требования к оптимизации могут довольно сильно нарушать общие подходы точечно для решения узкоспециализированных проблем. Это не отменяет правило, но говорит о том, что случаи бывают разные и нужно чётко понимать, что и зачем делаешь. Если же отвергать правила потому что я их не понимаю, чего вы пристали, мне так не нравится.. Ну такое.
А в самом деле, вам не нравится goto? :)
Тем, что это - сишная идиома, проистекающая из сишной проблемы невозможности автоматически вызвать некую функцию при выходе потока исполнения из заданного лексического скоупа. Как я уже писал, в других ЯП для этого есть более адекватные средства.
У меня вопрос к автору:
В конце концов, главная цель любого принципа проектирования — это создание понятного, поддерживаемого и эффективного кода.
Убираем все принципы на полку "какие-то там рекомендации". Озвучьте пожалуйста критерии понятного, поддерживаемого и эффективного кода. Я так понимаю, они у вас сугубо свои. Ведь все принципы это какое-то возведённые в религию зло, окей.
Как вы поймёте, что ваше решение хорошее? И что делать, если другой разработчик, конкретное ваше решение и архитектуру вообще не считает даже близко понятным, поддерживаемым и эффективным? Как вы будете решать эту проблему на одном проекте?
А так вообще бывает, чтобы код одного разработчика понравился другому?
Если два разраба не могут договориться, пусть зовут тимлида, пусть он скажет, как понятнее.
Это лучше, чем следовать общим принципам без учёта конкретной ситуации.
Всё это никак не соответствует идее написания понятного, поддерживаемого и эффективного кода. То, о чём вы говорите соответствует идее: пишем так, как понравится тимлиду. Люди не склонны идти на конфликт, и без каких-то общих критериев оценки, команда разработки превращается не в команду инженеров, а в команду художников. "Я так вижу!"
Три противоречащих точки зрения, конечно, лучше, чем две.
Ну а кто сказал что вообще говоря существует Единственный Верный Путь написать программу?
Задача тимлида сделать выбор. И дальше будет так как он скажет. В этом суть руководителя - своей волей и властью решать вопросы, которые не могут решить подчиненные.
Суть хорошего руководителя - чтобы при этом еще и решения были хорошими. Но это уже опционально. Часто по большому счету разницы между решениями нет, а люди зависают именно на выборе из двух равных альтернатив, что в итоге стопорит проект. Как та история про Королева и твердую Луну.
Если два разраба не могут договориться, пусть зовут тимлида, пусть он скажет, как понятнее.
Если тимлид сам не программист, то он понятия не имеет, как будет понятнее. А если программист - тогда это по сути три программиста будут обсуждать, как кому нравится, разве что у одного из них есть право просто заткнуть несогласных авторитетом.
Ну и каждый раз тимлида ненадёргаешься, и в итоге будет выстроена снова какая-то система, не факт что лучше SOLID.
задача тимлида чтобы задача продвинулась дальше, прекратить спор остроконечников с тупоконечников и принять решение (в крайнем случае даже рандомное). Это не тоже самое, что три программиста
Угу, и после рандомных решений с ненулевой вероятностью получается codestyle, одинаково ненавистный обоим программистам, поэтому споры прекращаются по естественным причинам, после обработки двух заявлений на увольнение.
Демократия точно так же работает, и ничё :)
49% населения хочет ходить в зелёном и наотрез отказывается — в красном
49% населения хочет ходить в красном, и наотрез отказывается — в зелёном
на выборы приходит три партии: Красные дьяволята, Зелёные тунберги, Серые кардиналы, понимая, что их партия не наберет требуемый максимум, и полагая своих оппонентов людьми не глупее себя — 98% процентов людей голосует за то, чего они на самом деле не хотят — за серое.
———
Если без «не глупее себя» —
33% за красное, 33% за зеленое, 34% за серое — выбрали серое, 66% (заметное большинство) — недовольны.
Тимлиду отвечать потом код эа этих двоих, поэтому его выбор имеет значение, хотя и не гарантирует что выбор правильный
Озвучьте пожалуйста критерии понятного, поддерживаемого и эффективного кода.
Хочу заметить, что объективных критериев понятного и поддерживаемого кода нет, и быть не может. По той причине, что понимают и поддерживают код люди. Или группы людей. То есть - субъекты. И понятность и поддерживаемость зависит от этих субъектов: их памяти, внимательности, интеллекта, предыдущего опыта и т.д.
С эффективностью же, если понимать под ней производительность, проблем нет: ее можно померить совершенно объективно.
Ну как это нет, здрасьте. Т.е. взяли и на помойку выкинули весь опыт разработки. И как код ревью проводить? Может и не надо? Пусть каждый пишет вообще как хочет, потому что никаких критериев и быть не может? В общем, приехали.
Ну как это нет
Внесите в студию, пожалуйста
Т.е. взяли и на помойку выкинули весь опыт разработки.
Нет, не выкинул. Просто обратил внимание на то, что у каждого разработчика опыт разный. А ещё - навыки разные.
А объективные критерии непонятного и неподдерживаемого кода?)
Иногда легче идти от обратного и сказать что точно относится к плохому коду, если отбросить очень редкие и специфичные ситуации. Например, код где 1,2,3 буквенные названия у большинства переменных и функций или одна гигантская функция на 10к+ строк, с кучей условий и флагов, которая делает все на свете или goto, который прыгает по всей кодовой базе, #define true false и т д
Озвучьте пожалуйста критерии понятного, поддерживаемого и эффективного кода.
Я бы предложил парочку в части понятности и поддерживаемости, вроде достаточно объективных:
Возможность и простота покрытия тестами. Код, для которого сложно написать тесты, с множеством условий и внешних зависимостей, как правило, достаточно сложен для понимания и последующего внесения изменений. Если тестов нет, то изменения ещё и небезопасны.
Результаты статического анализа кода. Есть множество инструментов, анализирующих такие метрики, как cyclomatic/cognitive complexity. Они тоже не идеальны, но в целом дают неплохое представление о читабельности кода.
Эффективность же легко проверяется нагрузочными тестами.
Возможность и простота покрытия тестами
Немного капитана: тот код который легко покрыть тестами скорее всего не эффективный )
код который легко покрыть тестами скорее всего не эффективный )
Эффективность – штука тонкая. Она не должна быть максимально возможной – скорее достаточной. И честно говоря, в большинстве проектов вопрос эффективности не стоит вообще. Даже там, где она на самом деле важна, часто дешевле купить железо помощнее, чем оплачивать команду супер специалистов, умеющих в Hi Load. Как говаривал один заказчик: вам нужно ещё 100 гиг памяти? Я вам её куплю, но дайте мне результат к концу недели. Да и тестируемость тоже – круто иметь 100% покрытие, конечно, но чаще всего это просто избыточно.
Покажите мне любую самую эффективную реализацию любого алгоритма на любом языке — и я ее за десять минут покрою тестами так, что комар носа не подточит.
Просто надо тестировать то, что надо тестировать, а именно — properties. Да, тесты — скорее всего — будут на Хаскеле, но если надо — могу на руби, эрланге, эликсире.
Я в целом согласен но есть одно большущее НО.
Работать по уму - замечательный совет. Думать то что вы делаете - замечательный совет.
Это все применимо либо когда работаешь один или небольшой группой, ну либо очень повезло с людьми.
А теперь вернемся в реальность.
Допустим у Вас хорошая команда, высока вероятность что все просто переругаются, потому что все индивидуалисты и уникальные снежинки. От того что пишет один будет воротить другого. Проект будет выглядеть как попало, потому что каждый сам себе буратин и творческая личность.
Либо более реальная ситуация. К сожалению, особенно сейчас, не так много разработчиков которые умеют писать просто, которые умеют применять подход и инструменты к месту и к еще большему сожалению еще меньше тех кто вообще хочет думать. Для огромного количество разрабов это все от звонка до звонка и просто нужно сделать задачу. Если пускать людей в свободное плавание то как показывает такая практика проект просто превращается в ад.
С Вами в целом соглашусь что каждый подход хорош к месту но мой опыт показывает что уже лучше все будут плюс минус единообразно фанатеть по solid и для всех будут сходные стандарты и догмы, но зато в среднем по больнице это дает лучшие плоды чем надеяться что каждый примет правильное решение и проект не будет похож на творческий зоопарк.
Писать просто - сложно. Для этого нужно много опыта, которого на рынке нет. Намного проще чтобы все писали единообразно, пусть где-то с овер-инженерингом. Пусть лучше все будет через интерфейсы чем то так то сяк. Пусть лучше разделяют интерфейсы и пишут больше классов, которые по итогу можно заменять и легко тестировать, чем пускаются во все тяжкие и верить что условный разраб, которому возможно вообще это ваше программирование не интересно, он тут чисто за деньги, будет обдуманно каждое решение выкатывать. Проверяли, еще хуже все.
Есть ещё одна вещь, которой все бояться, но при этом реальных проблем не бывает - множественное наследование. Главный аргумент - алмаз смерти, но при этом шанс того, что у Вас будет суперкласс, потом несколько потомков и снова общий класс происходит редко, а решается быстро, про это хорошо сказал Страуструп, можно и от условий if/else отказаться и всё работать будет, но есть же удобство. Это тема поднималась на SO, там целый ответ на эту тему
SOLID возник в 1995 году в переписке https://groups.google.com/g/comp.object/c/WICPDcXAMG8?hl=en&pli=1#adee7e5bd99ab111. Ребята весело проводили время, обсуждая количественный вопрос:
We are trying to compose The Ten Commandments of OO Programming.
Betrand Meyer said somewhere that there are about 20 good ideas in OO, and that is what the language(s) have to support.
Is it possible to boil everything down to 10?
Дескать, 20 хороших идей для ОО это хорошо, но абсолютно не практично - их же ни кто не запомнит. Заповедей должно быть 10! Robert Martin (aka Uncle Bob) тогда всех победил, предложив сначала 11 кандидатов, а потом еще сократив список.
Потом он публикует серию статей по мотивам, где спустя несколько итераций в итоге оставляет 5. Как оказалось мирянам, этого вполне достаточно, чтобы гарантированно создавать бурление в комментах. Принцип работает на протяжении уже 30 лет.
Как следует из самой постановки вопроса, за SOLID не было ни какой научной подоплеки. Просто несколько звучных тезисов, выбранных на собственный вкус. Насколько он практичен в области разработки софта, я думаю время уже сто раз показало. =)
А как должно быть? Люди в халатах с грамотами должны были выковать скрижали в жерле научной группы? Мне нравится ваша попытка обесценить опыт, под соусом "ребята собрались под пивка", но нет. В целом множество проектов, систем и прочего рождено энтузиастами, которым не всё равно.
Мне нравится ваша попытка обесценить опыт, под соусом "ребята собрались под пивка", но нет.
Им тогда было меньше лет, чем мне сейчас. Имею право. ;)
Ну есть, например, теория типов https://en.m.wikipedia.org/wiki/Type_theory , в бекграунде которой лежат серьёзные исследования из разных областей математической логики. Она неплохо справляется с проблемами разработки, в том числе и в ОО стиле.
А SOLID в общем случае это такая же чушь собачья как советы уважаемых врачей из телевизора.
Дядя Боб не практик, а философ из институтской среды. Он пишет захватывающие произведения и делает вдохновляющие доклады, в которых лестно искать глубинный смысл долгими зимними вечерами. Его многие за это любят, уважают и ценят. Но все его слова нужно воспринимать с большой долей скептицизма, как только речь заходит о вашем конкретном проекте.
Простите, но я так и не понял, на основе чего вы сделали вывод "чушь собачья". Если это апелляция к авторитетам, к научным лычкам и званиям, извините, этого недостаточно. Принципы это не теоремы и не законы, которые должны быть доказаны на бумаге. Это консолидация опыта. В научной среде также есть принципы, простые, лаконичные, и не требующие научных выкладок с формулами. Везде они есть. И никто в здравом уме не назовёт их "чушью собачей".
На основе своего опыта и опыта коллег в проектах различного масштаба, где довелось участвовать за 25 лет карьеры.
Я здесь как-то уже просил адептов SOLID привезти в качестве примера ссылку на исходный код хотя бы одного реального проекта, построенного в соответствии с принципами, за которые они агитируют. Буду признателен и вам.
Очень странно конечно, что за 25 лет карьеры сложилось такое впечатление. У меня 20 лет карьеры, и тоже мог бы много чего рассказать.
Но вы не с той стороны подходите к вопросу. Вы когда-нибудь видели хоть один проект, который бы имел какой-нибудь значок типа "сделано в соответствии с принципами SOLID, DRY, KISS.. etc."? Я не видел. Хотя в рекламных буклетах каких-то проектах можно что-то подобное встретить. Но это не конкретная методология разработки, которая бы конкурировала или противопоставлялась другой. Это консолидированный совокупный опыт написания хорошего кода.
Очень многие разработчики, и даже в комментариях к этой статье, замечали, что с большим опытом разработки и с намерением писать хороший код, он итак получается SOLID-ным. Даже без цели делать его таковым. Это не методика, по которой вы пишите, как по лекалам. Это принципы, которые вы соблюдаете. Но к этом можно прийти неосознанно, или быстрее, но осознанно.
Я часто на собеседованиях задавал такой вопрос "зачем практически нужен SOLID?". Нет, я не спрашивал за каждую букву, мне хотелось услышать, зачем это нужно. Но. К сожалению, не все понимают зачем действительно нужен SOLID. "Чтоб писать хороший код"? Не совсем. Вот прям пишешь код и держишь в уме принципы SOLID? Ну нет конечно :) Или вообще заучили определения для собеседований. И только единицы осознают его применимость.
А применимость у него очень простая. На код ревью разработчики тратят значительно меньше времени, вместо долгого объяснения "на пальца", можно сказать, что вот этот код нарушает такой-то принцип, и это уже само по себе может быть плохо. Да, код не обязан строго следовать никаким принципам, он в первую очередь должен решать задачу. Но при всех равных, если нет причин ему быть другим, то нарушение принципов сигнализирует о проблемах. Это совокупный набор критериев качества, которые всеми понимаются плюс-минус одинаково, и резко сокращает время на обсуждение.
Новым разработчикам, без опыта, сложно писать сразу хороший сопровождаемый код. И принципы помогают хотя обозначить корректный путь. И да, можно искаверкать любые принципы и довести до абсурда. Что на самом деле приведёт к нарушению этих же принципов.
Может быть в вашем опыте, на ваших проектах, у вас все разработчики звёзды, вы на одной волне и все абсолютно одинаково мыслите, вы делаете всё одинаково, понимаете что значит "хороший код" одинаково, вам конечно не нужны никакие талмуды. Вам вообще ничего не нужно, вы уже программисты от бога, всё сразу пишите правильно, хорошо, эффективно, понятно и код идеально сопровождаемый. Просто потому что. Но я работал и работаю с разными людьми, и принципы очень помогают в работе. Не потому, что я на них молюсь, а потому что реально применяю их на практике в командах. И это благоприятно сказывается на разработке.
И после этого, вы заявляете "чушь собачья". Ну да, ну да. Ставим разработку на холд и топаем изучать труды по теориям типов. Потому как их толком никто не знает, а большинство и не слышало даже. Но они есть, и это замечательно. Только работают в узкой специализации.
Вы сами себе противоречите, разве нет?
>"Чтоб писать хороший код"? Не совсем.
>Это совокупный набор критериев качества, которые всеми понимаются плюс-минус одинаково, и резко сокращает время на обсуждение.
Ну т.е. по вашим же словам, SOLID это критерии качества кода (т.е. критерии того, что является хорошим кодом), и тут же заявляете, что SOLID-то не совсем про качество кода. По крайней мере со стороны воспринимается так, что от ваших собеседников вы ждете конкретной фразы/формулировки, которая вас устроит (как преподаватель), нежели сути ответа.
Но всё именно так. Для написания хорошего кода нужен опыт, практика, комплексное понимание технологического стека, используемого языка, самой задачи и сути решения. Если код не нарушает принципы SOLID (и другие), это вовсе не означает сразу, что он качественный. Но их нарушение скорее всего показывает наличие проблем в коде, так называемый "запашок", который требуется устранить.
Поэтому, автор статьи пускается в крайности, и говорит, а давайте я сейчас вам покажу, как легко сделать отвратительный код следуя принципам SOLID. И когда у него это получается, вывод: принципы совсем плохие, чего вы на них молитесь (как будто кто-то молится), давайте их выкинем. При чём принципов гораздо больше, чем SOLID. Если применить и другие, например связность/связанность, выяснится, что этот "солидный" но плохой код нарушает другие принципы. И таки МОЖНО с очень хорошей оценкой выявить проблемы кода, без индивидуализма.
Я часто на собеседованиях задавал такой вопрос "зачем практически нужен SOLID?".
Я бы ответил - чтобы самоутвердиться тимлиду. 30 лет в разработке.
Если вы думаете, что абстрактный принцип зачем-то вообще практически нужен, то покажите конкретные деньги, которые он приносит. С другой стороны, если вы можете посчитать конкретные деньги, то зачем абстрактный костыль?
Программирование, как профессиональная деятельность - это рубли, секунды и мегабайты (обычно в таком порядке).
Сколько конкретных денег приносит, например, уборщица? Есть процессы с прямым доходным выхлопом, а есть сопровождающие, поддерживающие, обеспечивающие, которые нельзя посчитать в "конкретные деньги, которые они приносят".
Программирование это автоматизация. Далеко не каждая автоматизация направлена на сокращения ручного труда или прибыль.
Я не думаю, зачем абстрактный принцип нужен. Я прекрасно знаю зачем он нужен. И вижу выхлоп в работе. Как по-вашему я должен относиться к заявлениям, что всё это чушь собачья, и не нужна, если я на опыте вижу обратное? Это вообще сюр какой-то нездоровый.
В зависимости от направления деятельности фирмы и конкретного сотрудника, последний может относиться к основному производственному персоналу (непосредственно создающему прибыль) или к вспомогательному (служащему для обеспечения основных функций). В программистской фирме программист будет приносить конкретные деньги, а уборщица обеспечивать его деятельность, а в клининговой фирме – наоборот. Но это всё, по большому счёту, просто способы организационной и финансовой оптимизации. Мы можем уборщицу в программистской фирме перевести на аутсоурсинг и посчитать, сколько готовы платить за её труд программисты и сколько она готова получать в качестве зарплаты. Вот и будет всё понятно в деньгах.
Что касается бухгалтеров, то я не исключаю, что они могут быть и правы. Автоматизация далеко не всегда имеет смысл.
"Чушь собачья" не в самих принципах, а в том, что именно эти 5, собранные ради красивого сочетания, выдвигаются вперёд остальных, не менее важных, и ещё и в не лучших формулировках. Это примерно как в школьной математике собрать вычитание, умножение, решение квадратного уравнения и теорему о равенстве углов при основании равнобедренного треугольника, а больше ничего не рассказывать, и требовать решать все задачи через этот набор.
Если это апелляция к авторитетам, к научным лычкам и званиям, извините, этого недостаточно
Действительно, куда же там всем этим учёным, в говне мочёным, до инфоцыгана, который даже не факт что хотя бы был архитектором, не то что код писал
А как должно быть? Люди в халатах с грамотами должны были выковать скрижали в жерле научной группы?
Да
Иииии я учил не пойми что, что группа челов придумала по приколу в переписке. Ииииии люди дрочат этим друг друга на собесах, а потом создают иерархии абстрактных фабрик, в которых чёрт ногу сломит. Ииииии у нас до сих нет доказательств, что Роберт Мартин работал на реальных проектах
По-моему, сейчас во всём этом начали сомневаться сильнее. Во-первых, в IT-индустрии кризис и сокращения. А раньше разрабы МБ мыслили: «SOLID? Ок, будет вам SOLID. Да что угодно будет. Хоть в жопу дам, хоть мать продам. Только деньги платите». А теперь этого нет. Кроме того, успело накопиться большое количество легаси с абстрактными фабриками, порождающими абстрактные фабрики, от которого волосы встают дыбом. Самый главный недостаток такого подхода — необходимость при добавлении какой-то одной фигни сделать изменения одновременно в 10-и местах на разных уровнях. А ещё весь этот дроч на переиспользование. Серьёзно, кто-то вообще умудрялся переиспользовать код бизнес-логики? Переиспользуется только технический код фрэймворков и библиотек, который уже был вынесен заранее кем-то другим
Вот интересно, если бы IDE были на 1 файл (открыл один, другой закрылся) и без перехода по клику: так-же бы размазывали всё по десяткам файлов или забили на принципы?
Вероятно бы сделали патч для ИДЕ для поддержки мульти-файловости? :]
Если бы у бабушки был ...
Можно еще пофантазировать. Вот еще одна мысль на подумать: "А если бы мы писали в нулях и единицах, мы бы все еще использовали ООП?"
Вы так говорите как будто речь идет о чем-то невероятном. Но по факту вы просто озвучили то, что уже было в человеческой истории и мы наверняка знаем ответ на ваш вопрос. Что было бы в таком случае? Предприимчивые люди разработали бы инструмент, который удобнее этой IDE =)
В функциональном программировании есть реальные теоремы, а многим концепциям соответствуют строгие математические объекты, для которых можно доказывать свойства. В императивном... ну спустя 60 лет, люди почти договорились, что вот так вроде красиво, а вот так чёт не очень. Принцип - это не закон, это вроде наблюдений, которые выразили словами, и относиться к ним нужно соответствующе.
Если бы SOLID был законом, а не заповедями, то статические анализаторы уже давно бы его блюли, имхо)
SRP
Радостно видеть, что автор понимает, что SRP - это принцип про людей (не только изначально, но и сейчас). То что этот принцип коверкают - это не проблема принципа. Если у автора есть аргументы против оригинальной версии SRP, было бы интересно их услышать.
OCP
Согласен с тем, что фанатично этому принципу не стоит следовать, но и утверждать, что он всегда бесполезен тоже нельзя. В любом деле возведение в абсолют каких-то рекомендаций и принципов обычно до добра не доводит.
LSP
Вот тут очень плохой пример получился. API чтения файлов должно отдавать поток байтов и ему должно быть по барабану зашифрованы они или нет. Логика дешифрования пишется как обертка для потока байтов, которая на выходе тоже отдает поток байт. Это позволяет а) очень легко тестировать каждую часть по отдельности б) добавлять обертки для сжатия/разжатия данных и других манипуляций в) использовать в качестве исходного источника потока байт не только файлы.
ISP
То же что OCP - не следуем фанатично, но и совсем о нем забывать нельзя.
DIP
Грустно наблюдать, что этот принцип понимают как "всегда используйте интерфейсы". Этот принцип на мой взгяд очень плохо себя проявляет в рамках одного проекта т.к. обычно процесс сборки проекта означает выпуск нового билда, который должен так или иначе пройти все этапы тестирования. Другое дело - разные библиотеки. Если у нас есть библиотеки, которые образуют цепь зависимостей A->B->C->D->E->F, и что-то меняется в F, то нам придется проводить тестирование и пересобирать библиотеки E, D, C, B, A. Если же мы введем промежуточную API библиотеку G, то сможем разорвать цепь зависимостей:
A->B->C-->G<--D->E->F
и изменения в F уже затронут только две библиотеки - E и D.
Вот некоторые примеры такой инверсии из мира java:
slf4j-api - фасад для подсистемы логирования
opentelemetry-api - API для записи метрик и трейсинга
jjwt-api - API для работы с jwt токенами
Мне непонятны алгоритмы Хабра - 64 лайка, но около 6 тысяч просмотров?
А что не так? У меня на статье с 10к просмотров 6 лайков. Подавляющее число пользователей не имеет возможности ставить лайки, а у тех кто может - количество лайков в день ограничено кармой.
Да и чтобы статья на хабре замотивировала кого-то поставить лайк - она должна прям хорошо зайти, эта статья зашла. К тому же ЦА здесь не новички, процент полноправных пользователей больше
Например, комментарии интереснее статьи.
Хотя лично я поставил лайк.
Когда я писал, я говорил не о комментариях, а о том, что статья имеет очень маленькую популярность, даже в поддержку писал, даже в поддержку писал
Пример для LSP кажется мне не слишком удачным. Там и базовый, и производный класс может выкинуть исключение о том, что "по каким-то причинам не удалось прочитать файл". Т.е. снаружи поведение производного не отличается от базового и не добавляет каких-то новых сюрпризов.
Отличная статья, хорошие примеры (хотя можно было и побольше) и описание. Полностью поддерживаю автора. Заранее хотелось бы добавить, что для ее понимания для начала надо понять сам SOLID, а уже потом то, что не нужно его везде вставлять. Хочется сделать отсылку к фильму "Всегда говори да".
В коде важна архитектура, и если принципы SOLID позволяют архитектуру создавать и поддерживать на долгой дистанции, то нужно эти принципы соблюдать для своего же блага. Оно не из-за хорошей жизни появилось. У меня есть опыт работы с кодом после, так называемых, «сениоров». Которые не пишут комментарии в коде. Которые любят KISS. Которые оптимизируют вызовы функций, что одна функция может выполнять три задачи в зависимости от вводных параметров. Уж лучше обмазаться классами, и без головной боли их менять когда нужно, чем тратить время на разбор спагетийного ада из-за сверх оптимизаций
А вы ни когда не думали во что превращаются ваши абстракции? В машинном коде это все равно ветвление. Но чем больше и сложнее абстракции, тем больше затраты на поиски по таблицам виртуализации. По информации из вики в реальных проектах ваша программа может тратить 50% времени на оверхеды из-за виртуализации. В итоге "правильный" код работает в два раза медленнее.
Во-первых, за пределами специфичных областей типа геймдева, эмбеда или системного ПО всем давно пофиг на производительность. Стоимость поддержки и скорость выхода на рынок важнее.
Во-вторых, часто узким местом вообще не код является. Если мои абстракные фабрики синглонов тратят лишнюю секунду на все эти виртуальные вызовы, но затем моя программа еще пять секунд ждет загрузки по сети большого блоба с данными, или чтения гигабайта данных с диска, или ответа от удаленного тормзного апи - то вообще пофиг на эту секунду. Ну потратите вы кучу времени, ну ускорите эту секунду, ну будет пользователь ждать ответа не 10 секунд а 9. Зато для вас время разработки новых фич замедлится из-за лапшеобразного нечитаемого кода.
SOLID - это формализованные непонятно кем идеи Дяди Боба. Формализованные и превращенные в догмы, криво и (имхо) неправильно.
Я лично не мог понять их смысл, душа противилась. Потом я прочитал книги Дяди Боба и мне все стало понятно. В том смысле, что стало понятно, что имел в виду Дядя Боб.
Не надо изучать SOLID. Читайте Дядю Боба. У него все понятно, логично, красиво и правильно. В частности, только после его книг мне стал понятен смысл и сила Spring.
Как минимум OCP и LSP это вообще не идеи дяди Боба.
Потом я прочитал книги Дяди Боба и мне все стало понятно.
Мне непонятно вот это
They are closed for modification. Extending the behavior of a module does not result in changes to the source, or binary, code of the module. The binary executable version of the module whether in a linkable library, a DLL, or a .EXE fileremains untouched.
Как вы расширяете поведение модуля без изменения бинарников?
Плагины — знаете, как они работают? Ваша любимая IDE без них по функциональности — сродни Notepad’у.
Как вы расширяете поведение модуля без изменения бинарников?
Рядом кладется другой бинарник, с изменённым поведением и возможно, использующий первый. Что приводит к DLL hell, огромному зоопарку версий, которые должны присутствовать в системе одновременно, и реализаций одного и того же в разных местах повторно - потому что просто не заметили, что нужный функционал в одном из расширенных модулей есть или потому что этот расширенный модуль с собой еще что-то ненужное тянет.
Мое личное мнение - это один из принципов, который наибольшее число collateral damage принес.
Все верно. Если надо добавить функционал в модуль, то мы текущий модуль не меняем, но делаем новый модуль, куда реализуем расширение, а переключение должно осуществляться в вызываемой программе. То есть, это не модуль должен знать, куда ему к примеру, выводить информацию, какая структура окружающего кода, а вызываемая система знает, какой интерфейс у исходного модуля и какой новый модуль нужно написать для расширенного функционала.
Текущие модули мы не меняем, а для нового функционала пишем новые модули.
Пример. Есть система. Есть модуль, который делает расчет. Расчет возвращается в окружающую его систему. Понадобилось, чтобы расчет форматировался определенным образом.
По старому и неправильному: мы меняем модуль расчета, добавляем код, который будет форматировать результаты. При этом этот модуль расчета должен будет знать детали реализации подсистемы вывода, необходимые форматы и кучу всего прочего.
Правильно: мы пишем новый модуль, который будет заниматься форматированием. Может, даже несколько модулей с одним и тем-же интерфейсом, но для html, json и так далее. Проводим в окружающей системе изменения для вызова форматирования результатов после выполнения расчета.
Если нужно расширить, чтобы система делала кроме расчета А еще и расчет Б, мы не меняем модуль с расчетом (который был только А, а мы даже не подозревали, что будет еще Б), а делаем еще один модуль с расчетом Б, а в вызывающей системе делаем switch и вызов (ну или применяем ООП).
Все верно. Если надо добавить функционал в модуль, то мы текущий модуль не меняем, но делаем новый модуль, куда реализуем расширение
И у вас на каждую новую фичу новая дллка или jar файл. Так вы и делаете, серьезно?
Был какой-нибудь класс Report, к нему добавили наследника AdvancedReport с новыми возможностями. В тот же самый модуль. В чем проблема?
В том, что в большом проекте эти наследования приводят к ООП-HELL. Огромное количество наследников, разобраться в их взаимосвязи и что откуда вызывается и какой путь выполнения - становится совершенно невозможно.
SOLID негативно относится к наследованию, и я этим совершенно согласен.
Выделение общего функционала следует делать не через выделение в parent, а через выделение в отдельный класс со своим интерфейсом. Spring этим и занимается.
Добавление функционала следует не через добавление наследника, а через композицию - выделение функционала в отдельный класс и либо иньекцией в наш класс, который будет это использовать, либо (что лучше, но надо смотреть), в окружение, которое вызывает наш класс и будет следом вызывать класс с дополнительным функционалом. Этим тоже занимается Spring.
На каждую новую фичу jar не будет, но новый class-файл - да, будет.
На каждую новую фичу jar не будет, но новый class-файл - да, будет.
Нарушаете заветы дяди Боба!
The binary executable version of the module, whether in a linkable library, a DLL, or a Java .jar, remains untouched
В том, что в большом проекте эти наследования приводят к ООП-HELL.
Ну пусть будет не наследование, а еще одна реализация интерфейса, это уже зависит от контекста.
Огромное количество наследников, разобраться в их взаимосвязи и что откуда вызывается и какой путь выполнения - становится совершенно невозможно.
Тоже самое можно сказать про огромное количество интерфейсов и их реализаций.
А еще многое зависит от проекта. Если это публичная библиотека ситуация одна. Если некий внутренний проект, ситуация другая. А у Мартина про это вообще ни словечка. ИМХО правильный подход это когда сравниваются разные способы реализации и выбирается наиболее уместный в данном конкретном случае. И в зависимости от конкретной ситуации - новый класс, новый наследник, новая реализация интерфейса, может банально новая перегрузка метода. А у Мартина просто дичь - нельзя менять бинарник!
SOLID негативно относится к наследованию, и я этим совершенно согласен.
Вспоминаются слова Альфа про котов: "Вы не любите котов? Вы просто не умеете их готовить!". Точно также и с наследованием: им надо уметь пользоваться.
Добавление функционала следует не через добавление наследника, а через композицию
Не всегда. Если это именно композиция (а не агрегация с чем-то, что можно использовать отдельно или в других кусках программы), и предполагаемая реализация добавляемого функционала, как и уже существующий его аналог в старом коде, активно используют вызовы внутренних методов объекта - то есть, наличиствует сильная cohesion - то лучше эти методы сделать protected и использовать для расширения именно наследование, а не композицию. Лично я в таком случае - если уже один из вариантов функциональности уже реализован в классе - предпочитаю разбить существующий код на два класса и использовать шаблон Template method: в базовом классе на месте используемой функциональности оставить вызов абстрактного метода, а код существующей функциональности - перенести в производный класс, в котором переопределить этот метод. Тогда новую функциональность можно разместить в другом производном классе, переопределив этот абстракный метод по-другому, и при этом - созранив возможность использования уже написанных методов базового класса (а если cohesion силная, то это заведомо потребуется). При использовании же в таком случае композиции пришлось бы выделять эти методы в отдельный интерфейс, который делать доступным для внедряемых классов. А это создает возможность возникновения паразитных связей (coupling), потому что обычно ограничить область видимости этого интерфейса только для внедряемых классов не удается.
Ни одна из этих идей не его. Вообще аббревиатуру SOLID ему подкинули в переписке.
SOLID (сокр. от англ. single responsibility, open–closed, Liskov substitution, interface segregation и dependency inversion) в программировании — мнемонический акроним, введённый Майклом Фэзерсом (Michael Feathers) для первых пяти принципов, названных Робертом Мартином[1][2] в начале 2000-х[3], которые означали 5 основных принципов объектно-ориентированных проектирования и программирования.
Это цитата из Вики.
А где-то есть переписка в открытом доступе? Мне только какие-то куски попадались.
Можно ли SOLID использовать не правильно? да. Это значит надо учиться правильно применять, вот и всё, отсюда и огромное количество статей, так как это не очень просто.
Не умение их применять очень удобно списывать на "солид переоценён", "солид не догма", "именно в моей задаче он не нужен" и тд
Имхо, очень часто встречаются ведь как раз обратные приведённым примерам ситуации - код написанный вообще без оглядок на всякие правила. И он тоже вызывает огромное количество проблем.
Смело-смело! Раньше за такое на костёр отправляли. Проблема в том, что Программирование - довольно юная наука, по сравнению с Физикой и Математикой, мы ещё не разобрались до конца, что хорошо и что плохо и в каких пропорциях.
Плюс, примеры Дядюшка Боб писал на Java, а адепты бросились слепо тянуть не только идеи, но и синтаксис, причём для других языков. Вон, в Си до сих пор нормально goto сделать в конец функции для освобождения памяти. Или в JS можно пяток функций вспомогательных сделать, а в джаве сразу класс, методы... а давайте наперёд подумаем, при том, что Хрустального шара предсказаний обычно у программистов нет. В питоне используют множественное наследие для Миксинов (примесей) - классический пример sorl.thumbnail для django-админки - подставляет превьюшки для изображений.
Когда-нибудь мы напишем учебники, которые на хороших примерах будут рассказывать про ООП, подходы, когда использовать, а когда - нет.
Плюс, примеры Дядюшка Боб писал на Java
Изначально он писал примеры на плюсах. И его первая книга была про плюсы.
Вон, в Си до сих пор нормально goto сделать в конец функции для освобождения памяти
break
, continue
, throw
и ранний return
- это в Java тоже есть, и широко применяется.
А ещё вот, хоть это и редкость, и у многих людей от непривычки мозг вскипит:
mmark: if (condition) {
dep0.call();
if (!dep0.verify()) {
break mmark;
}
dep1.call()
dep2.call();
}
Или в JS можно пяток функций вспомогательных сделать, а в джаве сразу класс, методы
А как же Function<String, Integer> helper = a -> Math.pow(a.length(), 1337);
?
Церемонии чуть больше, но это только из-за типизации. На кондовом Typescript оно столько же места займёт.
В питоне используют множественное наследие для Миксинов
Прямо Mixin можно сделать через AOP какой-нибудь, но честно лучше всё-таки без него, их потом дебажить умучаешься.
В примере в FileReader
принцип Лисков не соблюдается ни в первом, ни во втором варианте.
Вы поменяли класс на интерфейс, но EncryptedFileReader
все еще усиливает предусловия (требует зашифрованный файл), в то время как IFileReader
никак не описывает в контракте возможность исключений из-за того что файл "какой-то не такой".
Проблема здесь, на самом деле, в нарушении принципа SRP. EncryptedFileReader
занимается одновременно и чтением файла, и дешифровкой. Эти две функциональности стоило бы разделить.
Как же вовремя вы с этой статьей! Я вот буквально то же самое начинал писать для второй части статьи про заговор зраработчиков, теперь смогу просто на вас сослаться!
Плохая практика — использовать лучшие практики исключительно ради самих лучших практик.
Честно говоря, уже введение заставило посмотреть на статью с некоторым прищуром. В одну кучу свалено невежество и осознанный подход. Потому что вообще ко всему можно подходить и с позиции "священной коровы" и с позиции осознанного подхода. Но! Первичным было именно стремление делать дело и делать его хорошо. Те, кто вкладывает свои усилия и внимание в создание фреймворков, подходов, написание книг и т.п., в первую очередь, не священных коров плодят, а хотят сделать свой вклад в качественную работу. Это небольшое замечание по форме.
" Принципы SOLID, предложенные Робертом Мартином, давно стали одной из таких "священных коров"." - что-то я вот в этом как то сомневаюсь.
Просто для примера, если на ревью на ваш вопрос вам отвечают: "это по СОЛИД" - на этом все заканчивается? Что-то мне подсказывает, если эта фраза ничего не объясняет, то ничего и не заканчивается, а начинается конкретное выяснение сути вещей. И что-то мне подсказывает, если Солид трактуется неверно, то это очень быстро станет ясно. А все просто потому, что Солид как проверенный годами подход позволяет выявлять как раз такие случаи говнокода.
Принцип единственной ответственности
В самой книге Мартин сразу же вводит темин "актор". Т.е. это уже не про людей. И тут сразу имеет смысл вспомнить, что мы говорим не о создании "модуля" в вакууме, а о проектировании программной системы. И любой принцип нужно рассматривать в контексте того как он помогает эту систему создавать.
Например, еще 20 лет назад была придумана такая штука как UML и UP, т.е. универсальный язык моделирования и универсальный процесс проектирования. Которые наглядно показывают, что программная система в любом случае имеет разные итерационные этапы разработки.
И, например, на начальных этапах составления требований уже присутствуют актеры:
"Актеры – это роли, исполняемые сущностями, непосредственно взаимодействующими с системой."
Выделяя на начальном этапе актеров и варианты использования системы мы как раз и получаем исходные данные для принципа единственной ответственности.
Поэтому "изначальная идея" на данный момент подкреплена опытом и теорией и она далеко не про людей, что бы это ни значило, потому что актером может быть не человек.
"Однако сегодня этот принцип часто интерпретируется совершенно иначе — как требование "разбивать класс на части, если он делает слишком много разного" или даже "делать классы маленькими"." - небольшое замечание по форме. Кем интерпретируется? Само по себе разбиение на части - это не солид. Если программист не объясняет зачем это надо никакого отношения к солиду это не имеет.
Таким образом получается, проблема не в SRP. SRP как помогал проектировать чистую архитектуру так и помогает. Проблема как раз в том, что этот принцип могут не понять.
Принцип открытости/закрытости
Тут вы большой акцент делаете на "предсказании будущего". Все принципы содила они про чистую архитектуру, т.е. про разумный подход к выбору конкретных решений для конкретных задач и для будущих задач. Именно про это чистая архитектура, когда требования к продукту изменяются и вам приходится обновлять поведение системы. Так вот, если у вас куча говнокода, то такое обновление будет крайне затратно, а если оно, например, поддерживает солид, то уже связность будет такая, что обновлять поведение программы нужно будет в конкретных ограниченных местах. Солид весь про это, а не только этот принцип. Иными словами уже при обсуждении SRP можно было говорить про это.
Тоже есть небольшое замечание по форме. Вы выбрали пример с одним классом Order, и кодом, который должен вести себя по разному в зависимости от type. Тут сразу встает вопрос, какая у вас стояла задача? Я это вижу как для разных типов Заказов должно быть свое поведение. Тогда почему у вас разные типы заказов обслуживаются одним классом? Просто для примера, если бы вы тут использовали шаблон проектирования "Посетитель", то все описанные в вашем случае задачи были бы решины. Вы получили бы ясную иерархию классов заказов и ясный механизм отделения ненужной в классе логики внурь другого класса. И любые дополнительные параметры так же были бы ответственностью другого класса. И никаких if не было бы, потому что разные типы заказов создаются НА МЕСТЕ, и имею одинаковый интерфейс.
Я это все к тому, что Солид - это хорошо, но и шаблоны проектирования тоже стоит в голове держать. ВСЕ проверенные временем шаблоны проектирования написаны по СОЛИД.
Принцип подстановки
Я, честно говоря, не понял вообще посыла этой главы. Потому что когда я дочитал вот до этого: "При этом изначальная версия с наследованием, хоть и нарушает LSP, более практична и понятна. " - я прям растерялся. Т.е. вы пишете, что объект с одним интерфейсом, который он получил от родительского класса, но при этом его не поддерживает "более практична и понятна"? А вы понимаете, что начнет ваш код использовать другой разработчик и он исходников не видел. И для него автоматом поведение вашего кода станет непрактичным и непонятным просто по факту его работы.
Этот принцип в первую очередь про то, что везде где заявлен родительский интерфейс можно использовать любой объект дочернего класса. Поэтому если вы этого не подразумеваете, то никакого отношения к данному принципу ваш код не имеет. Он может быть как вы пишете практичным, удобным, но к этому принципу он отношения не имеет. Поэтому лучше не смущать людей и не наследовать то, что будет работать по другому.
И опять замечание по форме, а зачем вообще метод чтения вы наделили ответственностью что-то проверять? Он должен только читать файл. Никаких проверок там быть просто не должно.
Дальше я уже пропустил. Для меня статья получилась не очень интересно и сразу перешел к концу.
"Вместо слепого следования принципам SOLID, нужно всегда учитывать контекст конкретного проекта. Маленькому проекту не нужна сложная архитектура корпоративного приложения." - тут тоже замечание по форме, Солиду просто по определению нельзя следовать слепо. Солид - это про осознаный вдумчивый подход к написанию надежных, расширяемых программ. Если что-то делается слепо - это уже автоматически не про Солид.
"Если код можно написать проще — его нужно писать проще. Не стоит создавать сложные абстракции только потому, что "так говорит SOLID". - тоже небольшое замечание по форме. Ничего подобного Солид не "говорит".
"Иногда лучше начать с простого решения и усложнять его только при возникновении реальной необходимости, чем пытаться предусмотреть все возможные сценарии заранее." - просто для примера, вы начнете писать код сразу и все таки чуть чуть подумаете, а стоит ли выбрать фреймворк, а стоит ли выбрать именно этот фреймворк и т.п.? Характер всех подобных размышлений он как раз про попытки что-то предусмотреть. Иными словами, уже с самого начала вы будете их предпринимать и будете выбирать то, что вам лучше подходит. Так вот Солид ровно про тоже самое.
"На код ревью вас ожидает жесткая порка, если, не дай бог, класс делает сразу две вещи." - логичный вопрос. И у вас должен быть на него ответ. А вы предлагаете все принимать на веру и вопросов не задавать? Речь идет не о жесткой порке, а о понимании ревьювера того, что вы осознаете что делаете.
"Возможно, нам стоит перестать использовать сам термин "принципы" применительно к SOLID. Это слово подразумевает некие универсальные истины, которым нужно следовать всегда и везде." - нет. Оно в первую очередь подразумевает, что вы понимаете смысл всего этого и только после этого следуете по возможности всегда и везде.
"Вместо этого имеет смысл говорить о "паттернах решения проблем" — это лучше отражает их истинную природу как инструментов, которые полезны в определённых ситуациях, но не являются универсальным рецептом." - Солид - это универсальные принципы, которые могут служить отправной точкой для обсуждения качества кода. Они как раз и являются паттернами решения проблем.
Очень странно на фоне всего что выше, видеть ваше заключение. Если действительно Солид перестал использоваться осознанно нужно какое-то реально этому подтверждение. Я такого в своем опыте не помню, поэтому мне было бы интересно увидеть какую-то статистику. Потому что то, что я прочитал в вашей статье вызывает больше вопросов чем дает ответов. Вопросов в основном к вам.
Возьму SRP:
И, например, на начальных этапах составления требований уже присутствуют актеры:
"Актеры – это роли, исполняемые сущностями, непосредственно взаимодействующими с системой."
Проблема в том, что для этого надо определить, что такое "роль". И тут уже будет конфликт.
Вот например задача формирования и печати платёжки (пример практически от Фезерса, из его книги по тестированию). Пока мы знаем, что единственный способ вывести её это напечатать на принтер, причём чисто буквенный (представим себе, что мы на ЕС-1022 без дисплеев), то это у нас одна задача. А если мы переместимся на PC с экраном и разными типами принтеров (матричный для чернового формата или когда надо быстро и дёшево, и лазерный для VIP-клиентов), то у нас уже формирование в промежуточном виде - просто данные - одна задача, а печать или вывод на экран - другая. И пока мы не разделили их в ТЗ, потому что кто-то сверху подумал или потому что программисты сказали "а накойхер три идентичных формирования данных?", то это не будет осознаваться. Фезерс тут проводит границу на "а как вы будете тестировать его по частям?", и это частый и полезный вариант.
SRP не даёт ответа на то, как проводить границу. Он просто говорит про "одну роль". Это полезный принцип, да. Но сам по себе он ничего не даёт.
Аналогично для остальных принципов.
В этом и проблема всех(!) 5 компонент SOLID, ну кроме того, что принципы выбраны произвольно ради красивой комбинации. Они не просто выбраны, они ещё и неверно или недостаточно определены. По-нормальному согласно SOLID вообще ничего нельзя утверждать.
Инфоцыганство в чистейшем виде, "успешный успех" в формате "я тут стратегию разрабатываю".
Во первых, никакого конфликта с ролями не будет. UML - это язык моделирования. Т.е. просто по определению речь идет о моделях, т.е. полезных для решения данной задачи абстракциях сущностей реального мира. Они не с потолка взяты, не выдуманы что бы было интересно, а выводятся из сущностей реального мира. Поэтому и роли так же выводятся из условий реального мира.
Во вторых, пример который вы выбрали уже в реальной истории был и был признан проблемой еще в 80х. И именно при решении таких вот проблем и появились принципы Солид, шаблоны проектирования т.д. и т.п. SRP даст ответ как проводить границу, если снабдить достаточным количество данных. SRP работает как на этапе проектирования, так и на этапе кодинга
"В этом и проблема всех(!) 5 компонент SOLID, ну кроме того, что принципы выбраны произвольно ради красивой комбинации. Они не просто выбраны, они ещё и неверно или недостаточно определены. По-нормальному согласно SOLID вообще ничего нельзя утверждать. " - на самом деле нет. Эти принципы сформировались сами после десятилетий кодинга. А вот записаны в такой форме были действительно с определенной целью. И утверждать анализируя систему по Солиду можно вполне конкретные дискуссионные вещи.
UML - это язык моделирования. Т.е. просто по определению речь идет о моделях, т.е. полезных для решения данной задачи абстракциях сущностей реального мира.
Речь идёт о написании кода. UML может применяться для предварительного моделирования, но всё равно результат представляется в коде. И работать программисту именно с кодом. UML может быть критичен для понимания (за счёт того, что графичен), но всё равно второстепенен.
Во вторых, пример который вы выбрали уже в реальной истории был и был признан проблемой еще в 80х.
Возможно, ещё раньше. И на что это влияет?
SRP даст ответ как проводить границу, если снабдить достаточным количество данных. SRP работает как на этапе проектирования, так и на этапе кодинга
SRP ничего не даёт, потому что сформулирован так, что без ящика водки не разобраться. Вы явно пропустили, что я писал: что что такое "single responsibility", никому не понятно, пока не будет уточнено в задаче, что с чем должно сочетаться и чем грозит нарушение этого принципа. Фактически, каждый случай, что именно там single, доказывается, математическими словами, "от противного" - выводится не из абстрактного знания, а из представления проблем от его нарушения.
А вот какие именно проблемы - формулируется в GRASP, а не в SOLID. Опять же, "от противного", но этот противный там чётче и яснее.
А вот записаны в такой форме были действительно с определенной целью.
Именно. Чтобы "uncle Bob" имел не хлеб с маслой, а икру и яхту (или что там у него вместо яхты).
И утверждать анализируя систему по Солиду можно вполне конкретные дискуссионные вещи.
Можно. Но заведомо эффективнее это делать с помощью других комплектов принципов.
"Речь идёт о написании кода...." - нет. Речь идет о решении задачи путем использования определенной программы. До того как вы начнете писать код вы должны будете решить, что вы будете делать. Неважно uml это или что-то еще, но вы будете думать о своей модели данной задачи. И программист еще до написания кода должен иметь модель того, что он будет реализовывать. Иначе в принципе быть не может, потому что код это проекция вашей мысли на язык машины.
"Возможно, ещё раньше. И на что это влияет?" - я наглядно показал на что это влияет. Могу еще раз повторить, была причина - это вот задачи подобные вашей и получилось следствие - это принципы типа SOLID С позиции SOLID то, что вы написали это пройденный этап. А вы привели пример как будто это что-то о чем еще не думали и что-то что нельзя проанализировать с позиции принципов SOLID. Но исторически все иначе. Эти задачи уже были решены и на основе решений подмечены принципы.
"SRP ничего не даёт, потому что сформулирован так, что без ящика водки не разобраться. Вы явно пропустили, что я писал: что что такое "single responsibility", никому не понятно, пока не будет уточнено в задаче, что с чем должно сочетаться и чем грозит нарушение этого принципа. " - вы понимаете, что принцип как раз и применяется к конкретной задаче? И более того, он будет применятся на этапе разработки ПО используя конкретные данные, которые уже есть. Это принцип организации кода, т.е. что бы его применять у вас уже должно быть то, что вы будете организовывать.
"Фактически, каждый случай, что именно там single, доказывается, математическими словами, "от противного" - выводится не из абстрактного знания, а из представления проблем от его нарушения. " - причем тут абстрактные знания? Принципы Солид применяют к конкретных данным конкретной проблемы. Модель хоть и является абстракцией, она тесно связана с поставленной задачей. Это не какая-то абстракция взятая с потолка - это упрощение конкретной задачи.
"А вот какие именно проблемы - формулируется в GRASP, а не в SOLID. Опять же, "от противного", но этот противный там чётче и яснее. " - после вашего признания, что вы не понимаете SRP странно видеть подобные утверждения. Т.е. вы не понимаете, что такое S, но утверждаете, что проблемы "в SOLID" не сформулированы. Я бы тогда выкинул все упоминания SOLID и остановился только на GRASP.
"Именно. Чтобы "uncle Bob" имел не хлеб с маслой, а икру и яхту (или что там у него вместо яхты). " - вы видимо плохо понимаете масштаб того о чем говорите. Например, все шаблоны проектирования которые вы используете проходят проверку на принципы SOLID. Не важно есть такая аббревиатура или нет, говорит о ней кто-то или нет, но эти принципы следуют из такого кода. Не наоборот. Еще раз, их не выдумывали, это то, что реально существует.
"Можно. Но заведомо эффективнее это делать с помощью других комплектов принципов. " - так там тоже все дискуссионное. Просто для примера:
Information Expert Шаблон определяет базовый принцип распределения ответственностей. Обязанности должны быть назначены объекту, который владеет максимумом необходимой информации для выполнения обязанности. Такой объект называется информационным экспертом.
Кто наделяет объект максимумом необходимой информации? По какому принципу это происходит?
Что должно входить в обязанности? По какому принципу выбираются обязанности?
Если я вас правильно понял, эти ответы уже должны содержаться в описании данного принципа. Но этого нет. Так чем это лучше SRP? Уже не эффективнее.
И программист еще до написания кода должен иметь модель того, что он будет реализовывать. Иначе в принципе быть не может, потому что код это проекция вашей мысли на язык машины.
Вообще-то модель присутствует далеко не всегда, мягко говоря. Достаточно часто срабатывают и другие подходы: например, просто начать писать, втыкая метки TODO во все места, где можно и нельзя, потом расширяя, и уже по результату написания кода начиная понимать, где какие обобщения и абстракции вводить, что надо выносить в структуры, классы, где подставлять базовые классы, и всё такое. Я достаточно часто в таком режиме работаю и он меня не подводил, если я вообще в принципе представляю себе, как надо решать на глобальном уровне.
Модель в голове возможна только в самых простых и очевидных случаях. В остальных, как правило, проходит несколько PoC, которые пишутся так, что из них можно сделать что-то полезное, но можно и просто выбросить, если не подошли. И тот PoC, который показал хоть что-то полезное, идёт в развитие.
Не знаю, как у вас получается, что сначала есть полная мысль, а потом из неё порождается код. Может, в некоторых доменах такое и возможно. Но я с ними незнаком.
А вот для того, чтобы легко преобразовывать промежуточные состояния в следующие версии, и нужно соблюдение некоторых принципов. В какой-то мере это и обсуждаемое, но не как главное.
я наглядно показал на что это влияет
Я честно пытался найти в ваших репликах. Не нашёл.
Это принцип организации кода, т.е. что бы его применять у вас уже должно быть то, что вы будете организовывать.
О! А теперь вы играете за мою сторону? Пишем код. На каком-то этапе (чем раньше, тем лучше, но это как повезёт) осознаём, что тут лучше разделить на разные сущности и прописать взаимодействие между ними. И вот тут разделение происходит по одной из двух причин:
1) Неподъёмно-зашкальная сложность конструкта - что хочется разделить даже искусственно, потому что есть возможность сгруппировать.
2) Вместо одного фиксированного варианта из сторон взаимодействия - требуется подставлять в зависимости от неважно чего (но реальной потребности) две или больше. Это включает в себя и тестирование: стабы, моки, как бы их сегодня ни звали - и реальные применения, как в примере с разными типами принтеров.
Но, и снова, должен быть код и должно быть понимание, что его надо разделять. Ну или, по-вашему, кода ещё нет, но есть гарантированное на всех этапах представление, что там должно быть (в общем случае - не верю, но в частном - естественно, бывает).
Т.е. вы не понимаете, что такое S
Ну я, очевидно, понимаю его иначе, чем вы. Если вы от этого считаете, что я его не понимаю... извините, это не ко мне.
Например, все шаблоны проектирования которые вы используете проходят проверку на принципы SOLID.
Серьёзно? Так уж и все? Мощная телепатия, однако. Может, вы тогда назовёте хотя бы 5 (магическая цифра) "шаблонов проектирования", которые я использую?
Если я вас правильно понял, эти ответы уже должны содержаться в описании данного принципа.
Нет. Ответы в задаче. Но эти принципы позволяют чётче рассмотреть вопрос и сформулировать ответ, чем туманный SRP.
Что такое модель? Что значит "просто начать писать"? Во что вы собрались втыкать TODO? Даже если перед написанием кода вы выбираете самое грубое и примитивное представление о том, что будете делать прямо сейчас - это уже модель, которую вы будете реализовывать. То, что вы дальше будете ее уточнять, декомпозировать, заменять и т.п. - это следующие шаги.
Модель есть всегда. Вы не можете взять и начать что бы то ни было с написания 100 ифов или циклов взятых с потолка они все будут иметь отношение к вашей задаче. Если у вас в голове мутная комкообразная модель такой будет первый код. Дальше могут быть следующие шаги отталкивающиеся от этих результатов.
"Я честно пытался найти в ваших репликах. Не нашёл. " - могу только сказать, что как то странно и плохо вы искали. Все просто в лоб обозначено.
"О! А теперь вы играете за мою сторону? Пишем код. На каком-то этапе (чем раньше, тем лучше, но это как повезёт) осознаём, что тут лучше разделить на разные сущности и прописать взаимодействие между ними. И вот тут разделение происходит по одной из двух причин: " - нет. Это не так работает. Сначала бизнес договаривается с менеджером что нужно сделать. Потом менеджер договаривается с аналитиком и распиливает это все на конкретные бизнес-задачи. Потом конкретные бизнес-задачи распиливаются до задач на конкретных специалистов. Конкретные специалисты дают свой фидбек и может потребоваться дополнительная конкретика. Нормальные аналитики и прецеденты опишут. И актеры там будут обозначены. Поэтому писать вы код будете уже не с нуля. И даже если эту работу никто не сделал, вы ее сами проведете пусть и в сильно более усеченном виде. Вот к этому всему уже будут применяться принципы солид, а не только к исходному коду.
Можно как вы пишите отложить принятие решения и писать код с того, что пришло сверху не думая деталях модели. Но это именно откладывание на потом и все равно уточнять модель будете.
"Но, и снова, должен быть код и должно быть понимание, что его надо разделять. Ну или, по-вашему, кода ещё нет, но есть гарантированное на всех этапах представление, что там должно быть (в общем случае - не верю, но в частном - естественно, бывает). " - всему свое время. Часть работы будет проделана до написания кода. Часть работы во время написания кода, поэтому процесс разработки ПО и является итерационным.
"Серьёзно? Так уж и все? Мощная телепатия, однако. Может, вы тогда назовёте хотя бы 5 (магическая цифра) "шаблонов проектирования", которые я использую? " - тут все несколько иначе. Вы можете вообще их не использовать. Поэтому вы можете назвать, что вы используете и мы можем рассмотреть что это.
"Нет. Ответы в задаче. Но эти принципы позволяют чётче рассмотреть вопрос и сформулировать ответ, чем туманный SRP. " - еще раз, я привел описание принципа, которое не менее туманное, а может и более чем SRP. Вы поняли что мне ответили? Цитирую: "ответы в задаче", а теперь цитирую вас же в отношении принципа единственной ответственности: "SRP ничего не даёт, потому что сформулирован так, что без ящика водки не разобраться. Вы явно пропустили, что я писал: что что такое "single responsibility", никому не понятно, пока не будет уточнено в задаче, что с чем должно сочетаться и чем грозит нарушение этого принципа. "
Т.е. и тут ответы в задаче и там ответы в задаче, но туманные почему только Солид.
Open-Closed Principle изначально (лихие 1990-е) был про наследование реализации, а не интерфейсов. В типовом тиражируемом энтерпрайз-продукте продуманный и вылизанный "стандартный" класс открывается разработчикам-"конфигураторам", которые создают свои подклассы под требования конечных клиентов.
Согласился бы однозначно, но вот "продуманный" и "вылизанный" в "типовом тиражируемом энтерпрайз-продукте" это, как правило, нереальная фантастика :(
Это скорее для "коробочного" продукта на открытом рынке, и то начиная с версии этак 5-й...
В мире разработки программного обеспечения существует множество "священных коров"
Не в "мире разработки", а на хабре и в воспаленных умах отдельных фантазеров.
А в мире разработки программного обеспечения все хорошие практики (солиды, киссы, драи, чистые коды и т.д.) скорее стандарт де-факто. Они скорее выполняются, чем игнорируются. Если не буква, то дух их присутствует.
Я не знаю, что там у всяких питонистов или железячников, но если тыкнуть в какой-нибудь популярный продакшн-реди софт из мира энтерпрайза, то я на 99% уверен, что он более вероятно написан с учетом лучших практик, чем с их отрицанием. Да, отдельные эксцессы говнокода там будут 100%, но именно В ЦЕЛОМ состояние ближе к феншую. Поэтому я смотрю на все попытки сокрушить то солид то чистый код где-то с иронией, а где-то уже с усталостью,
Я не знаю, что там у всяких питонистов или железячников
За всех железячников сказать не могу, только про себя и свой опыт могу. На первом месте чаще всего быстродействие, то есть необходимость выполнения требований по частотам. Толку нет от красивого кода, если он разводится максимум на 50 МГц, а в требованиях - 150 МГц. Второе место - это экономия блочной памяти. Её обычно не так много и бездумно транжирить ёё не стоит. Ну а SOLID? Нет у нас никакого солида. И ООП у нас тоже нет)
Для меня одна из загадок мироздания - это что делают железячники в темах про SOLID. Они почему-то там исправно отмечаются, неизменно заявляя, что вот этот ваш чистокодовый солид им ну никак не подходит. Непонятно, кто его им навязывает. Можете показать статью, книгу или выступление на конференции, где бы поясняли необходимость следовать ООПшным практикам при программировании контроллера LED-ленты? Я таких кейсов не знаю, если что.
Напоминает ситуацию, когда в чат любителей внедорожников врывается любитель шоссейно-кольцевых мотогонок и начинает пояснять, что ураловские шины ну никак не подходят его мотоциклу. Ну да, не подходят - факт. Но никто и не предлагал вроде.
Для меня одна из загадок мироздания - это что делают железячники в темах про SOLID
Читают, изучают практики, применяемые в смежной для них области и немного делятся своим опытом.
Разве вам не интересно было бы узнать, как на уровне схемотехники работает контроллер памяти или MAC-ядро Ethernet? И почему там в первую очередь идет борьба за мегагерцы и попытки выжать каждый такт, чтобы железо работало на все 100 процентов?
Думаю, что программистам было бы крайне непросто работать и писать код, если бы после обращения к RAM ответ от неё приходил бы только через секунду, но зато всё по солиду было бы сделано)
Разве вам не интересно было бы узнать, как на уровне схемотехники работает контроллер памяти или MAC-ядро Ethernet?
В статье про практики из энтерпрайза? Уж извините, но не особо интересно. Всему свое время и место.
Думаю, что программистам было бы крайне непросто работать и писать код, если бы после обращения к RAM ответ от неё приходил бы только через секунду, но зато всё по солиду было бы сделано)
Сюда вместо солида можно что угодно подставить. Ну типа если б все даташиты на свете были на шумерском языке, то тоже непросто. А если б на клавишах ноутбука были иголки, то вообще кранты. Но ни то ни другое вашему цеху никто не навязывает. Как и солид.
Прочел ваш комментарий и вспомнил. Пришел я как то на проект, где люди пытались затащить гексагональную архитектуру. И все это непотребство пытался вытянуть один техлид, который до этого писал на С и занимался геймдевом) Человек яро отрицал необходимость чистой архитектуры и SOLID в этом подходе. Постоянно упарывался в преждевременную оптимизацию.
Я с вами согласен с одной поправкой на размер кодовой базы. Для встраиваемых систем она, как правила, невелика 10-100К строк кода на Си. По мере приближения к 1М становится выгодной организация кода в ООП-стиле. Кстати, накладные расходы по использованию Си++ и виртуализации не более 5-10%, мы тестировали на довольно типовом проекте эмулятора, когда новый проц гоняет старый микрокод.
Ну, и принципы SOLID они, скорее, не ООП, а здравый смысл. В Си тоже приходится передавать функциям разные типы через void*, просто в ++ этим занят компилятор.
Я с вами согласен с одной поправкой на размер кодовой базы. Для встраиваемых систем она, как правила, невелика 10-100К строк кода на Си. По мере приближения к 1М становится выгодной организация кода в ООП-стиле.
Или по мере организации кода в ООП-стиле размер кодовой базы приближается к 1М?
но если тыкнуть в какой-нибудь популярный продакшн-реди софт из мира энтерпрайза, то я на 99% уверен, что он более вероятно написан с учетом лучших практик, чем с их отрицанием
Первый фундаментальный закон кибернетики заключается в том, что разнообразие сложной системы требует управления, которое само обладает некоторым разнообразием.
Проблемы, которые встают на уровне энтерпрайз имеют большую комплексность и вариативность. Поэтому их решения не могут быть описаны таким простым языком, как SOLID, KISS, YAGNI и прочим народным фольклором.
Поэтому сборники лучших практик в энтерпрайзе: что-то вроде ISO/IEC 27001, Enterprise Integration Patterns, и т.п. - все они представляют собой очень толстые документы или книжки. Такие, что простым землекопам их как правило читать лень, некогда и дорого.
Нет, сами по себе такие "принципы" в отдельности имеют право на существование - равно как пословицы, прибаутки или анекдоты. Порой анекдот, рассказанный к месту, может описать точку ситуации лучше любой толстенной книги.
Но гипотетическое решение заменить несколькими анекдотами весь накопленный багаж знаний и опыта в области архитектуры и дизайна софта было бы уже само по себе анекдотом, да?
А ведь в некоторых кругах это обсуждают на серьёзных щщах, начиная прямо с тестирования сотрудников при приёме на работу. Тут мне становится страшновато за будущее.
Вы занимаетесь подменой тезиса. Я не говорил, что
их решения могут быть описаны таким простым языком, как SOLID, KISS, YAGNI и прочим народным фольклором.
ИЧСХ никто так особо и не говорит. Эти практики - просто советы, а не всеобъемлющие инструкции. Я вообще другое утверждаю. Если возьмем кодовую базу хорошо функционирующего софта из области энтерпрайза, то обнаружим, что она скорее следует лучшим практикам, чем отвергает их. В том числе всем или некоторым практикам SOLID. Поэтому пытаться их отменить глупо - они просто работают.
Если возьмем кодовую базу хорошо функционирующего софта из области энтерпрайза, то обнаружим, что она скорее следует лучшим практикам, чем отвергает их. В том числе всем или некоторым практикам SOLID. Поэтому пытаться их отменить глупо - они просто работают.
Мне нравится ход ваших рассуждений: красивый переход от "просто советы", до "просто работают". Возьму на вооружение когда нужно будет продать какой-нибудь хлам. Неспроста же он есть в любой кодовой базе - вещь в хозяйстве абсолютно незаменимая).
Лучшие практики, применененные не к месту, будут таким же плохим решением как и все остальные. Т.е. их качество не является каким-то абсолютом, а в каждом конкретном случае определяется контекстом конкретной задачи.
Давайте лучше с другой стороны зайдём - покажите крупную кодовую базу, в которая вообще не следует практикам SOLID.
Ещё показательней будет пример кодовой базы, из которой эти практики выкинули за ненадобностью.
В тред с ноги врывается 1С! )
Живут себе люди без DI и абстрактных фабрик, быдлокодят потихоньку, на хлеб с икрой зарабатывают и себе, и нанимателям.
без DI
Это как? Откуда там берутся зависимости в коде - каждый раз новый объект сервиса создаётся?
без абстрактных фабрик
SOLID не требует наличия абстрактных фабрик, это вещь ортогональная.
И как там с остальными буквами?
И как там с остальными буквами?
Никак. В 1С нет пользовательских типов: https://v8.1c.ru/platforma/sistema-tipov/
Собственно, и SOLID'а там нет.
Я видел и даже имел несчастье сопровождать объектную систему с виртуальными функциями на bash. При отсутствии в нём пользовательских типов.
Был бы тьюринг-полный язык, а место и для ООП, и для, простите, SOLID - найдётся.
Я видел и даже имел несчастье сопровождать объектную систему с виртуальными функциями на bash. При отсутствии в нём пользовательских типов.
Абстрагируемся от вопроса "зачем", хотя про "как" - с удовольствием почитал бы статью от вас (видимо, эрзац-виртуальными функциями выступал импорт из соседних скриптов по условию? или переопределяли функции на лету по условиям?)
Что забавно, встроенный язык 1С не позволяет даже таких "шалостей", типа условных импортов или переопределений. И функций первого порядка он тоже не позволяет. На выходе все реализации "ООП в 1С", которые я видел, сводились к префиксу у методов по типу РозничныйЗаказ_ПосчитатьСтоимость(Заказ) vs ОптовыйЗаказ_ПосчитатьСтоимость(Заказ).
Ну и про место для SOLID... Не, я понимаю, что-то совсем эзотерическое можно попытаться выдавить из себя и родить, но в терминах языка 1С проблематика SOLID не описывается.
Принцип подстановки Лисков (ввиду отсутствия наследования и даже интерфейсов) выродится в "не передавайте в метод то, для чего он не предназначен", а уж как сформулировать инверсию зависимостей - я вообще не представляю.
хотя про "как" - с удовольствием почитал бы статью
Не хочу учить плохому:)
видимо, эрзац-виртуальными функциями выступал импорт из соседних скриптов по условию? или переопределяли функции на лету по условиям?
Примерно. Грубо говоря, red_cow.sh и blue_cow.sh включали в себя cow_base.sh, base определял cow_moo(), cow_eat(), cow_poop() и так далее, конкретные скрипты переопределяли отдельные функции. Работало там такое, естественно, в условиях, что каждого "класса" не более одного "объекта", это было пригодно для задачи. Но ничто не мешает придумать аналог, где будут работать для каждого по-своему - объект будет "массивом" разнотипных элементов, включая имена функций (чтобы не включать сами функции, имена зовутся через eval). Метапрограммирование через eval с построением аргумента через конкатенацию строк я тоже видел.
Что забавно, встроенный язык 1С не позволяет даже таких "шалостей", типа условных импортов или переопределений. И функций первого порядка он тоже не позволяет.
Ну в C тоже можно получить работающую систему без таких возможностей. Хоть ссылки на функции (как бы они ни были сделаны) позволяет? Если нет - тогда да, сложно.
Хоть ссылки на функции (как бы они ни были сделаны) позволяет?
Нет( Ни функций первого порядка, ни чего-то похожего на указатели на функции, ни условных импортов, ни манки-патчинга. Вообще ничего... Я вот про это и говорю, что язык беден (внезапно, являясь достаточно приличным DSL для своих задач), ничего интересного в нем сделать не получится, тот же SOLID не описываем в терминах языка.
А в "мире разработки программного обеспечения" мы пока даже не можем однозначно сказать, к какой области деятельности этот самый "мир" относится - производство это, или искусство, и, быть может, как выше упоминали, вообще наука. Не... большинство, конечно, сильно надеется, что таки производство, но "единства в умах" тут пока нет. Так что, например, я не рискнул бы эти "практики", "принципы" и т.п. записывать в "хорошие".
Во-первых не столько много времени прошло, чтоб мы могли говорить о каких-либо объективных оценках. Прошедшего времени не хватило даже для того, что бы просто выработать хоть сколько-нибудь объемлющие критерии этой "хорошести". В том числе, и потому, что вопрос с "областью деятельности" до сих пор открыт.
А во-вторых, на данный момент, весь опыт развития "мира разработки программного обеспечения" показывает нам, что этот "мир" развивается (если таки развивается) путём - "теперь мы точно знаем, что так делать не надо". И для этого тоже есть множество причин (и даже объективных). Но суть-то в том, что уже даже этот опыт указывает нам, что определенная доля, скажем так, скептицизма "в подходах" - "это есть хорошо".
Это я всё к тому, что ваше "стандарт де-факто", "скорее выполняются, чем игнорируются" и прочие "не буква, то дух" - это, мягко говоря, не совсем так. Как раз наоборот. Мы знаем кучу полезного (и коммерчески успешного) программного обеспечения (в том числе, и enterprise уровня), которое создано в нарушение всех этих "хороших практик". По разным причинам "так получилось". Но, при этом оно вполне себе хорошо живёт и развивается (и продается) по сей день.
Т.е. в целом - всё, о чем сейчас можно говорить применительно к этим "хорошим практикам", что они (эти "практики") возможно применимы в каких-то весьма узких областях "мира разработки программного обеспечения".
Не в "мире разработки", а на хабре и в воспаленных умах отдельных фантазеров.
Только вот пока знание и бойкий речекряк SOLID сначала на интервью при найме, а потом во внутренних нытингах является обязательным умением - как-то такие "отдельные фантазёры", оказывается, воспроизводятся промышленным способом. Распиаренные книги и шумные конференции весьма способствуют этому.
А потом после того, как новички начинают размахивать принципами того же SOLID - ещё и неверно понятыми - критикуя существующие решения и выдвигая свои замки из слоновой кости - приходится чуть ли не матом приземлять их обратно, поясняя, что здесь им не тут ™ и что хороший код, который и прост и понятен и поддерживаем, может нарушать вдобленную им ерундень.
Если не буква, то дух их присутствует.
Только этот дух может радикально отличаться от дословного понимания пяти букв из мавзолея.
но если тыкнуть в какой-нибудь популярный продакшн-реди софт из мира энтерпрайза, то я на 99% уверен, что он более вероятно написан с учетом лучших практик, чем с их отрицанием.
Эээ видел я примеры. На 99% там как раз кошмар, а если принципы и применяются, то для того, чтобы перейти от 100-кратного торможения разработки по сравнению с идеалом - хотя бы к 20-кратному:) на большее не хватает, ибо надо фичи гнать.
но именно В ЦЕЛОМ состояние ближе к феншую.
Феншуй в exUSSR, как известно, это сочетание икебаны с гигиеной. Так вот - обычно нет ни одного из них.
Эээ видел я примеры
Слушайте, я же предложил простой эксперимент:
берем на гитхабе популярный продакшн-реди софт для энтерпрайза. Желательно написанный на ООП-языке, чтобы солид в полной мере можно было применять
оцениваем, там скорее соблюдаются солид, чистый код и прочие практики или отрицаются
про не-энетрпрайз я не знаю - я в этом не шарю и честно это признаю
Вместо этого мне рассказывают без пруфов, кто что видел, и цепляются к отдельным словам. Я так-то тоже много чего видел.
берем на гитхабе популярный продакшн-реди софт для энтерпрайза.
Эээ не так. 1) Было "из мира энтерпрайза", а сейчас у вас "для энтерпрайза". Вы так и ядро Linux можете записать как "для энтерпрайза":) 2) Почему вдруг на гитхабе? Основная масса софта "из мира энтерпрайза" туда никогда не поступит (по крайней мере в публичные репы).
оцениваем, там скорее соблюдаются солид, чистый код и прочие практики или отрицаются
Формулируем 5 принципов написания хорошей книги:
"Не" с глаголами пишется раздельно.
Актуализируй свои представления о предмете.
Хорошее здоровье - необходимый залог успеха.
Есть люди, которые ждут твою книгу, и ориентироваться надо на них.
Редактор - твой друг.
Всё, я сформулировал принцип "НАХЕР". Сложно найти книгу, которая была бы написана вопреки этим принципам. Я поехал по конференциям передавать свой опыт и загребать деньгу. И миллион поклонников скажет, что "оцениваем, там скорее соблюдается НАХЕР или отрицается".
И перешибить это инфоцыганство можно только изобретением какого-нибудь нового ПОФИГ, после которого скажут, что НАХЕР устарело и не соответствует новым веяниям.
Почему вдруг на гитхабе?
Ну ровно потому, что
Основная масса софта "из мира энтерпрайза" туда никогда не поступит
Было "из мира энтерпрайза", а сейчас у вас "для энтерпрайза". Вы так и ядро Linux можете записать как "для энтерпрайза":
У вас что-то кроме демагогии есть? Давайте как вам удобнее "из мира энтерпрайза". На гитхабе тонны либ и фреймворков например для JVM или .NET. Их трудно отнести к направлению "системное программирование". Вполне себе из мира энтерпрайза и нигде больше не используются. Я же нигде не писал про именно прикладные конечные решения.
Я же нигде не писал про именно прикладные конечные решения.
Я не вижу варианта понять ваш первый комментарий из этой подтветки иначе, как "прикладные конечные решения".
OK, сочтём это недопониманием и давайте попробуем переключиться на библиотеки. У меня всё тот же главный вопрос: да, оно формально может соответствовать большей части этих принципов. Иногда даже слишком:) вон как в соседнем примере - на FileReader навешиваем BufferedReader, чтобы производительность была адекватной.
Но: точное выполнение OCP вообще не предполагает возможности несовместимых изменений. Внутри творите что хотите, но снаружи сохраняйте API и контракты. Какая часть долгоживущих библиотек не имела таких изменений? По-моему, каждая раз в 5-10 лет такое проходит.
Похожая проблема с DIP. У какой части библиотек наружу выставлены только интерфейсы? Я такого не помню ни в одном популярном фреймворке.
Вот два принципа уже с ходу нарушаются, я уверен, у большинства.
Вы можете взять что-то крупное и долгоживущее и показать, что в нём хотя бы эти 5 принципов соблюдены?
А потом ещё надо будет поискать, какие принципы реально соблюдались с точки зрения самих разработчиков. Что-то я уверен, что 5 буквами из SOLID там не ограничится.
Это прекрасно! Уже перевел (авторски) и отправил коллегам.
Sonnet has a strict rhyme scheme of three quatrains, and then one rhyming stanza of two lines, known as a couplet
No preposition should be used to end a sentence with
An analogy does not prove a statement
Four and more syllables make a word sound sophisticatedly
Ultimately, the written text must fit a page
→ SNAFU
Слушайте, я же предложил простой эксперимент:
берем на гитхабе популярный продакшн-реди софт для энтерпрайза. Желательно написанный на ООП-языке, чтобы солид в полной мере можно было применять
оцениваем, там скорее соблюдаются солид, чистый код и прочие практики или отрицаются
Оценить проект целиком на предмет соблюдения/несоблюдения солид это просто? Я сильно сомневаюсь что вы сами проводили такую оценку. А если проводили, будет интересно ознакомиться с результатами.
Ради смеха вот примерчик (проект целиком не оценивал) нарушаются правила "No class should derive from a concrete class", " No method should override an implemented method of any of its base classes "
public class CommandLine extends Option {
...
class Option {
public class WikiRunner extends FileRunner {
...
@Override
public void process() {
В целом поиск по репозиторию нашел 398 java файлов со словом "extends" и 496 java файлов со словом "@Override". Там еще встречается "Override" в реализации интерфейсов, так что видимо не каждый из 496 файлов нарушает принципы, но все равно внушительное число. Не один-два косяка, а прям много.
Оценить проект целиком на предмет соблюдения/несоблюдения солид это просто?
Нет, не просто. Кто такое сказал?
Я сильно сомневаюсь что вы сами проводили такую оценку.
Сомневайтесь сколько угодно. Я много лазил по кишкам Spring Framework, случалось лазить по SLF4J, Kafka-клиенту для JVM, библиотекам Atlassian, Netflix Hystrix. Ну и сам JDK, естественно. Вопросики есть только к Atlassian, а у остальных и солид, и чистый код и паттерны - в общем все как вы любите.
Ради смеха вот примерчик
Ок, вот это деловой разговор. Согласен, у этих классов дизайн так себе. Их немного оправдывает, что они написаны 21 год назад.
Их немного оправдывает, что они написаны 21 год назад.
Однако это проект самого Мартина, так что выглядит это сильно так себе. Да и разработка не прекратилась 21 год назад.
Сомневайтесь сколько угодно. Я много лазил по кишкам Spring Framework, случалось лазить по SLF4J, Kafka-клиенту для JVM, библиотекам Atlassian, Netflix Hystrix.
Про Spring я и не сомневаюсь (но утверждать не берусь), но дело в другом. Огромный пласт энтерпрайза это закрытый код, который никто не видел кроме сотрудников. По своему опыту могу сказать что есть много коммерчески успешного (и не очень) софта в котором не то что паттерны, элементарный здравый смысл рядом не стоял. Я не говорю что это хорошо и правильно, но судить обо всей индустрии только по опенсорсу нельзя.
Радостно видеть, что люди наконец начинают говорить это вслух. В отрыве от конкретного языка/фреймворка/системы/компании ни один из этих принципов смысла не имеет. Может быть, за косноязычными формулировками Мартина и стояли важные идеи, но не справился с задачей выразить их словами.
DIP часто превращается в догму "всегда используйте интерфейсы"
Вот это абсолютно точно, встречал несколько раз на самых терминальных стадиях, без каких либо реальных или хотя бы уважительных причин.
При этом, к примеру, декомпозиция отвратительная.
То есть у нас есть ненужный интерфейс, в нем - ненужные методы (потому что плохо спроектирован объект), но типа архитектура.
Да интерфейсы страшная вещь и не нужная)) Мой вопрос не касается DIP, а как вы пишете модульные тесты? Либо вы их не пишите, либо они у вас не модульные. И то и другое ок, просто интересно
В основном не пишем (
В геймдеве такое если и принято, то я не видел (
На самом деле по разному, для серверов и логических библиотек пишем обычные, модели обычно можно тестировать целиком. В смысле можно целиком инициализировать кусок иерархии и тестировать сами классы. А для всего что дальше хорошо если есть обычные интеграционные.
По поводу нужности, ну вот тесты - хорошая причина для наличия интерфейса.
Архитектурные швы и вынос в модули - хорошая причина.
Изоляция чужого кода.
Ну и в конце концов полиморфизм для объектов, которым это требуется.
Но все вышеперечисленные штуки все еще не требуют писать интерфейсы к каждому файлу.
Согласен, что интерфейсы сами по себе не особо полезны. Но какая связь DIP и интерфейсов? Может вы, как и автор статьи, спутали с Dependency Injection? В последнем действительно рекомендуется (но не обязательно) использовать интерфейсы для внедрения зависимостей.
Я понимаю, легко ошибиться так как акронимы совпадают. Dependency Injection это одна из реализации идеи IoC.
DIP про уровни абстракции. Скажем, есть 2 дивизии. Ими управляют 2 генерала. Если генералу дивизии 1 что то нужно от дивизии 2 он не пойдёт напрямую к солдату из дивизии 2, он пойдёт к генералу дивизии 2 (или к маршалу, который управляет дивизиями), так как это его уровень. Здесь про уровни подчинения, но с уровнем абстракции похоже. Да часто абстракции определены интерфейсами или другими абстрактными типами или же DTO или же просто другими типами.
DIP про связи между модулями, а модули это не только типы, но и проекты, сборки, пакеты и т.п. Это советы, а не требования.
Не, вы не поняли.
Я в оригинале не говорил, что DIP - это интерфейсы.
Я говорил, что "люди понимают DIP как 'всегда используйте интерфейсы'" и под воздействием этого понимания херячат интерфейсы где надо и где не надо.
DI это разумеется полезный инструмент в том числе и для DIP, но смысл не в этом.
По поводу второго, это был ответ на ваш вопрос про тесты.
Интерфейсы - это инструмент, который нужно использовать не просто чтобы было, а по какой то причине. Тесты - возможная причина. DI - тоже. Но все равно речь не о том, чтобы по умолчанию делать всему интерфейсы.
Интерфейсы - это инструмент, который нужно использовать не просто чтобы было, а по какой то причине. Тесты - возможная причина. DI - тоже. Но все равно речь не о том, чтобы по умолчанию делать всему интерфейсы.
Но погодите, весь критичный код в идеале должен быть покрыт тестами. Что приводит нас к забавному выводу, что, раз "тесты = интерфейсы" (не настолько грубо и в лоб, но, тем не менее, связь есть), то весь критичный код должен принимать интерфейсы...
Многие тесты могут тестировать напрямую инстансы. Соответственно для этого не требуются моки и интерфейсы.
Например модели обычно делают без внешних зависимостей, моки в таких случаях нужны только для DI внешних сервисов, в остальном можно создавать и тестировать напрямую объект или дерево объектов.
Но да, если активно используете тесты - нужно много интерфейсов.
Например модели обычно делают без внешних зависимостей, моки в таких случаях нужны только для DI внешних сервисов
Ну ведь это именно то, о чем говорит D из SOLID. Осуждаемая тут максима "все-все-все закрывать интерфейсами и абстрактными реализациями" - она же не из SOLID'а, а из головы осуждающих. SOLID буквально рекомендует использовать DI для инъекции внешних зависимостей.
А непосредственно модели данных никто в здравом уме и не предлагал абстрагировать. Нету про это в SOLID ничего.
Да, и именно про это был мой изначальный комментарий)
Некоторые люди в обязательном порядке делают интерфейсы для всего
Это не имеет причиной ни SOLID/DIP, ни DI, ничего. Просто чтобы было.
Культ карго, растущий из неправильного понимания зачем это делается.
SOLID буквально рекомендует использовать DI для инъекции внешних зависимостей.
Где это сказано? Мартин буквально пишет что надо использовать фабрики.
Don’t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, whether statically or dynamically typed. It also puts severe constraints on the creation of objects and generally enforces the use of Abstract Factories.
In most object-oriented languages, such as Java, we would use an Abstract Factory to manage this undesirable dependency.
Безусловно не обязательно использовать фабрики, но у Мартина про DI не говорится ни слова (в описании DIP). Некритически настроенные люди могут начать делать абстрактные фабрики потому что так написал сам Мартин и хрен что ты им докажешь.
Осуждаемая тут максима "все-все-все закрывать интерфейсами и абстрактными реализациями" - она же не из SOLID'а, а из головы осуждающих
Именно это пишет Мартин.
Don’t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, whether statically or dynamically typed.
Never mention the name of anything concrete and volatile. This is really just a restatement of the principle itself.
Don’t refer to volatile concrete classes. Refer to abstract interfaces instead.
Но он же говорит "не ссылайтесь на хрупкие конкретные реализации классов".
to manage this undesirable dependency
И про "нежелательные зависимости".
Вот у вас, например, есть класс, считающий отчетность по договорам за период. Очевидно, что для того, чтобы оную отчетность посчитать, договора надо сначала откуда-то получить. Поэтому у класса есть зависимость - источник данных.
Можно напрямую отдать внутрь обертку над БД, откуда и брать договора тупым SQL-запросом. Ну вот Мартин говорит, что это не самая умная стратегия.
Закройте базу каким-нибудь простеньким репозиторием, и его интерфейс отдайте в ваш класс отчетности. Как минимум, это позволит (уже прямо здесь и сейчас) покрыть логику класса юнит-тестами.
При этом Мартин оперирует термином юнита (да, в классическом ООП оно почти 1-в-1 ложится на границы класса), т.е. акцентирует внимание на публичном интерфейсе. Вот эти правила - они прежде всего о публичной части класса, во внутренней реализации любую дичь творить можно.
DIP - это про то, чтобы нужный экземпляр зависимости не инстанцировался внутри класса, а передавался уже готовый в него снаружи. Ведь, в самом деле, для того, чтобы какой-то класс, работающий с БД, смог установить соединение с оной, нужно, чтобы этот класс знал, что это БД, оперировал такими понятиями как параметры подключения и т.д. и т.п. DIP - про то, чтобы класс бизнес-логики не знал ничего о том, как устроена зависимость, к которой он обращается.
and generally enforces the use of Abstract Factories.
А вот это - один из самых широкоиспользуемых приемов того, как добиться того, чтобы класс, зависящий от данных в БД, не имел никакого понятия о том, что там БД вообще какая-то есть внутри.
Закройте базу каким-нибудь простеньким репозиторием, и его интерфейс отдайте в ваш класс отчетности. Как минимум, это позволит (уже прямо здесь и сейчас) покрыть логику класса юнит-тестами.
Спасибо, Кэп.
Наличие зависимостей, "закрытие" зависимости интерфейсом и инъекция зависимостей вещи все-таки разные.
DIP - про то, чтобы класс бизнес-логики не знал ничего о том, как устроена зависимость, к которой он обращается.
Этого можно легко добиться без всяких интерфейсов и DIP, просто напросто скрываете все подробности реализации у класса и дело в шляпе. Что и должно делаться по умолчанию, подробности реализации скрыты, наружу "торчит" некий интерфейс aka публичный контракт. Использовать же интерфейс aka абстрактный класс нужно для подмены реализаций. Из банального для тестов.
Вот пример кода от Мартина, где используется фабрика. Заодно можете посмеяться надо самой фабрикой.
[Test]
public void TestCreateCircle()
{
Shape s = factory.Make("Circle");
Assert.IsTrue(s is Circle);
}
public interface ShapeFactory
{
Shape Make(string name);
}
public class ShapeFactoryImplementation : ShapeFactory
{
public Shape Make(string name)
{
if(name.Equals("Circle"))
return new Circle();
else if(name.Equals("Square"))
return new Square();
else
throw new Exception(
"ShapeFactory cannot create: {0}", name);
}
}
Роберт Мартин — отличный пропагандист и так себе программист. Одно то, что он называет себя "дядюшкой Бобом", уже прекрасный прием, чтобы втереться в доверие. Но какой он мне, блин, дядюшка?
Если внимательно почитать примеры кода из "Чистого кода" , можно охренеть от того, как этот "дядюшка" на ровном месте усложняет нормальное решение. Я бы такого "улучшенного" кода в своих рабочих сорсах видеть не хотел. А если код из книги его сложный для восприятия, то зачем следовать принципам, которые он в этой книге продвигает?
Кажется, в 2025 году уже точно стоит прекратить не только молиться на "Чистый код", но и перестать его советовать в принципе. А на собсесах начать проверять не знание принципов SOLID, а наоборот — спрашивать, почему эти принципы не стоит использовать.
Автор плохо понимает то о чем пишет(( Статья изобилует кучей выводов из воздуха, основанных на очем.
SOLID это советы, следовать которым нужно с головой. Появились они когда люди находились по граблями ООП. Когда ООП был дикий. По сути это best practics для ООП.
Основания их идея - сделать код простым за счёт уменьшение его связанности.
Давайте поконкретнее как-то
DIP часто превращается в догму "всегда используйте интерфейсы", что приводит к созданию ненужных абстракций. И вот уже весь код набит интерфейсами, которые имплементируются ровно один раз в одном классе и только затрудняют навигацию. "На всякий случай"
Какая догма? DIP это совет о том "что от чего должно зависеть" чтобы ваш код был максимально простым и его было максимально дёшево поддерживать. Абстракция там не обязательно про интерфейс. Там про уровень абстракции. Так почти везде у вас
Нет смысла объяснять что то опытном человеку)) Это сложно.
Абстракция там не обязательно про интерфейс.
But when the DIP is applied, we find that the clients tend to own the abstract interfaces and that their servers derive from them.
According to this heuristic
No variable should hold a pointer or reference to a concrete class.
Что это? Мысль, написанная на английском языке должна быть более убедительной? Типа как мысль, произнесенная во время протирания очков?))
Это цитаты из книги Мартина. Если вы не в курсе, он писал на английском. А если почитаете его первоначальную статью, там даже написано почему интерфейс должен быть абстрактным классом.
Я знаю, что он писал на английском, а еще его книгу переводили на другие языки. Вы просто взяли несколько фраз, без учета контекста.
A somewhat more naive, yet still very powerful, interpretation of the DIP is the simple heuristic: “Depend onabstractions.” Simply stated, this heuristic recommends that you should not depend on a concrete class—that allrelationships in a program should terminate on an abstract class or an interface.
According to this heuristic,
• No variable should hold a pointer or reference to a concrete class.
• No class should derive from a concrete class.
• No method should override an implemented method of any of its base classes.
А тут вам привели пример "наивной" интерпретации DIP, когда есть некий абстрактный слой, который чаще всего выглядит как набор интерфейсов - это базовый сценарий. Задача DIP, как и других принципов SOILD уменьшить связанность кода. Можно обойтись и без интерфейсов, что бы выделить некий API (более высокого уровня абстракции) и уменьшить зависимость между слоями. Просто интерфейсы используются чаще всего для этого в языках где они есть. Но с другой стороны, если нет понимания зачем это все вообще нужно, можно наделать много интерфейсов, которые будут раскрывать кучу деталей и ни как не будут абстракциями "более высокого уровня".
Пример "наивного" (в трактовке Мартина) разделения на слои это как раз отсутствие абстрактного интерфейса. А "правильное" (опять же в трактовке Мартина) разделение на слои это как раз с наличием абстрактного интерфейса.
Figure 11-2 shows a more appropriate model. Each of the upper-level layers declares an abstract interface for the services that it needs. The lower-level layers are then realized from these abstract interfaces. Each higher level class uses the next-lowest layer through the abstract interface. Thus, the upper layers do not depend on the lower layers. Instead, the lower layers depend on abstract service interfaces declared in the upper layers. Not only is the transitive dependency of PolicyLayer on UtilityLayer broken, but even the direct dependency of the PolicyLayer on MechanismLayer is broken.
Тут еще надо не запутаться между интерфейсом в смысле публичного контракта класса или модуля и интерфейсом в смысле абстрактного класса.
Я настоятельно рекомендую почитать оригинальную статью Мартина, там написано почему должен быть абстрактный интерфейс (абстрактный класс в плюсах), и зачем нужна инверсия владения интерфейсом. Зачем чисто плюсовые приколы в других языках вопрос риторический.
С чего вы взяли что я не читал? В книге вам приводят пример того что может быть абстрактным слоем на примере кода приложения. Чаще всего абстракции в языках программирования для модулей типов - это интерфейсы, там где они есть. Так всем понятней. Но совсем необязательно использовать именно интерфейсы. Основная мысль там "что и от чего должно зависеть, что бы зависеть поменьше". DIP не только про модули типов, а вообще про зависимости. Использование интерфейсов не дает ни какой гарантии что вы придерживаетесь DIP и наоборот. Например у вас есть 100500 nuget/maven пакетов - это тоже модули. Если грамотно разбить библиотеки по пакетам, то DIP позволит их использовать и поддерживать более эффективно. Тоже самое и про компоненты приложения, про плагины, про микро-сервисы и т.п.
Я рекомендую вам читая книгу, больше вникать в смысл прочитанного.
С чего вы взяли что я не читал?
Ну вот вы же в следующем предложении пишете про книгу, а я вам пишу про статью.
В книге вам приводят пример того что может быть абстрактным слоем на примере кода приложения.
В какой из книг и где именно? Формулировки у него менялись (в сторону размытия), но про абстрактные интерфейсы у него как минимум в "Agile.Principles.Patterns.and.Practices", "Agile.Principles.Patterns.and.Practices in C#" и в "Clean Architecture"
Если грамотно разбить библиотеки по пакетам, то DIP позволит их использовать и поддерживать более эффективно.
Типичное утверждение про solid - за все хорошее и против всего плохого. Но все же есть вопросы. Если разбить неграмотно, то DIP не позволит? А грамотно это как именно? Таким принципам грош цена.
Я думаю, что SOLID это набор принципов, необязательных к применению. Они могут сделать жизнь проще если их разумно использовать. Ни какой фанатизм не предполагается. Их можно и нужно менять со временем, что бы делать жизнь еще проще - адаптация наше все.
Но все же есть вопросы. Если разбить неграмотно, то DIP не позволит? А грамотно это как именно?
Сделает менее вероятным плохой результат
Давайте я вам чуть по-другому выделю:
the clients tend to own the abstract interfaces
Как видите, эта цитата вообще не противоречит тому, что
Абстракция там не обязательно про интерфейс.
А про что там абстракция, если там написано abstract interface? Абстрактный класс с частичной реализацией не катит - абстракции не "должны зависит от деталей".
Если совсем строго - там про API. Который так-то тоже "interface".
Абстрактный класс с частичной реализацией не катит - абстракции не "должны зависит от деталей".
Опять же, не должны - но могут. И не надо сильно привязываться к похоже звучащим ключевым словам из других языков. Этот принцип применяется не только в рамках одного файла, но и в рамках целых модулей.
Если совсем строго - там про API. Который так-то тоже "interface".
Это да, речь о публичном контракте модуля. Отдельный хороший вопрос что такое модуль в мире дотнета. Вот в турбопаскале были прямо модули которые так и назывались и у которых был интерфейс в смысле публичная часть.
Но, механизм у Мартина это абстрактный класс (ну или интерфейс, в разных языках по разному). О чем он и пишет
Figure 4 shows a more appropriate model. Each of the lower level layers are represented by an abstract class. The actual layers are then derived from these abstract classes. Each of the higher level classes uses the next lowest layer through the abstract interface.
Или более поздняя формулировка
In a statically typed language, like Java, this means that the use, import, and include statements should refer only to source modules containing interfaces, abstract classes, or some other kind of abstract declaration.
Тут еще есть какие-то "some other kind of abstract declaration", но что это совершенно непонятно.
А дальше вот такое правило
Don’t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, whether statically or dynamically typed.
Так что речь у него именно об абстрактных классах/интерфейсах.
Отлично. Давайте теперь то же самое про скрамы всякие)
Как можно всерьёз воспринимать статью про SOLID, в которой слово тест упоминается... ноль раз? Даже такой маленький штрих уже кричит, что может быть больше одной реализации IOrderCalculator. Более того, в TDD вы скорее всего начнёте с интерфейса, его моков и тестов ещё задолго до реализации. Аналогично ни слова по DI/IoC-контейнеры как мощные инструменты по организации слабосвязных сложных приложений.
Очевидно, что SOLID, как и подавляющее большинство существующих концепций, не идеален. Но если отбросить очевидные агрументы о том, что любой инструмент надо применять с умом, остаётся сплошная вкусовщина. Постоянная отсылка к какому-то мифическому "контексту", но ни одного примера, где следование принципам SOLID несёт очевидный вред. Фактически вся контраргументация сводится к небольшому росту количества кода. Но и это ничем не обосновано. Я могу с лёгкостью привести пример как, скажем, атомизация (SRP, ISP) классического паттерна репозиторий приводит напротив к росту повторного использования, обобщений и как итог - существенному сокращению объёма кода.
Как итог, на одну чашу весов кладётся копеечная экономия на определении интерфейсов, а с другой - качественная дизайнерская работа над определением этих самых интерфейсов, из которых потом растёт API. А в чём аргумент? Начать использовать интерфейсы, когда уже серьёзно припрёт? Где та тонкая грань, когда можно переходить на нормальный дизайн? Может в статье есть метрики, как сильно описание интерфейсов усложняет жизнь?
Начать использовать интерфейсы, когда уже серьёзно припрёт? Где та тонкая грань, когда можно переходить на нормальный дизайн?
У сожалению когда "вирмиешеподобный" код достигает определенной критической массы. Его уже невозможно или сложно поддерживать: исправлять там баги, добавлять функционал. Часто, рефакторинг займет больше времени чем просто выкинуть и переписать с нуля. Но не так все просто, так как вокруг еще сотня таких "классов", которые знают о всех остальных - по сути приложение из 1 большого класса распиханного по файлам с разными названиями. Классы 10K линий, пол сотни методов, пяток вложенных классов, и поток погоняет 10-ком других потоков с кучей локов - друзей дедлоков)).
Я вижу тут несколько проблем. 1. Часто в разработку идут люди без специального образования. 2. В ВУЗах РФ дают много математики, алгоритмов, и всего чего угодно, но не учат делать ПО.
Очень часто когда я вижу статьи что SOLID не подходит, почти всегда то рассматривается с точки зрения Объектно‑Ориентированного Программирования. Хотя книга, где впервые использовались эти понятия, даже называется как Principles Of OO Design.
Ребят! Пока вы на SOLID смотрите как на паттерны программирования, а не принципы проектирования, у вас будет взрыв головного мозга. Не делайте так! Это не про код, это про квадратики (модули).
В начале 00х ООП был на первом уровне проектирования, когда 90% программных продуктов в реальности заключались в 5–6 классах, небольшом конфиге(программе) над RAPID и десятке DTO. Сейчас это уровень микросервиса. А проектирование заключается в умении состыковать несколько микросервисов. Поэтому, если в 00х принципы SOLID надо было применять к ОО, то сейчас эти принципы надо применять к схеме диаграммы микросервисов. Время поменялось. У нас сейчас нет времени заниматься чистотой внутри микросервисов — надо заниматься чистотой архитектуры.
Читаю статью по диагонали наоборот )) В секции "Liskov Substitution Principle" вообще LOL. Если разговор про SOLID, так почему у вас "чтец" из файла занимается его расшифровкой, т.к.
могут быть разные алгоритмы расшифровки
разные источники данных: поток, строка, файл, тестовые данные
разные способы получения данных: синхронно, асинхронно, блоками и т.п.
разный формат файлов
хочется проверять каждый функционал в отдельности, иначе у вас тестов будет миллион
Лучше сразу подумать о всем SRP, ISP, DIP, LSP, OCP и отделить мухи от котлет:
Чтение данных
Расшифровка данных
Итог:
абстракция чтения данных
абстракция алгоритма расшифровки
абстракция самих данных
реализация чтения
реализация алгоритма расшифровки
тесты алгоритма расшифровки поверх абстракции данных
тесты чтения данных
Ваш итог:
В данном случае строгое следование LSP привело к:
Увеличению количества кода
Дублированию логики чтения файла
Усложнению структуры проекта
Мой итог:
простые и надежные реализации < 10 строк
без дублирования кода
дешево в поддержке (можно мозг вообще не включать)
минимум необходимости в последующих модификациях (OCP)
возможность использовать как кирпичики (ISP, LSP, DIP)
Покажите, пожалуйста, вашу реализацию кодом, очень хочется посмотреть. Меня тоже LSP в статье ввел в ступор.
Да конечно. По рукой только Rider, так что вот на C#:
new FileReaderTests().Test();
new DecryptorTests().Test();
var compositionRoot = new Consumer(new FileReader(), new Decryptor());
compositionRoot.Run();
interface IFileReader
{
Data Read(string filePath);
}
interface IDecryptor
{
Data Decrypt(Data data);
}
class Data
{
public Data(string data)
{
Val = data;
}
public string Val { get; }
}
class FileReader : IFileReader
{
public Data Read(string filePath)
{
return new Data("abc");
}
}
class Decryptor: IDecryptor
{
public Data Decrypt(Data data)
{
return new Data("decrypted " + data.Val);
}
}
class Consumer
{
private readonly IFileReader _fileReader;
private readonly IDecryptor _decryptor;
public Consumer(IFileReader fileReader, IDecryptor decryptor)
{
_fileReader = fileReader;
_decryptor = decryptor;
}
public void Run()
{
var encriptedData = _fileReader.Read("mydatafile.txt");
var decriptedData = _decryptor.Decrypt(encriptedData);
Console.WriteLine(decriptedData.Val);
}
}
class FileReaderTests
{
public void Test()
{
var fileReader = new FileReader();
var data = fileReader.Read("mydatafile.txt");
if (data.Val != "abc")
{
throw new Exception("test failed");
}
}
}
class DecryptorTests
{
public void Test()
{
var decryptor = new Decryptor();
var data = decryptor.Decrypt(new Data("xyz"));
if (data.Val != "decrypted " + "xyz")
{
throw new Exception("test failed");
}
}
}
Это хорошо выглядет. Если мы сразу знаем что у нас будут разные файлы, и разные алгоритмы шифрования. Тогда можно сразу так и писать.
Но в реальной жизни произойдет одно из двух. Или изначальная задача будет просто "читать файл" и тогда городить этот огород сразу нет смысла, а потом (когда мы узнаем через год теперь надо еще и расшифровать) нет возможности. Или для той же задачи "прочитать файл" юный адепт чистых архитектур нагородит на всякий случай миллион интерфейсов для расшифровки, зашифровки, валидации, конвертации и парсинга, а задача так и останется "просто прочитать файл".
Мне кажется все нормальные архитектуры рождаются только при рефакторинге, в результате обобщения уже имеющегося кода и послезнания того как именно надо было делать. Вот тут можно и вспомнить про солид, кисс и это все. Но не раньше
Я согласен с вами что изначальное решение можно и нужно делать проще и не учитывать требования, которые, возможно, будут в будущем, но их может и не быть. Нужно полагаться на текущие требования.
Но я бы изначально сделал именно так, например, потому что смогу писать модульные тесты на классы. Например в тестах на Decryptor
я не хочу проверять чтение реальных файлов, я хочу проверять лишь работу метода Decrypt
. Если объединить функционал классов Decryptor
и FileReader
, как предложил автор статьи, то у меня будет гораздо больше модульных тестов, они буду сложнее, мне нужно создавать реальные файлы, тесты будут более ломкие и их будет сложнее поддерживать и рефакторинг будет сложнее.
Ну и код в моем варианте не нужно менять часто, так как он выглядит "закончено" как кирпичики, которые можно сложить в любой нужной комбинации. Изменения нужны будут только если алгоритм изменится (но тогда скорее всего будет создан новый класс реализации) или будут найдены ошибки. А так "работает не трогай" - OCP рулит.
Вот репо с кодом примерно в таком стиле
Про "Interface Segregation Principle: Размер имеет значение"
interface Readable {
String read();
}
interface Writable {
void write(String data);
}
interface Closeable {
void close();
}
class FileHandler implements Readable, Writable, Closeable { ... }
ISP поможет уменьшить связанность и значительно упростит рефакторинг. А так же без следования ISP у вас не получится LSP. Например, из какого то источника можно только читать данные. Если не следовать ISP, то вы создадите реализацию для этого источника в котором, на запись будет брошено исключение, или ни чего не произойдет - это костыль, доп. логика обработки исключений или спец сценариев. Хорошо вы делали этот код и знаете куда можно писать а куда нет :), а если придет другой человек ...
ISP часто интерпретируется как "делайте интерфейсы маленькими", что приводит к взрыву количества микроинтерфейсов
альтернатива - говнокод (( вот пример этого :)
public interface Iterator<E>
{
boolean hasNext();
E next();
void remove();
}
remove() в итераторе и живите теперь с этим
remove() в итераторе и живите теперь с этим
Для всего есть, однако, причина. Весьма нередко такой интерфейс нужен. Потому что довольно часто итератор используется для поиска удаляемых элементов по какому-то критерию, и в, то же время, само по себе удаление может нарушить работу итератора. В таком случае полезно, чтобы можно было удалять через итератор - есть гарантия, что он от этого не сломается.
Другое дело, что такое поведение требуется не каждый раз. Не помню, можно ли в Java наследовать интерфейс от интерфейса (в C#, с которым я сейчас, в основном, работаю - можно). Если можно - проблем нет: метод remove уезжает в производный интерфейс, и используется тот интерфейс (базовый или производный), который нужен в этом месте. Но если нельзя, то это ограничение заставляет определять интерфейс итератора либо с, либо без метода remove, и ответ на вопрос, как сделать лучше, становится неочевиден.
Да я согласен с вами. Но по моему опыту это не частый сценарий. Чаще всего итератор использую для итерирования и то не напрямую. Ну а что добавили бы еще getCount()
удобно ведь, нет count
пусть бросит исключение. Это протечка абстракции, которая усложняет жизнь и поставщикам и потребителям. Поставщиков заставляет реализовывать функционал, который может и не нужен да и сложно бывает. Потребителям не понятно как это использовать, т.е. что ожидать: исключения или удаления. Еще и изменяемое состояние, его синхронизация между потоками и т.п. На мой взгляд не удачно получилось
ISP поможет уменьшить связанность и значительно упростит рефакторинг. А так же без следования ISP у вас не получится LSP. Например, из какого то источника можно только читать данные. Если не следовать ISP, то вы создадите реализацию для этого источника в котором, на запись будет брошено исключение, или ни чего не произойдет - это костыль, доп. логика обработки исключений или спец сценариев. Хорошо вы делали этот код и знаете куда можно писать а куда нет :), а если придет другой человек ...
Представил себе интерфейс ОС, где, чтобы и читать и писать файл, потребуется открыть его через два разных интерфейса. Хм, изначальный Паскаль именно так и требовал. И где он сейчас?
Представил себе интерфейс ОС, где, чтобы и читать и писать файл, потребуется открыть его через два разных интерфейса. Хм, изначальный Паскаль именно так и требовал. И где он сейчас?
Ну так причем тут это ОС? Можно получить файл, а уже для него получить и reader и writer. А с другой стороны множественная реализация интерфейсов почти везде есть, если идти более привычным путем.
Все принципы очень простые и жизненные. Например, ISP довольно просто объяснить так:
Интерфейс должен содержать достаточный набор методов для выполнения базового сценария. Т.е. если есть базовый сценарий где мы только читаем, то не нужно туда добавлять и методы для записи. Если есть сценарий где только пишем то там нужны методы для записи и все сопутствующие
Ну так причем тут это ОС? Можно получить файл, а уже для него получить и reader и writer.
Ну вот, у вас уже раздельно reader и writer. В принудительном порядке, а не в желательном для каких-то типовых применений.
Все принципы очень простые и жизненные. Например, ISP довольно просто объяснить так:
Интерфейс должен содержать достаточный набор методов для выполнения базового сценария.
Нет, ISP это про то, что интерфейс должен содержать необходимый набор методов для выполнения базового сценария. Разницу видите?
Но ISP это просто. А вот SRP или OCP это, мягко говоря, неоднозначно - особенно в заведомо проблемных формулировках Мартина. А ещё больше не "просто и жизненно" то, почему эти пять.
Нортон коммандер (дос навигатор, волков, фар, далее везде): F3 - View, F4 - Edit.
Первая и самая основная проблема SOLID как набора принципов - его эклектичность. Он был собран в таком виде только потому, что Michael Feathers однажды собрал такую аббревиатуру, а Robert Martinʼу понравилась такая находка. Больше ничего ценного именно в этом сочетании нет, это не 5 принципов, которые заведомо бы были полезнее массы остальных. Вообще критика именно с этой стороны была бы даже полезнее, чем то, что у автора статьи, который откритиковал каждый из принципов, но не то, почему именно эти.
Если уж брать какие-то принципы за базовые, то это набор GRASP. У него есть и более осмысленное обоснование, например, SRP.
Я не думаю, что стоит углубляться в отдельные пункты по тому же набору, потому что мы будем тогда играть на поле "дяди Боба" по его правилам, а это не то, чего требует разработка ПО. Но если уж идти этим путём, то совершенно вкратце:
SRP: Это минимизация взаимовлияния частей кода. В GRASP это и Information Expert, и Low Coupling - High Cohesion.
Open/Closed: Если устранить всё лишнее, то по сути это "выставили интерфейс - соблюдайте его по максимуму и постарайтесь не менять, иначе хоть где-то но вылезет боком".
LSP: Потомок должен не нарушать контракт, ожидаемый пользователем интерфейса (базового класса, как бы ни называлось), а если нарушается - то делать это с полным пониманием последствий. Что он ещё делает в рамках контракта - его дело и дело софтины в целом.
Interface segregation - "не экспортируй то, что не нужно" - low coupling в чистом виде.
Dependency Inversion - это приём, специфичный для ООП современного вида. Чисто ремесленный трюк, причём второстепенный. Для сравнения, тот же Information Expert, который говорит "собери данные поближе к месту, где их сопровождают и используют", более важен.
Зато мы тут простые "гребцы", а "дядя Боб", воспользовавшийся находкой своего коллеги - мегагуру.
Подобные заявления звучат в духе "Перестаньте молиться на физику и математику". А статьи пишутся ради статей. Люди же до нас были глупые. Взяли и напридумывал принципы всякие. Работаю 5 лет backend разработчиком. За это время имел дело с кодом, который был написан с использованием принципов и без. Ясен пень что работать с кодом, который пыл написан на базе каких-то принципов, приятнее. Возможно когда-нибудь мы начнем писать опираясь не на SOLID, а на другие принципы. И что то мне подсказывает, принципы те будут основаны на базе того же SOLID.
Математика основана на строжайших доказательствах. Гипотезы физики проверяемы. SOLID - это не очень внятно описанные идеи, которые не всегда применимы
Математика основана на строжайших доказательствах
SOLID - это не очень внятно описанные идеи, которые не всегда применимы
Да, любите вы демагогию развести. В инженерных дисциплинах в принципе крайне мало строгих доказательств чего-либо. А иначе б они были науками с полновесными теориями, а не инженерными дисциплинами. Зато есть эмпирические наблюдения, и SOLID одно из них. Если делать так-то, то будет норм. Но не всегда. А дальше уже выдумки ради хайпа, что кто-то на что-то молится.
Говнокод с сотнями интерфейсов и разделением на микроклассы с парой методов написаный по принципам солид ради следования принципам солид - обладает такой же ужасной читаемостью как и лапша из функций в 1000 строк. Просто потому что довольно трудно удержать такие контексты в голове во время чтения кода.
Проблема в том что на ревью автор первого говнокода достанет из рукава козыри в виде "лучшие практики", "чистый код", "солид" и т.п. и ты его ничем не прошибешь.
обладает такой же ужасной читаемостью как и лапша из функций в 1000 строк
Я бы с вами не согласился совсем. Большие функции это прямой путь к деградации кода. Тестов на них не напишешь, так как ветвлений логики слишком много, а каждое ветвление это умножение кол-ва тестов на 2. Что с этим делать? Найти ошибку это потратить на отладку много времени. Но что бы отладить нужно настроить среду из 100500 различных условий.
Маленькие и простые интерфейсы вместе с небольшими и понятными их реализациями - это основа порядка и простоты. Когда код простой на него есть модульные тесты, то работать с таким кодом одно удовольствие. Модульный тест это по сути пример как работает данный функционал класса. Иногда даже можно писать тест на абстракцию и использовать его для тестирования множества реализаций. Модульными тестами легко воспроизводить самые нереальные условия, которые на проде могу встречаться ооочень не часто, но ломают все напрочь.
Я не понимаю ваших аргументов. Возможно, вам не приходилось работать с хорошо написанным кодом ((
Тестов на них не напишешь, так как ветвлений логики слишком много, а каждое ветвление это умножение кол-ва тестов на 2.
А вы считаете качество тестов в покрытии всех веток? Местами это, конечно, имеет смысл, но делать из этого икону - поспешно.
Возможно, вам не приходилось работать с хорошо написанным кодом ((
Мне приходилось. И далеко не всегда, мягко говоря, его тестировали в режиме 100% покрытия веток (я уж не вспоминаю, что само по себе 100% покрытие недостаточно, проблема может быть в конкретной комбинации условий).
Я не говорю про покрытие, я согласен что эта метрика говорит мало о качестве тестирования. Покрытие кода хорошо продавать менеджерам, поэтому его часто мерят в интеграционных тестах, которые покрывают 50% сразу, но проверяют 1 сценарий, хоть и базовый :)
Я не говорю про покрытие
А как ещё назвать проверку всех веток тестами? Это покрытие и есть.
Можно запустить 1 большой интеграционный тест который пройдет по 50% путей логики. Но при этом проверить, например, только то что в результате появился какой-нибудь файл в директории. Но то что все отработало как нужно не проверить. То-же самое и с "модульными" тестами. Можно выполнить модульный тест но не проверить то что нужно.
Метрика покрытия кода работает только для идеальных тестов. И да, если проверит все ветки правильно, а это возможно, то покрытия кода будет отражать объем проверенного кода.
Когда люди пишут тесты им они часто тестирую только базовые сценарии. Но на проде чаще падают НЕ базовые сценарии. Поэтому лучше тестировать все ветки, если это возможно.
Да все так и есть. Это скорее рекомендации. Сегодня компьютеры очень мощные и можно городить миллионы классов. Нужно держаться каких-то разумных рамок.
Недавно мы установили Wordpress и какую-то несложную тему, пару плагинов и создали пару страниц. Тут тз немного меняют и нам уже нужно поэкспериментировать, я просто скопировал сайт на мой локальный компьютер. 250 МБ! Щито??!! Это просто CMS/CMF, пустой практически. КАК он может весить четверть гектара?!
Вордпресс с плагинами и темами может весить вполне больше. Особенно если каждый плагин имеет свою vendor папку, а фронт лежит с не удалёнными node_modules (вес которых может достигать до гига спокойно).
Сейчас такое время, когда плагин для вордпреса может весить больше, чем сам вордпрес. Про другие CMS или фреймворки вообще молчу.
Всё потому что для решения рутинных задач разработчики, используют репозитории, имеющие в зависимостях десяток других репозиториев. На выходе мы имеем, порой десятки тысяч файлов из которых реально используется максимум сотня другая, а рукописных и того меньше.
Проблема оптимизации частично решается тем, что всякие фронтенд фреймворки создают билды в которые подключают только используемый код, но на практике мусора в билдах обычно много. Однако, хоть исходники можно из релиза удалить, оставив минифрцированный билд. В php используют composer autoload, который по идее декларирует только используемые классы, но на практике так же периодически декларирует много "мусора". При этом папку vendor удалить нельзя по понятным причинам.
Причём разработчиков в этом винить тоже нельзя. Придумывать велосипеды каждый раз с нуля дело долгое и неблагодарное, а использование готовых решений сокращает время разработки. Здесь скорее проблема куда глобальнее и виновных в ней нет. Думаю в будущем комьюнити придёт к способам решения этих проблем.
Тоже можно сказать и про паттерны, которыми обожают "обмазываться эксперты", применяя их по поводу и без, зачастую с ужасной реализацией. Причём сами по себе паттерны не хорошие и не плохие. Просто рекомендательный свод шаблонных решений, а не свод законов. Неоднократно замечал, как на паттерны бездумно чуть-ли не молятся, вместо того чтобы разобраться, где и как их лучше применять на практике.
То же можно сказать, например про clean code, автора которого в прошлом году "пожурил" один инженер, что вызвало огромное количество холиваров. Хотя посыл был простым: не нужно бездумно следовать всему, что говорят.
Мне нравится программирование тем, что в нём всё имеет своё предназначение и логику. Даже самые плохие решения всегда можно разобрать и понять почему специалисты сделали так, а не иначе, будь то специфика технологий, skill issue или что-то другое.
Солид, так же имеют своё назначение и не следует их возводить в "священные писания". Это можно сказать про любые постулаты, архитектурные решения, паттерны, методологии и т.д.
Если всё свести до уровня пабликов пацанских цитат: опытный специалист, может не просто рассказать теорию, но и пояснить, как она применяется на практике. Ауф.
А давайте во всех аббревиатурах (SRP, OCP, LSP, ISP, DIP) заменим последнюю букву на R (Rule). А то Принцип загоняет людей в Догму, а Правило иногда имеет Исключение )
// Хорошо - следует OCP
interface OrderCalculator {
calculateTotal(order: Order): number;
}
class RetailCalculator implements OrderCalculator {
calculateTotal(order: Order): number { ... }
}
class WholesaleCalculator implements OrderCalculator {
calculateTotal(order: Order): number { ... }
}
// Новые типы калькуляторов можно добавлять без изменения существующего кода
Этот конкретный код не поменяется, да. Но... поменяется код бизнес-логики, где эти сущности используются.
Т.е. в "плохом" примере у нас был класс OrderCalculator и любые объекты этого типа сами знали, как себя вести, т.е. инкапсулировали в себе свое поведение в зависимости от типа заказа. При добавлении нового типа код бизнес-логики не менялся.
В "хорошем" примере при добавлении нового типа нам придется лезть в код бизнес-логики и там смотреть, какой конкретный класс заказа в каких местах нам надо использовать в зависимости от типа заказа. Т. е. вместо концентрации логики обработки заказа (в зависимости от типа) в одном месте, мы эту логику размазываем по всему коду более высокого уровня. Вероятность допустить ошибки при таком перелопачивании кода весьма высокая, имхо выше, чем при концентрации изменений в одном месте. Чтобы избежать этого, можно добавить еще один промежуточный уровень. Но стоит ли оно того в несложных проектах?
В дополнении, в двух примерах вроде и нарушения принципа нет. В первом примере мы изменяем метод, что определяет какой калькулятор, где использовать, но при добавлении нового калькулятора, мы никак не меняем другие калькуляторы. Во втором варианте, мы типизируем принцип работы с калькуляторами, указывая в интерфейсе обязательные методы и входные, выходные данные. Однако, сам подход не меняется.
Имхо, но неплохой пример для этого принципа, можно показать при помощи шаблонного метода. Самый банальный пример, создаём абстрактный класс с базовым функционалом для ряда объектов, скажем GameObject от которого наследуются все игровые объекты и мы не можем в GameObject, например изменить логику метода getPosition(), потому что от него наследуется сотня других сущностей (player, tree, enemy, bullet e.t.c) и изменение логики может привести к ошибкам в наследуемых классах. Если нам надо изменить метод мы добавляем новую его версию скажем getPosition3D(). Понятно, что пример такой себе мягко говоря, но отражает этот принцип вполне неплохо.
Поддерживаю. Это как в случае с микросервисами, которые не упрощают, а просто выносят сложность на уровень выше. Мелкие классы "делающие что то одно" - те же микросервисы, главная боль которых - их количество
Я не понял одного, зачем автор предложил создавать разные калькуляторы. Проще создать интерфейс Order с методом calculate, например. И на каждой новой тип Order создавать релевантный объект. Тем более, что код автора проверяет тип order, который закодирован в строке! Скорее всего автор плохо понял OCP.
SOLID задумывались именно как набор эвристик для решения конкретных проблем, а не как незыблемые правила. Роберт Мартин предложил их как способы решения определённых проблем, с которыми он сталкивался в конкретных проектах.
SOLID (Мартин сам об этом пишет) решает ровно одну конкретную проблему: как писать поддерживаемый код. Вся задача SOLID'а - сделать так, чтобы при последующей разработке не было мучительно больно от попыток добавить в проект новую фичу, не раз***в к хренам старые.
Если это простой продуктовый код (а не универсальная библиотека), то зачастую гораздо правильнее абстракции выделять по мере возникновения реальной потребности. Добавился новый тип чего-нибудь (или точно будет в будущем) — увидели что с этим много возни стало — добавили абстракцию.
Для того, чтобы "потом" добавить абстракцию, нужно переписать старый код. Т.е. например, при добавлении нового типа заказа вы буквально зачем-то переписываете старый. SOLID - про то, как "подстелить соломку".
Конечно, вы сейчас скажете, что "а зачем покрывать абстракцией один тип заказа?". Ну, собственно, один покрывать SOLID и не требует. Вопрос в том, когда "пора". Второй появился - уже пора, или еще нет, или еще хватит if'ов? А третий? А на каком этапе "пора" наступает? А точно не будет поздно?
Ведь для того, чтобы распилить логику на несколько реализаций и закрыть абстрактным типом, нужно будет еще и всех потребителей этой логики переписать! И чем сложнее логика внутри, тем сложнее будет рефачить.
Т.е. вся "философия" сводится к элементарному "не откладывай на завтра" + конкретные инструкции с указанием того, что не откладывать. Абстракция сейчас - дешевле, чем рефакторинг потом!
При этом изначальная версия с наследованием, хоть и нарушает LSP, более практична и понятна. В реальных проектах такое решение может быть предпочтительнее, особенно если мы уверены, что EncryptedFileReader не будет использоваться в контексте, где ожидается поведение базового FileReader. Т.е. принцип подстановки Лисков для начала подразумевает, что эта подстановка есть, ну или скорее всего точно будет.
"Вот за это вас и не любят". А вот это "если мы уверены, что EncryptedFileReader не будет использоваться в контексте, где ожидается поведение базового FileReader" - это у нас как-то контроллируется? Оно же не позволит его использовать при попытке сделать плохо?
А, позволит, но мы помним, что так делать нельзя? А через год мы вспомним? А через два мы вообще тут будем? А те, кто придет после нас - в курсе? Мы им не забыли сборник "Принципы разработки нашего проекта: 1001 и 1 нельзя" передать, в котором есть запись "п.467: никогда не используйте EncryptedFileReader в контексте, где ожидается поведение его базового класса"?
Мы общеизвестный сборник ограничений SOLID заменяем на внутренний документ "не используем наследника там, где ожидается поведение базового класса" - мы точно все правильно делаем?
Абстракция сейчас - дешевле, чем рефакторинг потом!
И в подавляющем большинстве случаев, по моей практике, оказывается, что направление абстрагирования было выбрано неверно.
Зато где действуют по вашему принципу - проект переполнен архитектурной астронавтикой.
общеизвестный сборник ограничений SOLID
Из его общеизвестности не следует его приоритетность или даже банальная полезность.
Вся задача SOLID'а - сделать так, чтобы при последующей разработке не было мучительно больно от попыток добавить в проект новую фичу, не раз***в к хренам старые.
Только вот конкретный сборник из 5 правил к этой задаче имеет весьма отдалённое отношение.
Только вот конкретный сборник из 5 правил к этой задаче имеет весьма отдалённое отношение.
Погодите, ну вот же оно:
если мы уверены, что EncryptedFileReader не будет использоваться в контексте, где ожидается поведение базового FileReader
Буковка L напрямую запрещает делать такое.
Вот это "если мы уверены" - это прямой путь к "раз****ь проект на ровном месте".
Если мы уверены, что EncryptedFileReader не будет использоваться в контексте, где ожидается поведение базового FileReader - нефиг от этого FileReader наследоваться. Просто до тех пор, пока будут возникать идеи закопать вот такое в коде (а оно только что возникло), приходится прописные истины оформлять в виде мнемоник типа SOLID.
Я вначале написал ответ подробнее, но сейчас сократил. С точки зрения локального примера автора статьи с LSP я поддерживаю, что таки интерфейс и раздельные реализации полезнее в дальнем прицеле. Но моё замечание относилось к другому: в первую очередь, повторюсь, SOLID это только один из сборников правил, выбранный за красивую аббревиатуру и ни за что больше. Можно делать код, отличный по SOLID и ужасный по всем остальным параметрам. Можно применять SOLID до полной непригодности кода к любой поддержке.
И вот тут ваше утверждение:
Абстракция сейчас - дешевле, чем рефакторинг потом!
оказывается, по моей практике, диверсионно-архипреступным. Почему - уже сказал: "И в подавляющем большинстве случаев, по моей практике, оказывается, что направление абстрагирования было выбрано неверно."
Моё замечание относилось к этому аспекту, что, кажется, достаточно ясно. А вы переключились обратно на конкретный пример автора статьи, пропустив главное.
Понимаете, SOLID - это такая мнемоника, сокращенная запись и напоминалка того, о чем в книжке "Чистая Архитектура" написано. В целом, без предварительного прочтения книжки и разбора примеров/ситуаций, вероятно, она и бесполезна.
Описанные в книжке принципы написания поддерживаемого кода разумны и действительно полезны. Чтобы не следовать этим разумным советам, нужна веская причина. Например, "хак для оптимизации производительности" или что-то вроде. Ну, как минимум, сформулировать причину, "почему SOLID тут не нужен" - стоит.
в подавляющем большинстве случаев, по моей практике, оказывается, что направление абстрагирования было выбрано неверно
Вот тут вы не совсем честно сформулировали эту самую причину. То, что вы описали - это "мы не собираемся поддерживать этот код". На самом деле - тоже вполне себе разумная причина не использовать правила написания поддерживаемого кода. Если вы решили, что в случае чего проще будет выкинуть и переписать - почему бы и нет, тут вам SOLID и не нужен, в целом.
Понимаете, SOLID - это такая мнемоника, сокращенная запись и напоминалка того, о чем в книжке "Чистая Архитектура" написано. В целом, без предварительного прочтения книжки и разбора примеров/ситуаций, вероятно, она и бесполезна.
Я "Чистую архитектуру" не читал, не довелось. Не хочу впадать в режим обсуждения Пастернака, но тот бред, который в "Чистом коде", отвращает от идеи намеренно гоняться за этой книгой с целью прочитать. Если случайно попадётся, посмотрю. А так - нет. Но я готов поверить, что SOLID как набор принципов ужат до того, что в этой книге. Есть где почитать? (не бумажно)
Описанные в книжке принципы написания поддерживаемого кода разумны и действительно полезны. Чтобы не следовать этим разумным советам, нужна веская причина.
Я так и понял, что 99% оппонентов не хочет понимать, что я пишу. Можно я ещё раз открытым текстом повторю? Я не считаю именно этот набор, именно в таком виде: S-O-L-I-D - чем-то особенно ценным по сравнению со многими другими. Я согласен с относительно положительной ценностью этого набора. Но я не считаю его чем-то выделяющимся по сравнению с другими правилами и комплектами. Это раз. А два - то, что даже в пределах этого комплекта принципы сформулированы слишком абстрактно и их проецирование на реальность проектов ещё требует разбирательства, как же их осознать - в отличие от более конкретизированных методик.
А то, что написание "поддерживаемого" (выделение ваше) кода приведёт в большинстве сложных случаев к достаточно полному соблюдению SOLID - это следствие других правильных методик, а не SOLID.
Вот тут вы не совсем честно сформулировали эту самую причину. То, что вы описали - это "мы не собираемся поддерживать этот код".
НЕТ. Это неверное суждение и я не понимаю, почему бы вам его выдвигать, кроме чисто религиозного намерения защитить SOLID.
Один из проектов, который я развивал много лет и до сих пор частично участвую, начат в 2003 (а я пришёл в него в 2004), выросши из десятка мелких модулей до весьма приличного объёма, и сейчас применяется сотнями кастомеров для миллионов их клиентов. Именно на его опыте я в первую очередь и формулирую: угадать направление развития практически невозможно, рынок задаст то, что никто не смог предсказать. Попытка строить что-то "на вырост" не работает. На глаз, уход за год от предыдущей спецификации - процентов 5-10, за 10 лет - до неузнаваемости. Поэтому и обобщение заранее - чревато сложностью, которая не применится. А вот пригодность к рефакторингу с перестройкой туда, куда надо для нового развития - это, да, критически важно.
И вот именно поддерживаемость и является у этого проекта одним из главных критериев развития (после прямой функциональности, разумеется, и без гонки "на вчера"). При этом в промежуточных состояниях по сравнению с "магической пятёркой" нарушено, наверно, всё. Но когда нарушение какого-то из этих принципов начинает мешать - его, естественно, исправляют.
Так что свои домыслы насчёт "честности" и "выкидывания" лучше честно выкиньте и начните смотреть не через искажающие пятибуквенные очки.
Понимаете, SOLID - это такая мнемоника, сокращенная запись и напоминалка того, о чем в книжке "Чистая Архитектура" написано. В целом, без предварительного прочтения книжки и разбора примеров/ситуаций, вероятно, она и бесполезна.
Акроним SOLID появился в начале нулевых, сами принципы сформулированы в середине 90-х, книга "Чистая архитектура" написана в 2017 году. Получается SOLID 20 лет был бесполезен? В общем и целом я соглашусь)
Абстракция сейчас - дешевле, чем рефакторинг потом!
Ваш финансовый директор в этом месте с вами поспорит.
Ой вот только финансовых директоров не надо вспоминать. Они как раз за SOLID будут топить руками и ногами, даже если против своей воли.
Всюду, где я попадал в энтерпрайз-проекты, выделение ресурсов на очистку, рефакторинг и сокращение техдолга отличалось от нуля только под микроскопом. Это даже если доказываешь, что он критически необходим для новой фичи: сделайте нашлёпку, и всё тут. А то, что код уже больше чем наполовину просто гуано - такие верхи не интересует.
И это как раз может быть причиной введения этой "методологии": она позволяет, при более-менее правильно угаданном направлении развития - то есть, навскидку, в 20-% случаев - воспользоваться уже готовой наработкой и сократить те рефакторинги, которые иначе всё равно придётся подпольно протащить. А то, что при этом абстрактные фабрики адаптеров абстрактных фабрик замедляют работу в разы - не волнует, затраты на железо и облака это по другому ведомству.
Но мы-то рассматриваем тут с точки зрения программистов, а не финансистов...
Всюду, где я попадал в энтерпрайз-проекты, выделение ресурсов на очистку, рефакторинг и сокращение техдолга отличалось от нуля только под микроскопом.
Правильно. Так если б финансисты знали, что вы ещё как-то заранее пытаетесь решить будущие проблемы, они бы и на первом этапе вам бюджет сократили.
Но мы-то рассматриваем тут с точки зрения программистов, а не финансистов...
А что программисты? Их дело веслом махать.
Перестаньте молиться на принципы S.O.L.I.D