В мире разработки программного обеспечения существует множество "священных коров" — принципов и практик, которые принимаются как данность и редко подвергаются критическому анализу. Особенно показательна ситуация с принципами SOLID на русскоязычных ресурсах: достаточно открыть Хабр, чтобы найти 100500 статей о SOLID, и в каждой из них принципы интерпретируются по-разному.
Само существование такого количества "объяснительных" статей говорит о фундаментальной проблеме: если принципы требуют толкования, значит их названия не являются самодостаточными и интуитивно понятными. А если каждый разработчик понимает принципы по-своему, возникает вопрос — зачем вообще нужны принципы, которые не дают однозначного руководства к действию? Принципы SOLID, предложенные Робертом Мартином, давно стали одной из таких "священных коров". Однако пришло время честно признать: то, как мы используем SOLID сегодня, часто противоречит изначальным идеям и в целом иногда может приносить больше вреда, чем пользы. Зависит от контекста.
SRP не SRP
Самый яркий пример искажения первоначального замысла — это интерпретация принципа единственной ответственности (SRP).
Когда Роберт Мартин впервые сформулировал этот принцип, он говорил о том, что класс должен иметь только одну причину для изменения. Но что это значит на практике?
Изначальная идея была вообще про людей: суть в том, чтобы разные заказчики изменений могли влиять на систему независимо друг от друга, не создавая конфликтов. Бухгалтеры меняют формулу расчета бонуса, технари — какие-то технические подходы к базе, и они не мешают друг другу. То есть, если над одним классом работают разные бизнес-юниты, это признак нарушения SRP.
Ответственность в том смысле, что если ты испортил класс, и тебя за это может уволить и CTO, и CFO, то у класса две ответственности, и надо его разделять.
Однако сегодня этот принцип часто интерпретируется совершенно иначе — как требование "разбивать класс на части, если он делает слишком много разного" или даже "делать классы маленькими".
Иногда небольшой понятный класс разбивается на 5 частей ради "единственности ответственности" в вакууме, да еще и добавляются абстракции, чтобы удовлетворить DIP.
Является ли такое разделение действительно необходимым? Иногда да, но точно не всегда.
Принцип открытости/закрытости
Принцип открытости/закрытости (OCP) — более странного названия и придумать сложно, новички стабильно впадают в ступор. Объяснение "программные сущности должны быть открыты для расширения, но закрыты для модификации" этот ступор только удваивает. Опять же, фанатичное следование этому правилу может переусложнить ваш код. Например,
// Плохо - нарушает OCP
class OrderCalculator {
calculateTotal(order: Order) {
if (order.type === 'retail') {
return this.calculateRetailTotal(order);
} else if (order.type === 'wholesale') {
return this.calculateWholesaleTotal(order);
}
// При добавлении нового типа заказа придется менять этот код
}
}
// Хорошо - следует OCP
interface OrderCalculator {
calculateTotal(order: Order): number;
}
class RetailCalculator implements OrderCalculator {
calculateTotal(order: Order): number { ... }
}
class WholesaleCalculator implements OrderCalculator {
calculateTotal(order: Order): number { ... }
}
// Новые типы калькуляторов можно добавлять без изменения существующего кода
Тут всегда вопрос в том, а появится ли еще один тип заказа в будущем? Или мы просто так добавили 2 класса и интерфейс (+ еще где-то фабрика нужна, if просто туда переедет)? Может пока что обойдемся этим ифом, да и хрен с ним?
Кроме того, чтобы действительно сделать абстракцию хорошо заранее, надо быть предсказателем будущего. Мало ли как нам придется этот total вычислять для нового типа заказа, может одного только аргумента order будет недостаточно, надо будет добавить еще что-то, и всё равно придется переделать всё, что мы напридумывали.
Если это простой продуктовый код (а не универсальная библиотека), то зачастую гораздо правильнее абстракции выделять по мере возникновения реальной потребности. Добавился новый тип чего-нибудь (или точно будет в будущем) — увидели что с этим много возни стало — добавили абстракцию.
Liskov Substitution Principle
Представим, что у нас есть базовый класс для чтения данных из файла:
class FileReader {
read(filePath: string): string {
// Читаем файл и возвращаем его содержимое
return fs.readFileSync(filePath, 'utf8');
}
}
И мы хотим создать специализированный класс для чтения зашифрованных файлов:
class EncryptedFileReader extends FileReader {
read(filePath: string): string {
// Здесь возникает проблема с LSP
if (!this.isFileEncrypted(filePath)) {
throw new Error("File is not encrypted!");
}
const content = super.read(filePath);
return this.decrypt(content);
}
private isFileEncrypted(filePath: string): boolean {
// Проверка, зашифрован ли файл
}
private decrypt(content: string): string {
// Расшифровка содержимого
}
}
Строго следуя LSP, этот код проблематичен, потому что:
EncryptedFileReader усиливает предусловия (требует зашифрованный файл)
Выбрасывает исключение в ситуации, когда базовый класс работал бы нормально
Чтобы соблюсти LSP, нам пришлось бы:
Либо заставить базовый класс проверять, зашифрован ли файл (что не имеет смысла для обычного чтения)
Либо создать общий интерфейс и два независимых класса вместо наследования
interface IFileReader {
read(filePath: string): string;
}
class PlainFileReader implements IFileReader {
read(filePath: string): string {
return fs.readFileSync(filePath, 'utf8');
}
}
class EncryptedFileReader implements IFileReader {
read(filePath: string): string {
if (!this.isFileEncrypted(filePath)) {
throw new Error("File is not encrypted!");
}
const content = fs.readFileSync(filePath, 'utf8');
return this.decrypt(content);
}
// ...
}
В данном случае строгое следование LSP привело к:
Увеличению количества кода
Дублированию логики чтения файла
Усложнению структуры проекта
При этом изначальная версия с наследованием, хоть и нарушает LSP, более практична и понятна. В реальных проектах такое решение может быть предпочтительнее, особенно если мы уверены, что EncryptedFileReader не будет использоваться в контексте, где ожидается поведение базового FileReader. Т.е. принцип подстановки Лисков для начала подразумевает, что эта подстановка есть, ну или скорее всего точно будет.
Interface Segregation Principle: Размер имеет значение
ISP часто интерпретируется как "делайте интерфейсы маленькими", что приводит к взрыву количества микроинтерфейсов:
interface Readable {
String read();
}
interface Writable {
void write(String data);
}
interface Closeable {
void close();
}
class FileHandler implements Readable, Writable, Closeable { ... }
Такое разделение может показаться элегантным, но иногда оно создает ненужную сложность и затрудняет понимание системы в целом. Зависит от контекста (банально, но правда).
Dependency Inversion Principle: Инверсия ради инверсии
DIP часто превращается в догму "всегда используйте интерфейсы", что приводит к созданию ненужных абстракций. И вот уже весь код набит интерфейсами, которые имплементируются ровно один раз в одном классе и только затрудняют навигацию. "На всякий случай"
Особенно непонятно зачем это делать для программы на Go, где зачастую интерфейс можно прикрутить в любой момент позже (там "классы" не требуют указания "implements").
Что делать?
-
Контекст важнее правил
Вместо слепого следования принципам SOLID, нужно всегда учитывать контекст конкретного проекта. Маленькому проекту не нужна сложная архитектура корпоративного приложения.
-
Простота — главное достоинство
Если код можно написать проще — его нужно писать проще. Не стоит создавать сложные абстракции только потому, что "так говорит SOLID".
-
Эволюционный подход
Иногда лучше начать с простого решения и усложнять его только при возникновении реальной необходимости, чем пытаться предусмотреть все возможные сценарии заранее.
Более глубокий взгляд на проблему
Важно понимать, что проблема не столько в самих принципах SOLID, сколько в том, как индустрия их использует. Мы превратили их в догмы и часто применяем механически, без понимания изначального контекста и целей. Показательный пример — повсеместное создание абстракций "про запас". На код ревью вас ожидает жесткая порка, если, не дай бог, класс делает сразу две вещи.
Возможно, нам стоит перестать использовать сам термин "принципы" применительно к SOLID. Это слово подразумевает некие универсальные истины, которым нужно следовать всегда и везде. Вместо этого имеет смысл говорить о "паттернах решения проблем" — это лучше отражает их истинную природу как инструментов, которые полезны в определённых ситуациях, но не являются универсальным рецептом.
SOLID задумывались именно как набор эвристик для решения конкретных проблем, а не как незыблемые правила. Роберт Мартин предложил их как способы решения определённых проблем, с которыми он сталкивался в конкретных проектах. Со временем индустрия превратила их в своего рода религию, потеряв первоначальный контекст и прагматичный подход.
Заключение
Это не призыв полностью отказаться от SOLID. Скорее, это призыв вернуться к более прагматичному подходу, где эти принципы воспринимаются как полезные инструменты в арсенале разработчика, а не как догмы. В конце концов, главная цель любого принципа проектирования — это создание понятного, поддерживаемого и эффективного кода. И если следование какому-то принципу противоречит этой цели, нужно иметь смелость отступить от него в пользу более простого и практичного решения.
Приглашаю вас подписаться на мой канал в telegram
Комментарии (284)
fo_otman
19.01.2025 15:58Я еще не очень понимаю тему с огромным количеством классов исключений на все случаи жизни, которые отличаются друг от друга ... ничем. На вопрос тимлиду, нафига это нам, он ответил что-то типа "ну вот если в будущем нам понадобится вот этот вот конь в вакууме, то...". Потом он свалил, команда перешла на другие проекты, на проекте остался только я. Спустя год конь в вакууме не понадобился. Исключения я все вырезал к чертям, и вместо 50+ классов осталось штук 5. Никакого дискомфорта не заметил.
aloginovpro
19.01.2025 15:58Типизация исключений нужна для реализации различных реакций. Если реакция везде одинаковая (catch Throwable e), то нет смысла и типизировать исключения. То же самое для http кодов ответа.
posledam
19.01.2025 15:58Вообще смысл есть, конечно не до фанатизма, но есть. Даже если исключения просто логируются и нет никакой специальной логики (сейчас), но в логах обычно мы выводим тип исключения в отдельном поле, и в моей практике это много раз позволяло правильно и максимально быстро провести оценку влияния и даже иногда замониторить конкретные типы. Поэтому да, всё вообще можно упростить: зачем нам это, зачем нам то, давайте всё уберём. А конь в вакууме может понадобится именно тогда, когда уже написано тонна кода и дорабатать за вменяемое время уже не получится. Принцип "лучше перебздеть, чем недобздеть" работает всегда, но и нужно соблюдать меру. Если обвес приходится обслуживать и тратить на это ресурсы, надо правильно оценить его необходимость. А если он есть не просит, в чём проблема?
sherbinko
19.01.2025 15:58Лучше недобздеть. Как показывает практика, переписать тонну кода не представляет большой трудности, если ясны цели и есть работающий код. Рефакторинг - это неотъемлемый процесс. Если код не меняется, то он умирает.
Кроме того, при работе с кодом, простота чтения гораздо важнее простоты написания. Переписываем мы, условно, 1 раз, а читаем месиво из интерфейсов и безумных конструкций сотни раз. Обвес именно что просит есть.cupraer
19.01.2025 15:58Как показывает практика, переписать тонну кода не представляет большой трудности, если ясны цели и есть работающий код.
А вот для переписи с нуля движка одного браузера изобрели целый новый язык (!) — а потом сами же авторы языка на нём запрограммировать так, чтобы работало — не смогли. Есть мнение, что просто язык оказался так себе, но оно непопулярное.
hardtop
19.01.2025 15:58Это Вы про rust? Разве Фаерфокс не переписан на расте?
Sanchous98
19.01.2025 15:58Не полностью. Переписали отдельные компоненты. Видимо те, которые были наиболее проблематичными
radtie
19.01.2025 15:58В разумных пределах это полезно, добавляет семантики, когда, чтобы точно понять, что случилось достаточно только типа исключения, и нет необходимости анализировать текст сообщения.
В некоторых языках, например, можно алиасы на простые типы данных создавать, тоже, казалось бы, избыточность, но если присмотреться:class Image { int id; int width; int height; string type; }
их использование делает код понятнее и устойчивее:
class Image { ImageId id; Dimension width; Dimension height; ImageType type; }
Sliptory
19.01.2025 15:58class Image { int imageId; int width; int height; string imageType; }
Все может быть намного проще, и не надо городить огород. Иногда возникает желание сделать что-то крутое и необычное, но это деструктивный как правило порыв.
Cels
19.01.2025 15:58если класс называется 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";
transcengopher
19.01.2025 15:58Дело не в классе и его полях, а в остальном коде. При вашем подходе у вас будет какой-то int, и какой-то string. А при определении подтипов - сразу везде становится понятно, что эта конкретная переменная не просто некое число, а целый идентификатор картинки - а значит, по нему, скорее всего, можно поискать саму картинку где-нибудь, и он, например, уникален среди всех остальных хранящихся
ImageID
, но не будет уникален среди каких-нибудьUserID
, а также что нет самостоятельного смысла в том, чтобы поImageID
искать пользователя.В чуть более продвинутых системах, переменная типа
Dimension
,скажем, не сможет иметь отрицательное значение, и это будет вам гарантировать компилятор вашего ЯП, а не "честное слово" предыдущего разработчика.Cels
19.01.2025 15:58Ну допустим сделал 20-30 подтипов на проект - как это упростит понимание? Например ImageType - это какой тип данных - string, int, enum или еще какой-то? т.е. мне нужно разобраться с произвольными типами данных и держать их всегда в своей голове?
В чуть более продвинутых системах, переменная типа
Dimension
,скажем, не сможет иметь отрицательное значениеОпять же есть int, uint.
Может это где-то и оправдано, но в большинстве проектов нет. Мы же здесь про упрощение кода говорим?
Был бы благодарен за реальный пример, где использование подтипов оправдано.
transcengopher
19.01.2025 15:58ImageType - это какой тип данных - string, int, enum или еще какой-то
ImageType - это ImageType, конечно же. Хотя это и не исключает наличия правил конвертирования его в int, string и обратно.
Мы же здесь про упрощение кода говорим?
Я не вижу здесь упрощения при любом направлении движения. Вам всё равно придётся как-то знать и помнить, что вот это конкретное значение кодирует идентификатор пользователя, а уж будет это имя переменной или тип это дело ваше. Просто хранение в виде именно типа, а не чего-то ещё добавит вашему коду невозможность использовать значение ImageType, например, для того, чтобы отправить его в поле с комментарием, без дополнительных движений кодом. Это, в своём роде, упрощает код за счёт ограничения возможного пространства применений для каждого значения.
Cels
19.01.2025 15:58ImageType - это ImageType, конечно же.
и что вы будете ему присваивать? т.е. это же все равно строка, число или еще что-то.
Я не вижу здесь упрощения при любом направлении движения. Вам всё равно придётся как-то знать и помнить, что вот это конкретное значение кодирует идентификатор пользователя
ну стандартные типы данных все и так знают и помнят, в отличии от подтипов и поле uint User.Id - вполне за себя говорит, что содержит идентификатор пользователя и запоминать его вовсе не нужно, а в коде подсветит, что тип данных uint, а не какой-нибудь UserIdentifier, который может быть чем угодно.
Это, в своём роде, упрощает код за счёт ограничения возможного пространства применений для каждого значения.
а зачем нужно еще ограничивать пространство применений переменной? - есть стандартные модификаторы доступа. Вот реально не придумать такую ситуацию, если только защита от не программистов - но это нонсенс.
Больше похоже на случаи из статьи, где интерфейсы и классы лепят кто во что горазд, т.к. реального преимущества и тем более упрощения так и не увидел.
transcengopher
19.01.2025 15:58это же все равно строка, число или еще что-то.
Да нет же. У него могут быть какая-то логика создания из строк или чисел, но сам по себе этот тип - не строка и не число. Если в ЯП, на котором оно пишется,
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
найти куда сложнее).Cels
19.01.2025 15:58Если в ЯП, на котором оно пишется,
enum
не относится к числам - тоImageType
в принципе можно отenum
унаследовать, но и это само по себе не обязательно.Зачем наследовать, если можно сразу определить enum?
Но не в методе
pow
, куда этотuint
можно передать, и не после операции"+ 2"
.не понял
Например, чтобы не было соблазна передать UserIdentifier в какой-нибудь Image? findImageById(uint id)
Так проще названия нормальные дать:
findImageById(uint Id) - поиск картинки по ID
findImageByUserId(uint Id) - поиск картинки по ID юзера.
ну а по поводу соблазна, как и сказал, это защита от совсем непрограммистов и если подходить с этой стороны, то весь код должен состоять из валидаций/проверок. И то, найдутся умельцы, что никакие подтипы и проверки не помогут.
Ну а про поиск использования любой переменной - так это есть во всех нормальных IDE (в PhpStorm & VS точно).
Ну не вижу я реального примера, чтобы прямо подтипы нужны были. И да, подтипы усложняют код и повышают безопасность (только от кого?) Неужели любой подтип нельзя заменить стандартными типами, классами, интерфейсами, структурами или перечислениями?
transcengopher
19.01.2025 15:58сразу определить 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, выбросить тесты, сложить весь код в один файл, и тому подобное. В программировании вообще много чего можно.
Cels
19.01.2025 15:58В данном случае "наследовать от 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, выбросить тесты, сложить весь код в один файл, и тому подобное. В программировании вообще много чего можно.
Так об этом и статья, что код переусложняют лишними классами, интерфейсами и прочими не нужными конструкциями. Вы же предлагает использовать подтипы для упрощения, а в чем упрощения не пояснили, да и в комментарии ниже сами написали, что для сравнения н-р ширины, нужно немного клея).. И сколько клея нужно будет всего добавить в проект и как это упростит код? Вы сами-то используете подтипы? Если да, то приведите, пожалуйста, кусок кода, где оно прям оправдано и делает код простым. А вы переусложняете код просто так - как в статье, не получая никаких явных преимуществ.
Я уверен, что подтипы сделали не просто так и где-то они нужны, но точно не для упрощения кода, а скорее для безопасности.
inkelyad
19.01.2025 15:58программирую очень давно и не разу не приходилось искать все переменные по типу - т.е. что дает знание всех переменных определенного типа?
Пример. Берем номер банковской карты, который как бы строка, но делаем типом. После этого анализировать "а куда мы номер карты передаем (и где он может убежать в какие-нибудь логи, в которые не положено)" - становится куда как легче.
Нужно такое, например, когда какое-нибудь новое регулирование приняли и нужно выяснять "а мы соответствуем, или надо переделывать?"
inkelyad
19.01.2025 15:58в чем упрощения не пояснили, да и в комментарии ниже сами написали, что для сравнения н-р ширины, нужно немного клея).. И сколько клея нужно будет всего добавить в проект и как это упростит код?
Вот еще пример(теоретический, правда, потому что языки не поддерживают/не поддерживали), когда могло бы помочь:
берем всякие ошибки (иногда сильно дорогостоящие) связанные с единицами измерения. Длину, переданную в функцию, выразили в дюймах вместо сантиметров или наоборот - и получили не то, что хотели.
Если бы компиляторы различали эти типы чисел и ругались - то оно бы ловились на этапе компиляции и написания кода.
Cels
19.01.2025 15:58Пример. Берем номер банковской карты, который как бы строка, но делаем типом. После этого анализировать "а куда мы номер карты передаем (и где он может убежать в какие-нибудь логи, в которые не положено)" - становится куда как легче.
а просто по использованию или имени переменной искать? Вы предлагаете делать подтипы в проекте, ради иллюзорных поисков? Вам это часто требуется?
берем всякие ошибки (иногда сильно дорогостоящие) связанные с единицами измерения. Длину, переданную в функцию, выразили в дюймах вместо сантиметров или наоборот - и получили не то, что хотели.
Так здесь подтип ничего не решает - это должно обрабатываться на этапе присвоения или в самой функции.
LengthInch length = 5; // здесь значение в дюймах или сантиметрах?
MonkAlex
19.01.2025 15:58Почему иллюзорных то?
Дальше вы этот номер карты маскируете и у вас два типа - один про полный набор цифр, другой про что-то типа "последние 4 цифры". И все потребители ваших данных знают, получают они полный номер или маскированный. И это контракт.
inkelyad
19.01.2025 15:58LengthInch length = 5; // здесь значение в дюймах или сантиметрах?
В языке, поддерживающем единицы измерения для числовых переменных - должно посылать с сообщением вида "Инициализация типизированного варианта значением без размерности".
Если эмулировать - то дожно быть что-то в духе
LengthInch length = LengthInch.ofUnitless(5);
Cels
19.01.2025 15:58Так а что это меняет? цифра 5 - может также быть как дюймами, так и сантиметрами и также без ошибок передастся в функцию.
inkelyad
19.01.2025 15:58Так а что это меняет?
Заставляет вспомнить программиста "а в чем мы тут считаем-то?"
А в процессе вычислений - мешает складывать сантиметры с дюймами, потому что типы не совпали. Ну или автоматически переводит одно в другое, если методы перевода написали.
Т.е. в языке без типов
int velocityOfCar = 1 // в метрах секунду int velocityOfPedestrian = 2 // в футах секундв // тут много кода и входа-выхода из функций // а тут уже забыли, что единицы не сходятся carIsFasterThanPedestrian = velocityOfCar > velocityOfPedestrian
В языке, что поддерживает единицы измерения - в сравнении будет ругань.
Cels
19.01.2025 15:58ну да, а если единиц измерения, помимо футов и метрах, еще есть в ярдах, миллиметрах, километрах, сантиметрах и т.д., то для каждой отдельную переменную и подтип делать? При добавлении новой ед. изм. менять остальной код?
разве не лучше:
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()){ ... }
Компактно, легко понять, легко расширить и модифицировать, аккуратная архитектура. Чем хуже подтипов?
inkelyad
19.01.2025 15:58Так тоже можно, да. Тут оно работает потому что и то и то - мера расстояния. Т.е. именно различные единицы одного и того же.
А присвоение копеек к метрам как ругаться?
Cels
19.01.2025 15:58Почему "Так тоже можно, да.", а не "Так лучше, да."? - в случае с классом, мы имеем легко читаемый, расширяемый и модифицируемый один тип переменной, а не 150 разных типов, 150 перегрузок, переписывание кода при изменении/добавлении ед. изм. и т.д.
А присвоение копеек к метрам как ругаться?
У вас примеры, не имеющие отношения к реальности. Никто в здравом уме, не будет сравнивать теплое с мягким. Ну а если такой найдется, то никакие подтипы/проверки/валидации уже не помогут.
inkelyad
19.01.2025 15:58Никто в здравом уме, не будет сравнивать теплое с мягким.
Перепутать местами два целочисленных аргумента где первый означает что-то одно, а второй - другое, вроде бы, не такая уж редкая ошибка. То самое присвоение метров к копейкам и получается в момент передачи аргумента.
То что в существующих языка оно все порядком неудобно получается - с этим согласен. Потому и говорил, что жалко, что единицы измерения языками нормально не поддерживаются.
transcengopher
19.01.2025 15:58ну да, а если единиц измерения, помимо футов и метрах, еще есть в ярдах, миллиметрах, километрах, сантиметрах и т.д., то для каждой отдельную переменную и подтип делать?
Если нет явного требования запретить сравнения длин в разных единицах, то смысла заводить по отдельному классу для каждой единицы измерений нет - но никто такого и не предлагал делать.
Lengh{uint, LenghUnit}
будет куда удобнее, а если его чуть более развить, но и никакогоToOneUnit
не нужно, а вместо этого определить некийIsGreater
, и пусть оно внутри уже само разбирается.Компактно, легко понять, легко расширить и модифицировать, аккуратная архитектура. Чем хуже подтипов?
Ничем не хуже, а наоборот.
Вы скорее всего смешали аргументацию про тип
ImageID
с про типDimension
из вообще другого комментария, от того и сомнения.
inkelyad
19.01.2025 15:58Вы скорее всего смешали аргументацию
Это я немного виноват, хотел показать, что запретить совместную работу длины в футах и длины в метрах - хорошая идея.
Потому что и то и то есть нормальное число и работает как число во все местах где математика есть. Те. после компиляции информация о типе теряется и остается просто арифметика.
netch80
19.01.2025 15:58А так его реально и делают, только чуть иначе: через темплеты или дженерики, где что есть. В качестве готовой реализации можно посмотреть на std::chrono из C++. Ну или Length<inch> и Length<millimeter> в вашем случае. Можно добавить конверсию между ними, например
template<> double Convert<Inch, Millimeter>(double ivalue) { return ivalue * 2.54; }
и где запросят конверсию (явно или неявно) - она будет вызвана через такую функцию.
Aleshonne
19.01.2025 15:58Немного информации к сведению. Из условно мейнстримных языков F# поддерживает единицы измерения довольно давно (лет 15 как минимум) и ругается, если их перепутать. Но вывод типов работает в этом случае с перебоями, почти всё нужно указывать явно.
transcengopher
19.01.2025 15:58отвечу также - а почему не должно?
Потому что идентификатор пользователя - это на самом деле не настоящий номер, и любые математические операции с ним бизнес-смысла не имеют.
а почему не так:
Image? findImageBy(uint ImageId)
Image[] findImagesBy(uint UserId)
Потому что можно вызвать findImagesBy(image.id), и будет баг. А ещё потому что я могу добавить ещё какой-нибудь
findImagesBy(ImageGroupID id)
- а вам нужно будет в названии метода перечислять все предикаты, как это в Spring Data делается, и в противном случае диспетчеризация сломается.программист знает, что передает, куда передает и какой ожидается результат.
Ну, так и запишем - в мире нет ни одного программиста. Потому что если бы это верно было, то в программах никогда не было бы багов.
код переусложняют лишними классами, интерфейсами и прочими не нужными конструкциями
Напротив, мы обсуждаем не "пере"-усложнение кода, а насыщение его дополнительной документацией, в которой сущности, которые имеют явный бизнес-смысл - действительно помечены соответствующим типом, и с ними можно совершать только заранее предусмотренные при разработке операции с ожидаемым результатом. Код от этого не становится ни сложнее, ни проще, но упрощается дальнейшая разработка и поддержка.
а в чем упрощения не пояснили
Как это не пояснил, когда уже третье (или четвёртое?) сообщение на разные лады повторяю одно и то же пояснение?
для сравнения н-р ширины, нужно немного клея
Не понял, что такое н-р, но клей-то действительно нужен, особенно для сравнения 200em с 1920px - в вашем языке почти гарантированно нет возможности сравнивать такие вещи из коробки, а значит кто-то их должен был написать. И клей, если читать внимательно, я предложил только для конкретного особого случая. А в другом случае, предположенном в том же комментарии - я предлагаю, что не надо никакого специального кода для написания сравнения размеров окна и размеров картинки, потому что и то и другое в принципе можно выразить и общим типом Dimension.
Я уверен, что подтипы сделали не просто так и где-то они нужны, но точно не для упрощения кода, а скорее для безопасности.
Если я где-то сказал, что такой подход упрощает код - я оговорился. Но, вроде бы, я и не говорил именно про упрощение именно кода. Разработку такой подход точно упрощает, и будущую поддержку тоже.
Cels
19.01.2025 15:58Перепутать местами два целочисленных аргумента где первый означает что-то одно, а второй - другое, вроде бы, не такая уж редкая ошибка. То самое присвоение метров к копейкам и получается в момент передачи аргумента.
На самом деле редкая - ошибки обычно не в передаче не того параметра в метод, а в самой реализации метода. Если вы передадите не тот параметр, то это вызовет или ошибку при запуске или результат будет сильно неожиданным и при первой же отладки, сразу станет все ясно.
Если нет явного требования запретить сравнения длин в разных единицах, то смысла заводить по отдельному классу для каждой единицы измерений нет - но никто такого и не предлагал делать.
Я и не предлагаю заводить по отдельному классу - класс один.
Lengh{uint, LenghUnit} будет куда удобнее, а если его чуть более развить, но и никакого ToOneUnit не нужно, а вместо этого определить некий IsGreater, и пусть оно внутри уже само разбирается.
Ну это уже тонкости реализации, ни на что не влияющие. Можно хоть так, хоть этак, хоть оба метода использовать когда нужно.
Потому что идентификатор пользователя - это на самом деле не настоящий номер, и любые математические операции с ним бизнес-смысла не имеют.
Ну с ИД пользователя возможно и не имеют, а с деньгами, н-р (например), вполне могут иметь.
А ещё потому что я могу добавить ещё какой-нибудь findImagesBy(ImageGroupID id) - а вам нужно будет в названии метода перечислять все предикаты, как это в Spring Data делается, и в противном случае диспетчеризация сломается.
Перезагрузка. А если это не ИД, а ед. измерения или валюты - сколько будет таких перезагрузок?
findImagesByGroupId(uint id) - ничуть не хуже, все понятно. Какие это предикаты мне придутся перечислять?Ну, так и запишем - в мире нет ни одного программиста. Потому что если бы это верно было, то в программах никогда не было бы багов.
Т.е. вы не знаете, что передаете, куда передаете и какой ожидается результат от функции? Как написал выше, ошибки в основном в самой реализации функции, а не в передаче не тех параметров (которые отлавливаются при первом запуске).
Напротив, мы обсуждаем не "пере"-усложнение кода, а насыщение его дополнительной документацией...
Ну да, 150 перезагрузок, 150 разных типов переменных, вместо одного класса и нескольких функций очень этому способствуют и прям облегчают разработку и делают код чище и проще, особенно если что-то нужно изменить/добавить.
Как это не пояснил, когда уже третье (или четвёртое?) сообщение на разные лады повторяю одно и то же пояснение?
Так не убедительно - слишком все за уши притянуто и выглядит, как защита от дурака при куче не нужных конструкций.
Не понял, что такое н-р, но клей-то действительно нужен, особенно для сравнения 200em с 1920px - в вашем языке почти гарантированно нет возможности сравнивать такие вещи из коробки, а значит кто-то их должен был написать.
function ToOneUnit() - приведет к единым единицам.
Разработку такой подход точно упрощает, и будущую поддержку тоже.
Не соглашусь с вами - чем он упрощает разработку? перезагрузками, кучей разных типов, легкой модификацией? - нет, код становится больше, типов много, при добавлении/изменении типа, нужно менять остальной код - все это сильно повышает вероятность ошибок. А через пол года-год, не работая с проектом, все будет еще сложнее.
transcengopher
19.01.2025 15:58Ну с ИД пользователя возможно и не имеют, а с деньгами, н-р (например), вполне могут иметь.
Но при этом, деньга - это не число, а число-и-валюта. И там тоже нельзя просто взять и прибавить, если валюты отличаются.
А если это не ИД, а ед. измерения или валюты - сколько будет таких перезагрузок?
Столько, сколько потребует логика приложения.
findImagesByGroupId(uint id) - ничуть не хуже, все понятно. Какие это предикаты мне придутся перечислять?
"ByGroupId" - это как раз предикат. Упомянутый мной Spring Data из имени методов может запросы генерировать, вот.
Т.е. вы не знаете, что передаете, куда передаете и какой ожидается результат от функции?
Если это не мой код - то нет, сначала придётся всё прочитать. Код, написанный мной год назад - это, в принципе, уже тоже не мой код, при этом.
Как написал выше, ошибки в основном в самой реализации функции, а не в передаче не тех параметров (которые отлавливаются при первом запуске).
В отдельных случаях у вас в функцию передаются UserID и ImageID, которые в рамках теста обе равны единице, потому что база чистая. Первый запуск такое отлавливает не всегда. И даже если отлавливает запуск - это всё равно куда дольше, чем если оно просто сразу же в IDE не скомпилируется.
Ну да, 150 перезагрузок, 150 разных типов переменных, вместо одного класса и нескольких функций очень этому способствуют и прям облегчают разработку и делают код чище и проще, особенно если что-то нужно изменить/добавить.
Если именно в этом коде задействовано 150 разных видов сущностей/переменных, то скрыть их настоящий тип за всякими
int
иstring
точно разработку сделает сложнее, а не проще.как защита от дурака при куче не нужных конструкций.
Тестирование - это так-то тоже защита от дурака, ведь у настоящих программистов всё работает и так, ну максимум после второго запуска \srcsm.
А уж сколько для тестирования конструкций всяких придумали...
function ToOneUnit() - приведет к единым единицам.
Её тоже кто-то должен написать, то есть это усложняет код.
при добавлении/изменении типа, нужно менять остальной код
Не нужно, если это новый тип - потому что существующий код о том типе ничего не знает. А при изменении то, что оно компилироваться перестанет - это куда лучше, чем если бы оно молча собралось, и проблемы проявились бы только после "первого запуска".
Cels
19.01.2025 15:58Но при этом, деньга - это не число, а число-и-валюта. И там тоже нельзя просто взять и прибавить, если валюты отличаются.
Зависит от ситуации: если к примеру, нужно сделать наценку в 2 раза больше, то тип валюты знать не обязательно.
"ByGroupId" - это как раз предикат. Упомянутый мной Spring Data из имени методов может запросы генерировать, вот.
Понял, но у меня здесь это просто часть названия функции, чтобы было понятно какой параметр передать.
Если это не мой код - то нет, сначала придётся всё прочитать. Код, написанный мной год назад - это, в принципе, уже тоже не мой код, при этом.
Безусловно - не зная код, вы ничего не сможете написать.
В отдельных случаях у вас в функцию передаются UserID и ImageID, которые в рамках теста обе равны единице, потому что база чистая. Первый запуск такое отлавливает не всегда. И даже если отлавливает запуск - это всё равно куда дольше, чем если оно просто сразу же в IDE не скомпилируется.
Вернет пустой результат. Но обычно все-таки база содержит необходимые записи, либо используются фикстуры, иначе как вы проверите работоспособность без базы?
Если именно в этом коде задействовано 150 разных видов сущностей/переменных, то скрыть их настоящий тип за всякими
int
иstring
точно разработку сделает сложнее, а не проще.В том-то и дело, что используется несколько простых классов (https://habr.com/ru/articles/874584/comments/#comment_27813296), а не на каждый чих по новому типу переменной. Например класс MyUnit для ед. измерения, MyMoney для хранения всех валют и т.д.
Её тоже кто-то должен написать, то есть это усложняет код.
нет - она будет абсолютно простая (с элементарной логикой), где разберется даже ребенок (ссылка на несколько строк выше)
Не нужно, если это новый тип - потому что существующий код о том типе ничего не знает. А при изменении то, что оно компилироваться перестанет - это куда лучше, чем если бы оно молча собралось, и проблемы проявились бы только после "первого запуска".
помимо создания нового типа, вам его еще надо как-то обрабатывать, добавлять перегрузки, менять уже существующий код и т.д., а если использовать н-р класс MyUnit, то просто добавить строку в перечисление и простой расчет в функцию ToOneUnit(), ну или IsGreater() в том же классе.
И в целом, большой разницы нет - будут ли ошибки во время компиляции или во время первого запуска. Код компилируются обычно перед запуском.
transcengopher
19.01.2025 15:58Вернет пустой результат
Или не пустой, потому что для значения uint есть и пользователь, и картинка, и что-то найдётся даже если аргументы перепутать местами. Ну подумаешь, чужие картинки кому-то показали.
Например класс MyUnit для ед. измерения, MyMoney для хранения всех валют и т.д.
Откуда тогда взялось 150 классов?
нет - она будет абсолютно простая
Ну уж точно сложнее, чем реализация ImageID в лоб.
помимо создания нового типа, вам его еще надо как-то обрабатывать, добавлять перегрузки, менять уже существующий код и т.д.
Ну да, типа того. А часто у вас новая фича состоит из добавления свойства в класс и всё? Существующий код менять всё равно нужно.
большой разницы нет - будут ли ошибки во время компиляции или во время первого запуска.
О, нет - это огромная разница по времени. Особенно в случае когда первый запуск будет только с CI, потому что машина разработчика такое приложение не потянет по любой причине.
Код компилируются обычно перед запуском.
В современных IDE код компилируется инкрементально практически постоянно, параллельно с его изменением.
cupraer
19.01.2025 15:58В современных IDE код компилируется инкрементально практически постоянно, параллельно с его изменением.
Как это от IDE-то зависит? Что вообще такое «современная IDE»? Что происходит в «современных IDE» с кодом на перле, руби, питоне, джаваскрипте? Если вы только про джаву — то код компилировался непрерывно еще в Forte (позднее — NetBeans) — 25 лет назад.
Cels
19.01.2025 15:58В отдельных случаях у вас в функцию передаются UserID и ImageID, которые в рамках теста обе равны единице, потому что база чистая.
Или не пустой, потому что для значения uint есть и пользователь, и картинка, и что-то найдётся даже если аргументы перепутать местами. Ну подумаешь, чужие картинки кому-то показали.
вы уже сами определитесь, чистая база или нет. в случае чистой базы вернет пустой результат, в случае если в функцию получения картинки по ИД, передать ИД пользователя - само собой, если ИД найден, то будет показана чужая картинка.
Откуда тогда взялось 150 классов?
я такого не говорил - наоборот, нужен всего один класс на N количество разных типов.
Ну уж точно сложнее, чем реализация ImageID в лоб.
как вы это поняли?
Ну да, типа того. А часто у вас новая фича состоит из добавления свойства в класс и всё? Существующий код менять всё равно нужно.
в данном случае еще метод ToOneUnit() поправить в том же классе и все.
О, нет - это огромная разница по времени. Особенно в случае когда первый запуск будет только с CI, потому что машина разработчика такое приложение не потянет по любой причине.
что за приложение и как делают его отладку?
IQuant
19.01.2025 15:58С подтипами как-то поорганизованнее получается ИМХО. К примеру: вот куда складывать документацию о том, какой именно формат ожидается от переменной user_phone_number? Если это новый тип вокруг строки, то всё довольно понятно - это идёт в докстринг к типу, со всеми прилагающимися удобвствами вроде работы IntelliSense с ней.
findImageByUserId вообще можно сделать методом UserId, тогда узнать о существовании этой функции как таковой будет проще.
По части подтипов в стандартных типов: файловые пути на первый взгляд просто текст, но в последнее время их делают подтипами байтовых массивов. Примеры можно найти в Python (pathlib.Path), c++ (std::filesystem::path), Rust (std::path::PathBuf).
inkelyad
19.01.2025 15:58файловые пути на первый взгляд просто текст
Что не очень верно. Один и тот же путь по дереву файловой системы A->B->C->D в виндовом представлении и юниксном выглядит по разному.
netch80
19.01.2025 15:58Если вы про то, что разделители разные, то это решается обычно на других уровнях - где-то ищется разделитель как константа и используются методы типа sys.path.join().
А вот на уровне одного компонента свои проблемы - набор допустимых символов и проблемы коллации...
inkelyad
19.01.2025 15:58и что вы будете ему присваивать? т.е. это же все равно строка, число или еще что-то.
Что-то мне кажется, что оно называется целое со связанной размерность или единицей измерения.
Количество ящиков и количество копеек на счете могут быть целым числом, но присваивать одно другому - ну так себе идея. Печально, кстати, что в основные языки этого понятия с руганью при неправильном присваивании не завезли.
Sanchous98
19.01.2025 15:58По принципам ООП вам как раз не надо разбираться в том, как работает каждый класс. Вы должны знать только их поведение и работать с ними как с "черными ящиками". Если у вас Image.type типизирован как ImageType, вас не должна волновать его реализация для работы с ним, достаточно понимания, что этот тип представляет собой валидный тип изображения. Вы же, используя фреймворки, не изучаете реализации каждого компонента. Скорее всего вы читаете документацию
vadimr
19.01.2025 15:58А потом width будет не сравнить с шириной окна, потому что они разных типов.
transcengopher
19.01.2025 15:58потому что они разных типов
Не вижу веских причин им быть разных типов именно так, чтобы они были несовместимы для сравнений.
И даже если почему-то вам прямо нужно чтобы это было так, но у вас, как программиста, всегда остаётся возможность написать немного клея, и явным образом добавить сравнение между ними.
Это может послужить, также, хорошим напоминанием о том, что размеры картинок и окон бывают в пикселях, а бывают в
em
.
vanxant
19.01.2025 15:58В таком виде вы можете сделать id = width или даже id *= width. Напрямую разве что из-за опечатки, а вот через несколько уровней стека - легко.
norguhtar
19.01.2025 15:58Особенно непонятно зачем это делать для программы на Go, где зачастую интерфейс можно прикрутить в любой момент позже (там "классы" не требуют указания "implements").
Для тестов. Добавляете интерфейс и вуаля вы можете подсовывать mock реализации и тестировать поведение.
php7
Раньше на хабре даже предъяви кидали типа "А ты используешь SOLID?"
И попробуй повыступать против догм - получи readonly
CBET_TbMbI
Просто большинство догм догмами не являются. Вернее, являются одними из.
Например, есть ещё KISS (Keep it simple, stupid), который даёт свою отличную от SOLID трактовку.
А ещё есть обычный "Ты делает программу для решения квадратных уравнений, нафиг тебе вообще там классы сдались"
И что? Кто из них прав? Да все правы и все неправы. Каждому подходу своё время и своё место.
posledam
Как можно найти противоречие там, где его отродясь нет и не было? KISS не противоречит SOLID, который не навязывает классы.
CBET_TbMbI
Я где-то писал слово "противоречие"? Вроде, нет. Но и полного сходства между ними не вижу.
Это просто 2 разных комплекса советов, которые живут сами по себе. В чём-то похожи, в чём-то нет.
posledam
Это разные принципы, а не разные трактовки для одного и того же. Или я в упор не понял посыла.
Сообщения вида "делай хорошо, плохо не делай", "каждый случай индивидуален", "все люди человеки", которые при всей своей правоте несут ровно ноль информации и только забивают эфир.
cupraer
Мне лень приводить примеры на все пять букв, но SRP (да и все остальные) подразумевают пять сущностей там, где KISS — одну, да и само слово «абстракция», которое повторяется в SOLID чаще вспомогательных частей речи — противоречит KISS.
CBET_TbMbI
Именно это я и написал.
Посыл в том, что я поддерживаю идею, что ни на один из них не надо молиться. Про каждый надо решить "а подходит ли он именно под эту задачу"
posledam
Так ни на что не надо молиться, это же итак должно быть очевидно.
ManulVRN
Я слышал в другой формулировке: если тебе нужно здесь и сейчас помыть чашку - просто помой чашку, а не городи три слоя абстракции с иерархией классов.
О! Подумалось, что это ИТ-вариант бритвы Оккама, о неумножении сущностей сверх необходимого.