В мире разработки программного обеспечения существует множество "священных коров" — принципов и практик, которые принимаются как данность и редко подвергаются критическому анализу. Особенно показательна ситуация с принципами 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").


Что делать?


  1. Контекст важнее правил
    Вместо слепого следования принципам SOLID, нужно всегда учитывать контекст конкретного проекта. Маленькому проекту не нужна сложная архитектура корпоративного приложения.


  2. Простота — главное достоинство
    Если код можно написать проще — его нужно писать проще. Не стоит создавать сложные абстракции только потому, что "так говорит SOLID".


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



Более глубокий взгляд на проблему


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


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


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


Заключение


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


Приглашаю вас подписаться на мой канал в telegram

Комментарии (284)


  1. php7
    19.01.2025 15:58

    Раньше на хабре даже предъяви кидали типа "А ты используешь SOLID?"

    И попробуй повыступать против догм - получи readonly


    1. CBET_TbMbI
      19.01.2025 15:58

      Просто большинство догм догмами не являются. Вернее, являются одними из.

      Например, есть ещё KISS (Keep it simple, stupid), который даёт свою отличную от SOLID трактовку.

      А ещё есть обычный "Ты делает программу для решения квадратных уравнений, нафиг тебе вообще там классы сдались"

      И что? Кто из них прав? Да все правы и все неправы. Каждому подходу своё время и своё место.


      1. posledam
        19.01.2025 15:58

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


        1. CBET_TbMbI
          19.01.2025 15:58

          Я где-то писал слово "противоречие"? Вроде, нет. Но и полного сходства между ними не вижу.

          Это просто 2 разных комплекса советов, которые живут сами по себе. В чём-то похожи, в чём-то нет.


          1. posledam
            19.01.2025 15:58

            Например, есть ещё KISS (Keep it simple, stupid), который даёт свою отличную от SOLID трактовку.

            Это разные принципы, а не разные трактовки для одного и того же. Или я в упор не понял посыла.

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


            1. cupraer
              19.01.2025 15:58

              Мне лень приводить примеры на все пять букв, но SRP (да и все остальные) подразумевают пять сущностей там, где KISS — одну, да и само слово «абстракция», которое повторяется в SOLID чаще вспомогательных частей речи — противоречит KISS.


            1. CBET_TbMbI
              19.01.2025 15:58

              Это разные принципы

              Именно это я и написал.

              являются одними из (подходов к программированию)
              даёт свою отличную от SOLID трактовку (подходов к программированию)

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


              1. posledam
                19.01.2025 15:58

                Так ни на что не надо молиться, это же итак должно быть очевидно.


      1. ManulVRN
        19.01.2025 15:58

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

        О! Подумалось, что это ИТ-вариант бритвы Оккама, о неумножении сущностей сверх необходимого.


  1. capfsb
    19.01.2025 15:58

    удалил


  1. fo_otman
    19.01.2025 15:58

    Я еще не очень понимаю тему с огромным количеством классов исключений на все случаи жизни, которые отличаются друг от друга ... ничем. На вопрос тимлиду, нафига это нам, он ответил что-то типа "ну вот если в будущем нам понадобится вот этот вот конь в вакууме, то...". Потом он свалил, команда перешла на другие проекты, на проекте остался только я. Спустя год конь в вакууме не понадобился. Исключения я все вырезал к чертям, и вместо 50+ классов осталось штук 5. Никакого дискомфорта не заметил.


    1. aloginovpro
      19.01.2025 15:58

      Типизация исключений нужна для реализации различных реакций. Если реакция везде одинаковая (catch Throwable e), то нет смысла и типизировать исключения. То же самое для http кодов ответа.


      1. posledam
        19.01.2025 15:58

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


        1. sherbinko
          19.01.2025 15:58

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

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


          1. cupraer
            19.01.2025 15:58

            Как показывает практика, переписать тонну кода не представляет большой трудности, если ясны цели и есть работающий код.

            А вот для переписи с нуля движка одного браузера изобрели целый новый язык (!) — а потом сами же авторы языка на нём запрограммировать так, чтобы работало — не смогли. Есть мнение, что просто язык оказался так себе, но оно непопулярное.


            1. sherbinko
              19.01.2025 15:58

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


            1. hardtop
              19.01.2025 15:58

              Это Вы про rust? Разве Фаерфокс не переписан на расте?


              1. Sanchous98
                19.01.2025 15:58

                Не полностью. Переписали отдельные компоненты. Видимо те, которые были наиболее проблематичными


    1. 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;
      }


      1. Sliptory
        19.01.2025 15:58

        class Image
        {
            int imageId;
            int width;
            int height;
            string imageType;
        }

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


        1. 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";


        1. transcengopher
          19.01.2025 15:58

          Дело не в классе и его полях, а в остальном коде. При вашем подходе у вас будет какой-то int, и какой-то string. А при определении подтипов - сразу везде становится понятно, что эта конкретная переменная не просто некое число, а целый идентификатор картинки - а значит, по нему, скорее всего, можно поискать саму картинку где-нибудь, и он, например, уникален среди всех остальных хранящихся ImageID , но не будет уникален среди каких-нибудь UserID, а также что нет самостоятельного смысла в том, чтобы по ImageID искать пользователя.

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


          1. Cels
            19.01.2025 15:58

            Ну допустим сделал 20-30 подтипов на проект - как это упростит понимание? Например ImageType - это какой тип данных - string, int, enum или еще какой-то? т.е. мне нужно разобраться с произвольными типами данных и держать их всегда в своей голове?

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

            Опять же есть int, uint.

            Может это где-то и оправдано, но в большинстве проектов нет. Мы же здесь про упрощение кода говорим?

            Был бы благодарен за реальный пример, где использование подтипов оправдано.


            1. transcengopher
              19.01.2025 15:58

              ImageType - это какой тип данных - string, int, enum или еще какой-то

              ImageType - это ImageType, конечно же. Хотя это и не исключает наличия правил конвертирования его в int, string и обратно.

              Мы же здесь про упрощение кода говорим?

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


              1. Cels
                19.01.2025 15:58

                ImageType - это ImageType, конечно же.

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

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

                ну стандартные типы данных все и так знают и помнят, в отличии от подтипов и поле uint User.Id - вполне за себя говорит, что содержит идентификатор пользователя и запоминать его вовсе не нужно, а в коде подсветит, что тип данных uint, а не какой-нибудь UserIdentifier, который может быть чем угодно.

                Это, в своём роде, упрощает код за счёт ограничения возможного пространства применений для каждого значения.

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

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


                1. 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 найти куда сложнее).


                  1. 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 точно).

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


                    1. 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, выбросить тесты, сложить весь код в один файл, и тому подобное. В программировании вообще много чего можно.


                      1. 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, выбросить тесты, сложить весь код в один файл, и тому подобное. В программировании вообще много чего можно.

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

                        Я уверен, что подтипы сделали не просто так и где-то они нужны, но точно не для упрощения кода, а скорее для безопасности.


                      1. inkelyad
                        19.01.2025 15:58

                        программирую очень давно и не разу не приходилось искать все переменные по типу - т.е. что дает знание всех переменных определенного типа?

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

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


                      1. inkelyad
                        19.01.2025 15:58

                         в чем упрощения не пояснили, да и в комментарии ниже сами написали, что для сравнения н-р ширины, нужно немного клея).. И сколько клея нужно будет всего добавить в проект и как это упростит код?

                        Вот еще пример(теоретический, правда, потому что языки не поддерживают/не поддерживали), когда могло бы помочь:

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

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


                      1. Cels
                        19.01.2025 15:58

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

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

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

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

                        LengthInch length = 5; // здесь значение в дюймах или сантиметрах?


                      1. MonkAlex
                        19.01.2025 15:58

                        Почему иллюзорных то?

                        Дальше вы этот номер карты маскируете и у вас два типа - один про полный набор цифр, другой про что-то типа "последние 4 цифры". И все потребители ваших данных знают, получают они полный номер или маскированный. И это контракт.


                      1. inkelyad
                        19.01.2025 15:58

                        LengthInch length = 5; // здесь значение в дюймах или сантиметрах?

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

                        Если эмулировать - то дожно быть что-то в духе

                        LengthInch length = LengthInch.ofUnitless(5);


                      1. Cels
                        19.01.2025 15:58

                        Так а что это меняет? цифра 5 - может также быть как дюймами, так и сантиметрами и также без ошибок передастся в функцию.


                      1. inkelyad
                        19.01.2025 15:58

                        Так а что это меняет?

                        Заставляет вспомнить программиста "а в чем мы тут считаем-то?"

                        А в процессе вычислений - мешает складывать сантиметры с дюймами, потому что типы не совпали. Ну или автоматически переводит одно в другое, если методы перевода написали.

                        Т.е. в языке без типов

                        int velocityOfCar = 1 // в метрах секунду
                        int velocityOfPedestrian = 2 // в футах секундв
                        // тут много кода и входа-выхода из функций
                        // а тут уже забыли, что единицы не сходятся
                        carIsFasterThanPedestrian = velocityOfCar > velocityOfPedestrian

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


                      1. 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()){
                        ...
                        }

                        Компактно, легко понять, легко расширить и модифицировать, аккуратная архитектура. Чем хуже подтипов?


                      1. inkelyad
                        19.01.2025 15:58

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

                        А присвоение копеек к метрам как ругаться?


                      1. Cels
                        19.01.2025 15:58

                        Почему "Так тоже можно, да.", а не "Так лучше, да."? - в случае с классом, мы имеем легко читаемый, расширяемый и модифицируемый один тип переменной, а не 150 разных типов, 150 перегрузок, переписывание кода при изменении/добавлении ед. изм. и т.д.

                        А присвоение копеек к метрам как ругаться?

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


                      1. inkelyad
                        19.01.2025 15:58

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

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

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


                      1. transcengopher
                        19.01.2025 15:58

                        ну да, а если единиц измерения, помимо футов и метрах, еще есть в ярдах, миллиметрах, километрах, сантиметрах и т.д., то для каждой отдельную переменную и подтип делать?

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

                        Lengh{uint, LenghUnit} будет куда удобнее, а если его чуть более развить, но и никакого ToOneUnit не нужно, а вместо этого определить некий IsGreater, и пусть оно внутри уже само разбирается.

                        Компактно, легко понять, легко расширить и модифицировать, аккуратная архитектура. Чем хуже подтипов?

                        Ничем не хуже, а наоборот.

                        Вы скорее всего смешали аргументацию про тип ImageID с про тип Dimension из вообще другого комментария, от того и сомнения.


                      1. inkelyad
                        19.01.2025 15:58

                        Вы скорее всего смешали аргументацию 

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

                        Потому что и то и то есть нормальное число и работает как число во все местах где математика есть. Те. после компиляции информация о типе теряется и остается просто арифметика.


                      1. netch80
                        19.01.2025 15:58

                        А так его реально и делают, только чуть иначе: через темплеты или дженерики, где что есть. В качестве готовой реализации можно посмотреть на std::chrono из C++. Ну или Length<inch> и Length<millimeter> в вашем случае. Можно добавить конверсию между ними, например

                        template<> double Convert<Inch, Millimeter>(double ivalue) {
                          return ivalue * 2.54;
                        }

                        и где запросят конверсию (явно или неявно) - она будет вызвана через такую функцию.


                      1. Aleshonne
                        19.01.2025 15:58

                        Немного информации к сведению. Из условно мейнстримных языков F# поддерживает единицы измерения довольно давно (лет 15 как минимум) и ругается, если их перепутать. Но вывод типов работает в этом случае с перебоями, почти всё нужно указывать явно.


                      1. 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.

                        Я уверен, что подтипы сделали не просто так и где-то они нужны, но точно не для упрощения кода, а скорее для безопасности.

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


                      1. 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() - приведет к единым единицам.

                        Разработку такой подход точно упрощает, и будущую поддержку тоже.

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


                      1. transcengopher
                        19.01.2025 15:58

                        Ну с ИД пользователя возможно и не имеют, а с деньгами, н-р (например), вполне могут иметь.

                        Но при этом, деньга - это не число, а число-и-валюта. И там тоже нельзя просто взять и прибавить, если валюты отличаются.

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

                        Столько, сколько потребует логика приложения.

                        findImagesByGroupId(uint id) - ничуть не хуже, все понятно. Какие это предикаты мне придутся перечислять?

                        "ByGroupId" - это как раз предикат. Упомянутый мной Spring Data из имени методов может запросы генерировать, вот.

                        Т.е. вы не знаете, что передаете, куда передаете и какой ожидается результат от функции?

                        Если это не мой код - то нет, сначала придётся всё прочитать. Код, написанный мной год назад - это, в принципе, уже тоже не мой код, при этом.

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

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

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

                        Если именно в этом коде задействовано 150 разных видов сущностей/переменных, то скрыть их настоящий тип за всякими int и string точно разработку сделает сложнее, а не проще.

                        как защита от дурака при куче не нужных конструкций.

                        Тестирование - это так-то тоже защита от дурака, ведь у настоящих программистов всё работает и так, ну максимум после второго запуска \srcsm.

                        А уж сколько для тестирования конструкций всяких придумали...

                        function ToOneUnit() - приведет к единым единицам.

                        Её тоже кто-то должен написать, то есть это усложняет код.

                        при добавлении/изменении типа, нужно менять остальной код

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


                      1. 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() в том же классе.

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


                      1. transcengopher
                        19.01.2025 15:58

                        Вернет пустой результат

                        Или не пустой, потому что для значения uint есть и пользователь, и картинка, и что-то найдётся даже если аргументы перепутать местами. Ну подумаешь, чужие картинки кому-то показали.

                        Например класс MyUnit для ед. измерения, MyMoney для хранения всех валют и т.д.

                        Откуда тогда взялось 150 классов?

                        нет - она будет абсолютно простая

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

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

                        Ну да, типа того. А часто у вас новая фича состоит из добавления свойства в класс и всё? Существующий код менять всё равно нужно.

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

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

                        Код компилируются обычно перед запуском.

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


                      1. cupraer
                        19.01.2025 15:58

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

                        Как это от IDE-то зависит? Что вообще такое «современная IDE»? Что происходит в «современных IDE» с кодом на перле, руби, питоне, джаваскрипте? Если вы только про джаву — то код компилировался непрерывно еще в Forte (позднее — NetBeans) — 25 лет назад.


                      1. Cels
                        19.01.2025 15:58

                        В отдельных случаях у вас в функцию передаются UserID и ImageID, которые в рамках теста обе равны единице, потому что база чистая.

                        Или не пустой, потому что для значения uint есть и пользователь, и картинка, и что-то найдётся даже если аргументы перепутать местами. Ну подумаешь, чужие картинки кому-то показали.

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

                        Откуда тогда взялось 150 классов?

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

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

                        как вы это поняли?

                        Ну да, типа того. А часто у вас новая фича состоит из добавления свойства в класс и всё? Существующий код менять всё равно нужно.

                        в данном случае еще метод ToOneUnit() поправить в том же классе и все.

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

                        что за приложение и как делают его отладку?


                    1. IQuant
                      19.01.2025 15:58

                      С подтипами как-то поорганизованнее получается ИМХО. К примеру: вот куда складывать документацию о том, какой именно формат ожидается от переменной user_phone_number? Если это новый тип вокруг строки, то всё довольно понятно - это идёт в докстринг к типу, со всеми прилагающимися удобвствами вроде работы IntelliSense с ней.

                      findImageByUserId вообще можно сделать методом UserId, тогда узнать о существовании этой функции как таковой будет проще.

                      По части подтипов в стандартных типов: файловые пути на первый взгляд просто текст, но в последнее время их делают подтипами байтовых массивов. Примеры можно найти в Python (pathlib.Path), c++ (std::filesystem::path), Rust (std::path::PathBuf).


                      1. inkelyad
                        19.01.2025 15:58

                        файловые пути на первый взгляд просто текст

                        Что не очень верно. Один и тот же путь по дереву файловой системы A->B->C->D в виндовом представлении и юниксном выглядит по разному.


                      1. netch80
                        19.01.2025 15:58

                        Если вы про то, что разделители разные, то это решается обычно на других уровнях - где-то ищется разделитель как константа и используются методы типа sys.path.join().

                        А вот на уровне одного компонента свои проблемы - набор допустимых символов и проблемы коллации...


                1. inkelyad
                  19.01.2025 15:58

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

                  Что-то мне кажется, что оно называется целое со связанной размерность или единицей измерения.

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


            1. Sanchous98
              19.01.2025 15:58

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


          1. vadimr
            19.01.2025 15:58

            А потом width будет не сравнить с шириной окна, потому что они разных типов.


            1. transcengopher
              19.01.2025 15:58

              потому что они разных типов

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

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

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


        1. vanxant
          19.01.2025 15:58

          В таком виде вы можете сделать id = width или даже id *= width. Напрямую разве что из-за опечатки, а вот через несколько уровней стека - легко.


  1. norguhtar
    19.01.2025 15:58

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

    Для тестов. Добавляете интерфейс и вуаля вы можете подсовывать mock реализации и тестировать поведение.


    1. anar66
      19.01.2025 15:58

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


      1. norguhtar
        19.01.2025 15:58

        Простите что значит моки под что угодно? Реализацию моков как подсовывать будете?


        1. funca
          19.01.2025 15:58

          Например, в python часто используют манкипаичинг. В JavaScript создаётся специальная сборка с API, которое позволяет заменять одни объекты другими в рантайме.