За 9 лет разработки ПО  я периодически выступал в  роли ментора и сталкивался с проблемой, которую недавно озвучил начинающий программист после онлайн-курсов:

«Не понимаю, как делить код на классы».

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

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

Так и родилась идея написать статью «Шаблоны и принципы деления кода на классы».

Типы классов и шаблоны

Все классы я делю на 3 основных типа:

  1. Дата-класс

  2. Класс-управленец

  3. Класс-исполнитель

Дата-класс

Самый простой тип класса. Его объекты статичны — они ничего не делают в программе, все действия происходят над самим объектом.

Назначение:

хранить данные

Аналогия
из жизни:

ящик с предметами

Характеристики:

- отсутствует бизнес-логика, зависимости от других классов и взаимодействие с внешними системами;

- в полях классах хранятся данные;

- методы класса — это setter'ы и getter'ы, иногда вспомогательные методы для работы с данными внутри такого класса.

Популярные шаблоны

Data Access Object (DAO)

Дата-класс, описывающий схему хранилища данных (БД). Упрощает работу с БД, особенно в ORM–фреймворках, и отделяет бизнес-модели данных от моделей данных БД.

Data Transfer Object (DTO)

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

Value Object

Дата-класс, состояние которого не меняется после создания.

Класс-управленец

Класс, который координирует выполнение действий в программе.

Назначение:

- описывать бизнес-процессы или алгоритмы на языке программирования; 

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

Аналогия
из жизни:

конвейер

Характеристики:

- хранит ссылки на объекты классов-исполнителей и(или) на другие классы-управленцы;

- не хранит никакие данные;

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

Популярные шаблоны

Controller

Класс-управленец с несколькими точками входа, где каждая запускает отдельную функцию. «Запускает функцию» значит, что класс делегирует выполнение другим объектам, обычно сервисам.

Service

Класс-управленец, описывающий набор операций (процессов, алгоритмов) в рамках одного домена. Не делает действия сам, а делегирует их классам-исполнителям.

Pipeline

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

Класс-исполнитель

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

Назначение:

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

Аналогия
из жизни:

почтальон

Характеристики:

- может хранить данные в полях класса, необходимые для выполнения работы;

- может хранить ссылки на другие классы-исполнители;

- может взаимодействовать с внешними системами;

- выполняет реальные действия в программе.

Популярные шаблоны

Класс-утилита

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

Класс-помощник (класс-компаньон)

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

Lambda-объект

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

Схема типов классов

Этих трёх типов достаточно для организации программы любой сложности.

(дополнительно) Объединение типов (классы-гибриды)

Когда есть разделение на типы, то рано или поздно возникает вопрос: :

а можно ли их объединять?

Отвечу так:

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

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

Пример хорошего объединения типов

Класс Result

Назначение:

позволяет выполнить действие над объектом, хранящимся внутри Result, если предыдущее действие завершилось успешно.

Какие типы объединяет:

Дата-класс (Value Object) и Класс-исполнитель (Класс-компаньон)

Характеристики:

- не меняет своё состояние после создания (Value Object)

- выполняет определённые действия над дата-классом и привязан только к нему (Класс-компаньон)

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

Принципы деления кода на классы

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

  1. Деление по домену

  2. Деление по роли

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

И есть два важных (!) момента, без которых деление корректно работать не будет:

  • принципы деления нужно применять всегда при создании нового класса и определении его назначения;

  • нужно применять оба принципа вместе, а не по отдельности.

Деление по домену

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

Сложность этого деления заключается в том, что слово «домен» имеет довольно обобщённое значение и с английского языка означает «область интересов». А интерес — это очень относительное понятие, которое зависит от ситуации и человека, выполняющего деление. Причём, человек в разной ситуации может по-разному сделать деление кода по домену.

Но не смотря на это, чаще всего деление по домену — это деление по классу (типу) предмета. Например, вынести в отдельный класс весь код, который работает только с сущностью Customer. Или например, вынести весь код в отдельный класс, который отправляет REST-запросы, а в другой класс — SOAP-запросы.

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

Деление по домену — это деление большими кусками.

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

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

Как же тогда уменьшить объём кода в классе после деления по домену?

Дать новому классу специализацию, другими словами, определить его роль.

Деление по роли

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

У класса должна быть только одна роль, максимально специфичная. Соблюдение этого правила означает следование принципу единственной ответственности (Single Responsibility Principle).

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

Приведу пример роли для класса. Возьму роль Client. С разными доменами можно создать множество классов: RestClient, SoapClient, Repository (тоже Client, только для работы с БД). Другими словами, роли могут повторяться в разных доменах, как в RestClient и SoapClient, где Rest, Soap — домены, а Client — роль.

Пример

Теперь расcмотрю пример, чтобы продемонстрировать работу принципов и описанные типы классов.

Представим проект:

Веб-сервис (веб-приложение), позволяющий пользователю выполнять математические операции.

Пользовательский сценарий:
  1. Пользователь заходит на сайт.

  2. Выбирает из списка математическую операцию.

  3. Заполняет необходимые поля.

  4. Нажимает кнопку «Выполнить».

Ожидаемый результат:

  • в поле «Результат» отобразится текст с результатом операции.

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

Теперь представим задачу:

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

(Задача специально упрощена, чтобы сосредоточиться на разделении кода по классам, а не на деталях реализации)

Коддинг

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

Первая версия кода (по структуре) обычно выглядит так:

public class MathService {

    public String multiply(String strNumber1, String strNumber2) {
        int num1 = Integer.parseInt(strNumber1);
        int num2 = Integer.parseInt(strNumber2);
        int result = num1 * num2;
        String strResult = new Integer(result).toString();
        return strResult;
    }
}

Как образовался этот класс?

Разработчик взял домен основной бизнес-операции веб-приложения и обозначил его словом Math, затем назначил ему роль Service.

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

Какой тип у получившегося класса?

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

Почему объединение типов класс-управленец и класс-исполнитель не самое удачное?

Объединив эти два типа, вы получаете играющего тренера, который и руководит игроками, и сам играет. Другими словами, класс получает две роли.

А в принципе «Деление по роли», я делал важное уточнение:

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

В приведённом примере разработчик взял слишком обобщённую роль — веб-сервис. Именно этот смысл заключён в слове Service в названии класса. Поэтому текущий класс следовало бы переименовать в MathWebService, чтобы точнее обозначить его домен и роль:

public class MathWebService {

    public String multiply(String strNumber1, String strNumber2) {
        int num1 = Integer.parseInt(strNumber1);
        int num2 = Integer.parseInt(strNumber2);
        int result = num1 * num2;
        String strResult = new Integer(result).toString();
        return strResult;
    }
}

Какие роли у класса MathWebService?

Если смотреть на реализацию метода multiply, то можно увидеть, что метод делает два вида операций:

  • парсинг строк в числа и обратно;

  • выполнение математической операции.

Эти две операции — роли классов-исполнителей. Однако разработчики часто упускают другую роль, которая неявно присутствует в этом методе — процесс (алгоритм) выполнения операции multiplyс точки зрения веб-сервиса:

  1. распарсить входные данные;

  2. выполнить математическую операцию умножения;

  3. преобразовать результат в строку;

  4. вернуть ответ в виде строки.

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

У каждого процесса (алгоритма) есть шаги. Каждый шаг с точки зрения класса-управленца всегда отвечает на вопрос «Что делать?», но не включает в себя информацию «Как делать?». Перечитайте шаги процесса операции multiply и задайте к каждому пункту эти два вопроса: «Что делать?», «Как делать?».

Ни один пункт процесса не отвечает на вопрос «Как делать?» — так и должно быть. Но тогда возникает вопрос: кто же будет отвечать на вопрос «Как делать?»

Ответственность за «Как делать» лежит на  классе-исполнителе.

Таким образом, у класса MathWebService фактически три роли:

  1. описание процесса (алгоритма) работы метода выполнения пользовательского запроса (Управленец);

  2. реализация шага процесса: парсинг (Исполнитель);

  3. реализация шага процесса: математическая операция (Исполнитель).

Как же тогда поделить MathWebService на классы?

Самую сложную задачу мы уже выполнили: выделили три специфичные роли. Обычно с определением домена сложностей не возникает — чаще проблема в определении роли класса и его зоны ответственности.

Но я не буду сразу показывать финальный результат, а подведу к нему через последовательный рефакторинг. Именно этим путем — последовательными улучшениями через рефакторинг — идёт разработчик в реальных проектах независимо от его опыта.

Первое, что сделаю: выделю операцию конвертации «строка -> число, число -> строка» в два отдельных метода:

public class MathWebService {

    public String multiply(String strNumber1, String strNumber2) {
        int num1 = toInt(strNumber1);
        int num2 = toInt(strNumber2);
        int result = num1 * num2;
        String strResult = toString(result);
        return strResult;
    }

    private int toInt(String strNumber) {
        return Integer.parseInt(strNumber);
    }

    private int toString(Integer number) {
        return number.toString();
    }
}

Напомню, что я определил тип класса MathWebService как класс-управленец, где каждый метод — это название процесса. Методы toInt(String) и toString(Integer) не являются процессами веб-сервиса, они делают определённую работу внутри этих процессов (в данном случае, операции multiply), а значит они должны относиться к классу-исполнителю. Поэтому пора выделить их в отдельный класс. Согласно принципам, при создании класса нужно сделать два действия: определить домен и роль. Я определил их так: домен — «работа с числами», роль «конвертер».

дополнительное пояснение

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

Поэтому класс назову NumberConverter. Вот так выглядит код:

public class NumberConverter {

    public int toInt(String strNumber) {
        return Integer.parseInt(strNumber);
    }

    public int toString(Integer number) {
        return number.toString();
    }
}

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

public class MathWebService {
    private final NumberConverter numberConverter = new NumberConverter(); // для простоты DI не буду использовать в примере.

    public String multiply(String strNumber1, String strNumber2) {
        int num1 = numberConverter.toInt(strNumber1);
        int num2 = numberConverter.toInt(strNumber2);
        int result = num1 * num2;
        String strResult = numberConverter.toString(result);
        return strResult;
    }
}

Теперь класс MathWebService полностью делегирует задачи парсинга классу-исполнителю NumberConverter. Осталось делегировать выполнение операции умножения. Буду действовать по уже проверенной схеме: вынести в отдельный метод, а затем —  в отдельный класс.

public class MathWebService {
    private final NumberConverter numberConverter = new NumberConverter(); // для простоты DI не буду использовать в примере.

    public String multiply(String strNumber1, String strNumber2) {
        int num1 = numberConverter.toInt(strNumber1);
        int num2 = numberConverter.toInt(strNumber2);
        int result = multiply(num1, num2);
        String strResult = numberConverter.toString(result);
        return strResult;
    }

    private int multiply(int num1, int num2) {
        return num1 * num2;
    }
}

Хотя имя метода multiply(int, int) совпадает с методом multiply(String, String), в Java это разные методы из-за разных типов аргументов. Чтобы не путаться, можно переименовать multiply(String, String), например в multiplyFeature(String, String):

public class MathWebService {
    private final NumberConverter numberConverter = new NumberConverter(); // для простоты DI не буду использовать в примере.

    public String multiplyFeature(String strNumber1, String strNumber2) {
        int num1 = numberConverter.toInt(strNumber1);
        int num2 = numberConverter.toInt(strNumber2);
        int result = multiply(num1, num2);
        String strResult = numberConverter.toString(result);
        return strResult;
    }

    private int multiply(int num1, int num2) {
        return num1 * num2;
    }
}

Теперь вынесу метод multiply(int, int) в отдельный класс. Определю его домен как «математика», и роль как «операции», поэтому назову класс MathOperations:

public class MathOperations {

    public int multiply(int num1, int num2) {
        return num1 * num2;
    }
}

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

public class MathWebService {
    private final NumberConverter numberConverter = new NumberConverter(); // для простоты DI не буду использовать в примере.
    private final MathOperations mathOperations = new MathOperations(); // для простоты DI не буду использовать в примере.

    public String multiplyFeature(String strNumber1, String strNumber2) {
        int num1 = numberConverter.toInt(strNumber1);
        int num2 = numberConverter.toInt(strNumber2);
        int result = mathOperations.multiply(num1, num2);
        String strResult = numberConverter.toString(result);
        return strResult;
    }
}

Теперь MathWebService стал полноценным классом-управленцем. Он описывает процесс выполнения фичи, но не содержит деталей реализации,  и делегируя выполнение шагов классам-исполнителям. Согласно типизации, MathWebService соответствует шаблону Сервис, а NumberConverter и MathOperations следуют шаблону Класс-утилита.

На этом пример деления кода на классы завершён.

Кто-то возразит: зачем создавать столько классов ради 4 строк кода в методе?!

(дополнительно) мотивация на классовое деление

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

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

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

Возможно, первая мысль — добавить новый метод в класс MathWebService, ведь конвертация весов — тоже математическая операция. Но представим, что в MathWebService уже есть куча методов: сложение, вычитание, корень квадратный, возведение в степень и логарифм. Эти операции явно соответствуют домену «математика» и роли «сервис для математических операций». Конвертация килограммов в граммы ещё как-то подходит по домену, но не по роли.

Поэтому логичнее создать новый класс. Я определил для него домен «веса», а роль «сервис конвертации величин». Так класс и назвал MeasureConverterWebService.

public class MeasureConverterWebService {
    private final NumberConverter numberConverter = new NumberConverter(); // для простоты DI не буду использовать в примере.
    private final MathOperations mathOperations = new MathOperations(); // для простоты DI не буду использовать в примере.

    public String kilosToGramFeature(String strKilos) {
        int kilos = numberConverter.toInt(strKilos);

        int result = mathOperations.multiply(kilos, 1000);

        String strResult = numberConverter.toString(result);
        return strResult;
    }
}

Обратите внимание, что для создания MeasureConverterWebService в проекте уже были все необходимые компоненты: NumberConverter и MathOperations. Разработчик собрал метод kilosToGramFeature из имеющегося кода, как конструктор. Более того, к моменту написания этого метода код NumberConverter и MathOperations уже был протестирован и есть уверенность в его работоспособности. А любой новый код ещё нужно протестировать и проверить реальными пользователями.

Заключение

В заключение подсвечу основные моменты:

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

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

  • С помощью трёх типов классов можно организовать хорошую структуру программы любой сложности.

  • Принципы деления кода на классы рекомендую использовать всегда при создании нового класса.

важно на заметку

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

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

Возможно кто-то удивится, что я не упомянул в статье принципы объектно-ориентированного программирования (ООП), DRY или SOLID. Это важные принципы, но у них немного другое назначение, хотя они тоже могут быть связаны с делением кода на классы. Кто-то, посмотрев на пример, может кинуться в меня принципами KISS или YAGNI. Что ж скажу... это очень дискуссионная тема, и вы можете написать на неё свою статью, а моя на этом заканчивается.

Всем доброго времени суток.

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


  1. akardapolov
    30.10.2025 13:03

    Все классы я делю на 3 основных типа:

    1. Дата-класс

    2. Класс-управленец

    3. Класс-исполнитель

    Получается дата-класс - это данные без логики, т.е. анемичная модель. Как быть тогда с rich объектами (в терминах DDD) - куда их отнести в данной классификации? Или мы заходим на территорию другой парадигмы?


    1. VladimirPolukeev Автор
      30.10.2025 13:03

      Класс, который следует концепции Rich объект (в терминах DDD) представляет собой класс-гибрид (я упоминал об этом в статье). Он сочетает в себе сразу несколько типов. В основе Rich объекта лежит Дата-класс, в который добавляется, как минимум Класс-исполнитель.


  1. SergeyEgorov
    30.10.2025 13:03

    Просто не делите код на классы и жизнь станет проще и приятнее.


    1. evgenyk
      30.10.2025 13:03

      Это же Java, там так нельзя!


      1. SergeyEgorov
        30.10.2025 13:03

        В Java тоже можно все сильно упростить так, что почти все методы станут реализацией

        public abstract class hashmap_handler {
          public abstract void handle(HashMap<String, Object> state);
        }

        У меня все в таком ключе реализовано. Работает отлично. Читать код одно удовольствие. Думать вообще не надо.


        1. WieRuindl
          30.10.2025 13:03

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


          1. SergeyEgorov
            30.10.2025 13:03

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


            1. WieRuindl
              30.10.2025 13:03

              Встречался ли я с процессами, где люди явно не думали в начале и не думают в процессе? Да, разумеется. Могу ли я сказать, что эти процессы вызывали что-либо кроме боли, страдания и непонимания, почему бы чуть-чуть не подумать и не сделать лучше? Нет, ни разу

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


              1. SergeyEgorov
                30.10.2025 13:03

                Вы ответили на вопрос, который я не задавал вовсе :-)


  1. WieRuindl
    30.10.2025 13:03

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


    1. VladimirPolukeev Автор
      30.10.2025 13:03

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

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


  1. Emelian
    30.10.2025 13:03

    Все классы я делю на 3 основных типа:

    1. Дата-класс

    2. ласс-управленец

    3. Класс-исполнитель

    У меня другое деление. Основные типы используемых классов:

    1. Менеджер потоков

    2. Менеджер интерфейса

    3. Менеджер событий

    Точка входа: «Main.cpp», в функции «WinMain()» – одинаковой во всех проектах, которая создаёт и вызывает экземпляр класса «CApp» и ничего более.

    В классе «CApp» создается экземпляр класса «CMainWindow», инициализируются необходимые библиотеки, вроде, «GDIplus», класс (менеджер) потоков, при необходимости, цикл, либо менеджер событий и класс (менеджер) интерфейса (видов – дочерних окон, занимающих всю клиентскую область главного окна, разного рода компонентов и элементов).

    В классе «CMainWindow» создаются и инициализируются компоненты главного окна и функция их отображения при изменении размеров окна приложения.

    Каждый используемый компонент (класс) размещается в отдельной паре файлов: *.cpp и *.h;

    Для большей структуризации проекта могут использоваться отдельные каталоги. Например, можно создать отдельную папку для опенсорсного кода консольного видео-проигрывателя, переделанного под оконный интерфейс (см. скриншот моей программы «МедиаТекст»: http://scholium.webservis.ru/Pics/MediaText.png ).

    Пример простого приложения, разработанного в этой парадигме, см. в моей статье: «Минималистский графический интерфейс, на C++ / WTL, для консольного загрузчика» – https://habr.com/ru/articles/955838/ ).


  1. KoIIIeY
    30.10.2025 13:03

    А состояние DTO меняется после создания?)

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


    1. VladimirPolukeev Автор
      30.10.2025 13:03

      А состояние DTO меняется после создания?)

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

      Насчёт примера.
      Цель моего примера исключительно образовательная: показать работу принципов. Всё, что не касается этой цели было намерено убрано или упрощено.


  1. ayevdoshenko
    30.10.2025 13:03

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

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

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

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

    Само собой - есть задачи, где описанное в статье применимо и адекватно, но не надо превращать это в Золотой молоток.


    1. VladimirPolukeev Автор
      30.10.2025 13:03

      Насчёт примера.

      Цель моего примера исключительно образовательная: показать работу принципов. Всё, что не касается этой цели было намерено убрано или упрощено.


  1. freecom
    30.10.2025 13:03

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

    Объясните мне не русскому что означает на русском языке слово ментор я такого в словарях русского языка почему то не нашел


    1. VladimirPolukeev Автор
      30.10.2025 13:03

      Объясните мне не русскому что означает на русском языке слово ментор я такого в словарях русского языка почему то не нашел

      Обычно ментор = наставник.

      Более подробно с описанием из разных толковых словарей можно посмотреть здесь: https://gufo.me/dict/kuznetsov/ментор


  1. Konadren
    30.10.2025 13:03

    Может кто-нибудь, пожалуйста, объяснить уместность вынесения одной строки кода в отдельный метод на примере парсинга стринга -> инт (за multiply молчу)?


    1. VladimirPolukeev Автор
      30.10.2025 13:03

      Уместность зависит от категории мышления программиста.

      Программист, который думает процедурно, то он будет писать код примерно, как в этом примере:

      public class MathService {
      
          public String multiply(String strNumber1, String strNumber2) {
              int num1 = Integer.parseInt(strNumber1);
              int num2 = Integer.parseInt(strNumber2);
              int result = num1 * num2;
              String strResult = new Integer(result).toString();
              return strResult;
          }
      }

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

      Программист, который думает объектами, то он будет писать код примерно, как в этом примере:

      public class MathWebService {
          private final NumberConverter numberConverter = new NumberConverter(); // для простоты DI не буду использовать в примере.
          private final MathOperations mathOperations = new MathOperations(); // для простоты DI не буду использовать в примере.
      
          public String multiplyFeature(String strNumber1, String strNumber2) {
              int num1 = numberConverter.toInt(strNumber1);
              int num2 = numberConverter.toInt(strNumber2);
              int result = mathOperations.multiply(num1, num2);
              String strResult = numberConverter.toString(result);
              return strResult;
          }
      }

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

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


      1. Lewigh
        30.10.2025 13:03

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

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