Дисклеймер: все нижеописанное относится к использованию ООП в бизнес-приложениях и я бы хотел вынести за скобки применение ООП для описания типов вроде int, string и т.д.
Концепцию ООП часто применяют для отражения объектов реального мира в коде программ. Так объект реального мира "Пользователь" может быть описан в коде как класс "Пользователь", у которого есть поля: имя и адрес электронной почты, а действия, которые можно совершить над объектами реального мира, превращаются в методы класса: назначить администратором, удалить учетную и т.д.
public sealed class User
{
public string Email { get; set; }
public string Name { get; set; }
public void Delete()
{
...
}
public void SetAsAdministrator()
{
...
}
}
В простых учебных примерах такой подход позволяет познакомить начинающих программистов с основными инструментами ООП. Но когда дело доходит до более сложных примеров, кол-во методов в классе растет, а с ними и количество строк кода — код становится сложно читаемым. Но, главное, связь между объектами реального мира, когда изменение одного объекта влечет за собой изменение другого (удаление учетной записи пользователя, приводит к удалению учетной записи из всех списков где она была отмечена), сложно отразить в коде одного класса. Для меня апофеозом такого подхода стал проект eShopOnContainers, когда для решения, казалось бы, простой задачи, которая была решена уже тысячи раз, приходится писать огромное кол-во строк кода. Связь того, как изменение состояния одного объекта влияет на другой объект, прослеживается не явно, через очередь событий и несколько классов. Кол-во зависимостей, требующихся для инстанцирования одного класса, растет, что приводит к сложности использования такого класса как в приложении, так и в тестах.
Вдобавок к этому, в современных системах практически отпала потребность хранить состояние объектов в приложении, все состояние хранят специализированные системы: базы данных или внешние сервисы (которые в итоге хранят его в БД). Задача многих приложений сводится к отображению состояния сущностей в разных проекциях и предоставлению методов изменения состояния с учетом правил предметной области этих приложений. В общем виде это можно представить как методы получения данных для отображения части состояния объекта и методы для изменения состояния. Описание и хранение самого состояния в приложении не требуется.
В классах-проекциях (Data Transfer Object), которые передают данные между слоями приложения, не требуется иметь методов изменения состояния. Тогда возникает вопрос, где же хранить методы, позволяющие получить состояние или изменять его? Мы в Retail Rocket пришли к решению сделать по одному классу на каждую такую задачу. Те классы, которые относятся строго к одному объекту, можно сгруппировать в папке с именем этого объекта, а те, что, например, меняют состояние более одного класса, положить в папку с именем Services.
Зачем же тогда нужен ООП, если все приложение — это набор функций? Для локализации границ зависимостей функции. ООП позволяет элегантно описать зависимости, которые требуются "функции".
Вот пример "функции", которая меняет состояние системы
internal sealed class RemoveVerificationRequestHandler
: IRemoveVerificationRequestHandler
{
private readonly IMongoCollection<VerificationRequest> verificationRequestCollection;
public RemoveVerificationHandler(
IMongoCollection<VerificationRequest> verificationRequestCollection)
{
this.verificationRequestCollection = verificationRequestCollection;
}
public void Handle(
VerificationRequestId verificationRequestId)
{
...
}
}
Для работы этой функции требуется 1 внешняя зависимость в виде интерфейса к конкретной коллекции в базе данных, ее мы и принимаем через конструктор и сохраняем в private поле.
Вот пример использования такой функции в MVC контроллере
public class VerificationListController
: Controller
{
private readonly IRemoveVerificationRequestHandler removeVerificationRequestHandler;
public VerificationListController(
IRemoveVerificationRequestHandler removeVerificationRequestHandler)
{
this.removeVerificationRequestHandler = removeVerificationRequestHandler;
}
public ActionResult RemoveVerificationRequest(
VerificationRequestId verificationRequestId)
{
this
.removeVerificationRequestHandler
.Handle(verificationRequestId);
return this
.RedirectToAction(
actionName: nameof(this.List));
}
public ActionResult List()
{
return this
.View("List");
}
}
Данный подход меняет привычное использование концепции ООП как способ описания объектов реального мира, на способ описания функции с ее аргументами, возвращаемым значением и зависимостями (сайд эффектами).