TL;DR
Всем привет!
Недавно работал над задачей. Нужно было получить из сети некоторые объекты по REST и обработать.
Ну все вроде бы ничего сложного. Загрузил, спарсил, вернул. Ок. Затем нужно было полученный массив обработать. Вкратце, по особой логике просуммировать некоторые поля - это могла быть строка, число или null. Начал делать как обычно: создал переменную sum, начал цикл for, в нем начал заниматься основной логикой. Закончил.
Продолжил кодить. Хоба! Эта же логика. Не стал копипастить, вынес в отдельную функцию. Тоже все хорошо.
Начал заниматься 3 задачей. Объединить результаты нескольких вычислений. Опять циклом начал перебирать. Но тут появилась мысль:
“А что, если создать для этого отдельный объект?”
Да нет, чушь! Ведь не существует в реальном мире ОбъединителяКакогоТоРезультата. Но что, если сделать? Попробуем.
Какого!!!?? Почему все вмиг стало так просто? Передал в конструктор нужные объекты и сделал методы, которые применяли свою логику к содержащимся в них объектам. Всего-лишь несколько строчек! Почему я так раньше не делал?
Я раззадорился. Начал видеть объекты везде. Это очень удобно: не нужно смотреть на каждый фрагмент кода с мыслью “а было ли это где нибудь раньше?”. А как тестировать легче стало!
Тут до меня дошло, что было со мной не так:
Я не разграничивал объекты реального мира и объекты в понимании ООП.
Объекты ООП != Объекты реального мира
Наверное главной моей ошибкой был недостаток практики: я много интересовался, читал, смотрел, но до кодирования руки не доходили. Поэтому, к моменту того события в моей голове было только 3 паттерна использования объектов:
DTO
Объекты из реального мира
Объекты, реализующие какой-то интерфейс (обычно для запросов по сети, для использования в DI контейнера)
Оглядевшись назад понял, что все дороги вели именно к такому мышлению:
В вузе нас учили ООП по каким-то моделям типа: “Вот это объект Человек. У него есть атрибуты Имя и Возраст”, а когда дело доходило до программирования, никто не смотрел как мы пишем код. Получалась каша из императивного программирования и набросков объектов.
Во всяких обучающих ресурсах (видео, книги, курсы) дают слишком простые примеры. Примеры слишком прямолинейные (как в выше перечисленном вузе). Не дают почувствовать мощь объектов.
Если были задачи, то слишком простые. Не тот уровень сложности, чтобы действительно над чем-то задуматься (например, приевшийся калькулятор). Они не показывали, что объекты могли бы решить многие проблемы.
В программе полно таких неявных объектов - служебных объектов: считают, фильтруют, агрегируют. Никогда не задумывался над тем, что практически любой for можно (наверное, даже лучше) заменить на объект, инкапсулирующий необходимую логику.
Пожалуй единственное, что меня ограничивало - идефикс, того, что объекты должны представлять концепции реального мира. Кто мне вообще это сказал?
Диаграммы мешают в понимании ООП
Но что насчет популярных инструментов проектирования? Нотаций. Наверное все видели различные UML диаграммы. Диаграмму классов так наверное любой программист должен был видеть хоть раз.
![Взято с https://medium.com/@uferesamuel/uml-class-diagrams-the-simple-approach-eee2d1ffc125 Взято с https://medium.com/@uferesamuel/uml-class-diagrams-the-simple-approach-eee2d1ffc125](https://habrastorage.org/getpro/habr/upload_files/d30/beb/f98/d30bebf9838d47d3ab7aa09d9804d82f.png)
ER диаграммы тоже хороши - они слишком сильно сцеплены с реальным миром. Там почти все представляет объекты реального мира.
![Взято с https://online.visual-paradigm.com/diagrams/templates/chen-entity-relationship-diagram/see-doctor-erd-chen-notation/ Взято с https://online.visual-paradigm.com/diagrams/templates/chen-entity-relationship-diagram/see-doctor-erd-chen-notation/](https://habrastorage.org/getpro/habr/upload_files/d29/0f1/993/d290f19939558be18cd5e44d070387d2.png)
Поразмыслив, я понял 3 вещи:
ER диаграмма ничего не имеет общего с ООП - это инструмент для бизнес-анализа. Я не обязан создавать такие же классы, как и на этой диаграмме. Кто мне такое сказал?
UML показывает высокоуровневую структуру программы: кто в ней есть и что они должны делать/иметь. Т.е. что делать, а не как делать. Реализация ложится на плечи программиста (спойлер, это будут методы на 100+ строк из циклов, условий и других прелестей)
Многие нотации ориентированы для простого понимания концепций программы - из каких компонентов состоит. Ничто не мешает нам вместо классов передавать массивы object. Не нужно ориентироваться на них как на истину в первой инстанции.
В итоге заканчиваем, тем что имеем много объектов. Ура, ООП! А что внутри? Громадные циклы на десятки строк, множество флагов и if’ов - полная императивщина.
Да о чем я говорю?
Что же я понял? Например,
public interface IWorkingScheduleService
{
// Возвращает тип дня: рабочий, предпраздничный, праздничный, выходной
int GetDayType(DateOnly date);
}
// Количество рабочих часов на каждый день недели
public class UserSchedule
{
public float Monday { get; set; }
public float Tuesday { get; set; }
public float Wednesday { get; set; }
public float Thursday { get; set; }
public float Friday { get; set; }
public float Saturday { get; set; }
public float Sunday { get; set; }
}
Задача - посчитать общее время рабочих часов.
Банально, да? Давайте сделаем функции:
public static class ScheduleHelpers
{
public static float GetTotalWorkingHours(IWorkingScheduleService service,
UserSchedule schedule,
DateOnly from,
DateOnly to)
{
// Какая-то логика
return 0;
}
public static float GetTotalWorkingHoursWithoutPreholiday(IWorkingScheduleService service,
UserSchedule schedule,
DateOnly from,
DateOnly to)
{
// Какая-то логика
return 0;
}
public static float GetTotalHolidayWorkingHours(IWorkingScheduleService service,
UserSchedule schedule,
DateOnly from,
DateOnly to)
{
// Какая-то логика
return 0;
}
}
Но тут мы заметим общую начальную часть: IWorkingScheduleService service, UserSchedule schedule
. Почему бы нам не вынести эту логику в отдельный объект?
public class WorkingScheduleCalculator
{
private readonly IWorkingScheduleService _service;
private readonly UserSchedule _schedule;
public WorkingScheduleCalculator(IWorkingScheduleService service,
UserSchedule schedule)
{
_service = service;
_schedule = schedule;
}
public float GetTotalWorkingHours(DateOnly from,
DateOnly to)
{
// Какая-то логика
return 0;
}
public float GetTotalWorkingHoursWithoutPreholiday(DateOnly from,
DateOnly to)
{
// Какая-то логика
return 0;
}
public float GetTotalHolidayWorkingHours(DateOnly from,
DateOnly to)
{
// Какая-то логика
return 0;
}
}
Как же стало удобно! Все находится рядом, сигнатуры стали короче и поддержка автодополнения в подарок - прелесть!
Выводы
Что я вынес из всего этого?
Объект это не концепция реального мира. Можно сделать объект который имеет имя, атрибуты, поведение, как у объекта реального мира, сделать максимально похожим, но это НЕ ОБЪЕКТ РЕАЛЬНОГО МИРА. Надо прекратить думать в данном ключе!
Объект - это (всего лишь) данные и функции, ассоциированные с ними
На каждый блок с логикой (цикл, последовательность условий и т.д.) я смотрю с мыслью: “Нельзя ли вынести это в отдельный объект?”
Таким же образом, смотрю на функции, которые принимают одинаковые аргументы. Их всех можно объединить в объекты, атрибутами которых являются эти общие аргументы.
P.S. Я не радикал, а за осмысленное и прагматичное использование объектов: для тривиальной логики можно оставить циклы, разрешаю)