«Світ є сукупністю фактів, а не речей»: Вітгенштейн та операційно-орієнтоване програмування

Наші програми моделюють світ. Кожен, хто прийняв постулати ООП близько до серця, швидко зіткнеться з тим, що процес моделювання в рамках цього методу принципово не піддається детермінації. Обговоримо докладніше.

Тут і далі я буду розглядати общекнижный приклад з працівниками підприємства, писати будемо на чомусь СІ-подібному. Успадковувати клас Співробітник (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% і став неймовірно дружнім стосовно будь-яких, навіть вельми значних — правок. Досвід вийшов вдалим: пропоную спробувати.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.