Наши программы моделируют мир. Каждый, принявший постулаты ООП близко к сердцу, быстро столкнется с тем, что процесс моделирования в рамках этого метода принципиально не поддается детерминации. Обсудим подробнее.
Здесь и далее я буду рассматривать общекнижный пример с сотрудниками предприятия, писать будем на чем-то СИ-подобном. Наследовать класс Сотрудник (Employee) от класса Человек (Person) – прекрасная идея, особенно если хранить данные исключительно в памяти: SQL имеет некоторые проблемы с наследованием таблиц, но речь не об этом — ООП со своим иерархизмом, агрегациями, композициями и наследованиями предлагает идеальный способ организации данных. Проблемы с методами.
За каждым методом бизнес-логики стоит факт мира, который этот метод (чаще не в одиночку) моделирует. Факты программирования – это операции: дальше будем называть их так. Делая метод членом класса, ООП требует от нас привязать операцию к объекту, что невозможно, потому что операция – это взаимодействие объектов (двух и более), кроме случая унарной операции, чистой рефлексии. Метод ВыдатьЗарплату (PaySalary) может быть отнесен к классам Сотрудник (Employee), Касса (Cash), БанковскийСчет (Account) – все они равнозначны в праве владения им. Дилемма о расположении методов сопутствует всему процессу разработки: неловкое ее разрешение может оказаться критичным и даже фатальным.
В книгах по программированию честные авторы стыдливо признают, что «объекты – это как бы не совсем объекты», а ООП – всего лишь способ организации кода, а не механизм моделирования. Но все дело том, что «мир есть совокупность фактов, а не вещей» – отсюда принципиальная неспособность построить адекватную модель, применяя ООП в том виде, как этого требуют писатели учебников. Важно понять: в коде возможно моделировать мир, но атомами модели должны стать факты, а не объекты.
Оказавшись два года назад в мире разработки ПО, я с ужасом осознал, что тут до сих пор царит Аристотель: ООП – прямое порождение его философии. Этот одиозный мыслитель придумал флогистон для химиков, движущую силу для физиков – да что там говорить! — приложился к каждой из крупных дисциплин. История европейского прогресса – это история преодоления Аристотеля. Науке он принес больше зла, чем вся Святая инквизиция. Две тысячи лет потребовалось нашим ученым, чтобы затереть следы его «Физики». ООП – последнее пристанище его мрачной тени. Встречаясь с ним здесь — в ядре самых передовых технологий — хочется взять античный стилус (так в Риме называли палку погонщика скота) и загнать злобного грека обратно в его каменные склеп, как это давно уже сделали все остальные.
Людвиг Витгенштейн (его афоризм вынесен в заголовок) интересен тем, что, будучи доктором философии, не прочитал и двух страниц из Аристотеля: это его – Витгенштейна — слова. Неудивительно, что неопозитивизм – единственная «работающая» философская система, по сути – единственная на сегодня корректная философия: в подтверждение могу упомянуть, например, неопозитивиста Карла Поппера, который разработал современную методологию научного познания.
Функциональное программирование стало интуитивной реакцией миллионов разработчиков на туманность ООП – его полным отрицанием. Сам я не вижу причины в том, чтобы вовсе отказываться от иерархических принципов: в них много удобного. Известные языки не предлагают готовых средств для строительства иерархии операций, и мне сейчас сложно представить, как должен выглядеть ее синтаксис и звучать ключевые слова. Перенести фокус с объекта на операцию можно и наличными средствами: любой известный ООП-язык с этим справится.
Для начала разделим бизнес-логику на два пространства: данные и операции. Пространство данных не представляет собой ничего особенного – это классы данных, построенные в обычную иерархию – живущие только в оперативной памяти (POJO) или с возможностью сохранения состояния (сущности .NET, модели YII). Классы данных могут располагать собственными методами, как того требует фреймворк: принципиально лишь, чтобы эти методы не имели отношения к бизнес-логике.
Public Class Account {
Public string accountBankName;
Public string accountMfo;
Public string accountNumber;
}
Public Class Company {
Public string companyTitle;
Public string companyPhone;
Public Account companyAccount;
}
Public Class Department {
Public string departmentTitle;
Public Company departmentCompany;
}
Public Class Person {
Public string personName;
Public date personBirthDate;
}
Public Class Employee inherits Person {
Public Department employeeDepartment;
Public double employeeSalary;
Public Account employeeAccount;
}
Есть компания (Company) с несколькими подразделениями (Department) и сотрудниками (Employee), унаследованными от класса людей (Person). У компании есть счет (Account), с которого сотрудникам перечисляется зарплата. Соответственно, счет (Account) для получения зарплаты есть и у каждого сотрудника. Предположим, что наша программа должна уметь:
— принимать сотрудника на работу;
— выплачивать сотруднику зарплату;
— увольнять сотрудника с выплатой выходного пособия;
Прием на работу и увольнение сотрудника можно назвать кадровыми (Staff) операциями, а выплату выходного пособия и зарплаты – бухгалтерскими (Accounting) операциями.
Для каждой из операций понадобится:
— инициализировать данные о компании;
— инициализировать данные о сотруднике;
— распечатать некий документ.
Для приема / увольнения сотрудника нам придется:
— инициализировать данные о соответствующем подразделении фирмы;
Для двух указанных бухгалтерских операций нам также понадобится:
— инициализировать данные о банковских счетах сотрудника и компании.
Переходим к главному – собственно пространству операций. Мы реализуем его как иерархию классов, каждый из которых представляет собой нечто, что мы назовем «контекст операций» (Operation Context). Публичными методами (API) этих классов будут операции бизнес-логики, а свойства и приватные методы помогут в формировании абстракций. В соответствии с принятым ранее разделением, в нашей программе появятся классы StaffOperationContext и AccountingOperationContext, унаследованные от базового BaseOperationContext. Уложить вспомогательные члены в иерархию операций окажется проще, чем в иерархию объектов.
Public Class BaseOperationContext {
// конструкторы
BaseOperationContext () {
InitCompanyData();
}
BaseOperationContext (Employee employee) {
InitEmployeeData(Employee employee);
InitCompanyData();
}
// приватные и защищенные методы
private void InitCompanyData();
private void InitEmployeeData(Employee employee);
protected void PrintDocument(Document doc);
}
Public Class AccountingOperationContext inherits BaseOperationContext {
// конструкторы
AccountingOperationContext () {
super();
}
// приватные и защищенные методы
Private InitAccountData(Account account);
Private BankTransfer(Account account, Double amount);
// публичные методы – API класса
Public void PaySalary (Employee employee) // выплата з/п
{
// … некоторый кусок логики
InitAccountData (employee.employeeAccount);
// … некоторый кусок логики
BankTransfer (employee. employeeAccount, salaryAmount);
// … некоторый кусок логики
PrintDocument (someSalaryPayDocument);
}
Public void PayRedundancy (Employee employee) // выплата выходного пособия
{
// … некоторый кусок логики
InitAccountData (employee. employeeAccount);
// … некоторый кусок логики
BankTransfer (employee. employeeAccount, redundancyAmount);
// … некоторый кусок логики
PrintDocument (someRedundancyPayDocument);
}
}
Public Class StaffOperationContext inherits BaseOperationContext {
// конструкторы
StaffOperationContext (Employee employee) {
super(employee);
}
// приватные и защищенные методы
Private InitDepartmentData(Department department);
// публичные методы – API класса
Public void RecruitEmployee (Person person, Department department) // прием сотрудника
{
InitDepartmentData(department);
Employee employee = person;
// … некоторый кусок логики
PrintDocument (someRecruiteDocument);
}
Public void FireEmployee (Employee employee, Department department) // увольнение
{
InitDepartmentData(department);
// … некоторый кусок логики
// инициализируем AccountingOperationContext для платы выходного пособия
AccountingOperationContext accountingOC = new AccountingOperationContext ();
accountingOC.PayRedundancy (employee);
// .. некоторый кусок логики
PrintDocument (someFireDocument);
}
}
Этим кодом мы ломаем принципы ООП: наш метод FireEmployee относится к своему классу StaffOperationContext как «является», а не «содержится»: то есть увольнение сотрудника становится частным случаем кадровой операции (наследником), а не ее элементом (членом). Компенсацией будет обретение здравого смысла. Некорректное высказывание «увольнение сотрудника является членом объекта 'сотрудник'» мы заменяем на корректное «увольнение сотрудника является кадровой операцией». Корректность высказываний дает надежду на построение корректной модели.
Проблема детерминации (какой метод куда положить) не кажется разрешенной по умолчанию, но она разрешима. В своем единственном разработанном «по Витгенштейну» приложении я опирался на интерфейс. Имея на фронт-энде около десяти экранов, я разбил логику на десять контекстов операций с базовым контекстом наверху иерархии.
Можно по-разному классифицировать операции, важен сам принцип: объектно-ориентированное программирование мы заменяем на операционно-ориентированное. Еще раз повторюсь: речь идет исключительно о бизнес-логике – мире программы. Использовать классы среды и порождать их наследников ничто не мешает.
Не имея широкой айтишной эрудиции, могу предположить, что подобные мысли уже посещали многих философов и программистов. Вероятно, они были даже представлены в форме текстов – независимо и многократно. Сам я с подобными изысканиями не встречался, и мне потребовалось два года, чтобы прийти самостоятельно к этому удачному – как мне кажется – сочетанию объектно-ориентированного и функционального программирования (так представляется внешне).
Описанным способом я реализовал бизнес-логику в своем последнем на сегодняшний день проекте. В результате полного рефакторинга (проект достался мне по наследству) код сократился на 70% и стал невероятно дружественным в отношении любых — даже весьма значительных — правок. Опыт вышел удачным: предлагаю попробовать.
Автор: mnikolaev83