Наши программы моделируют мир. Каждый, принявший постулаты ООП близко к сердцу, быстро столкнется с тем, что процесс моделирования в р��мках этого метода принципиально не поддается детерминации. Обсудим подробнее.
Здесь и далее я буду рассматривать общекнижный пример с сотрудниками предприятия, писать будем на чем-то СИ-подобном. Наследовать класс Сотрудник (Employee) от класса Человек (Person) – прекрасная идея, особенно если хранить данные исключительно в памяти: SQL имеет некоторые проблемы с наследованием таблиц, но речь не об этом — ООП со своим иерархизмом, агрегациями, композициями и наследованиями предлагает идеальный способ организации данных. Проблемы с методами.
За каждым методом бизнес-логики стоит факт мира, который этот метод (чаще не в одиночку) моделирует. Факты программирования – это операции: дальше будем называть их так. Делая метод членом класса, ООП требует от нас привязать операцию к объекту, что невозможно, потому что операция – это взаимодействие объектов (двух и более), кроме случая унарной операции, чистой рефлексии. Метод ВыдатьЗарплату (PaySalary) может быть отнесен к классам Сотрудник (Employee), Касса (Cash), БанковскийСчет (Account) – все они равнозначны в праве владения им. Дилемма о расположении методов сопутствует всему процессу разработки: неловкое ее разрешение может оказаться критичным и даже фатальным.
В книгах по программированию честные авторы стыдливо признают, что «объекты – это как бы не совсем объекты», а ООП – всего лишь способ организации кода, а не механизм моделирования. Но все дело том, что «мир есть совокупность фактов, а не вещей» – отсюда принципиальная неспособность построить адекватную модель, применяя ООП в том виде, как этого требуют писатели учебников. Важно понять: в коде возможно моделировать мир, но атомами модели должны стать факты, а не объекты.
Оказавшись два года назад в мире разработки ПО, я с ужасом осознал, что тут до сих пор царит Аристотель: ООП – прямое порождение его философии. Этот одиозный мыслитель придумал флогистон для химиков, движущую силу для физиков – да что там говорить! — приложился к каждой из крупных дисциплин. История европейского прогресса – это история преодоления Аристотеля. Науке он принес больше зла, чем вся Святая инквизиция. Две тысячи лет потребовалось нашим ученым, чтобы затереть следы его «Физики». ООП – последнее пристанище его мрачной тени. Встречаясь с ним здесь — в ядре самых передовых технологий — хочется взять античныйстилус стимулус (так в Риме называли палку погонщика скота) и загнать злобного грека обратно в его каменные склеп, как это давно уже сделали все остальные.
Людвиг Витгенштейн (его афоризм вынесен в заголовок) интересен тем, что, будучи доктором философии, не прочитал и двух страниц из Аристотеля: это его – Витгенштейна — слова. Неудивительно, что неопозитивизм – единственная «работающая» философская система, по сути – единственная на сегодня корректная философия: в подтверждение могу упомянуть, например, неопозитивиста Карла Поппера, который разработал современную методологию научного познания.
Функциональное программирование стало интуитивной реакцией миллионов разработчиков на туманность ООП – его полным отрицанием. Сам я не вижу причины в том, чтобы вовсе отказываться от иерархических принципов: в них много удобного. Известные языки не предлагают готовых средств для строительства иерархии операций, и мне сейчас сложно представить, как должен выглядеть ее синтаксис и звучать ключевые слова. Перенести фокус с объекта на операцию можно и наличными средствами: любой известный ООП-язык с этим справится.
Для начала разделим бизнес-логику на два пространства: данные и операции. Пространство данных не представляет собой ничего особенного – это классы данных, построенные в обычную иерархию – живущие только в оперативной памяти (POJO) или с возможностью сохранения состояния (сущности .NET, модели YII). Классы данных могут располагать собственными методами, как того требует фреймворк: принципиально лишь, чтобы эти методы не имели отношения к бизнес-логике.
Есть компания (Company) с несколькими подразделениями (Department) и сотрудниками (Employee), унаследованными от класса людей (Person). У компании есть счет (Account), с которого сотрудникам перечисляется зарплата. Соответственно, счет (Account) для получения зарплаты есть и у каждого сотрудника. Предположим, что наша программа должна уметь:
— принимать сотрудника на работу;
— выплачивать сотруднику зарплату;
— увольнять сотрудника с выплатой выходного пособия;
Прием на работу и увольнение сотрудника можно назвать кадровыми (Staff) операциями, а выплату выходного пособия и зарплаты – бухгалтерскими (Accounting) операциями.
Для каждой из операций понадобится:
— инициализировать данные о компании;
— инициализировать данные о сотруднике;
— распечатать некий документ.
Для приема / увольнения сотрудника нам придется:
— инициализировать данные о соответствующем подразделении фирмы;
Для двух указанных бухгалтерских операций нам также понадобится:
— инициализировать данные о банковских счетах сотрудника и компании.
Переходим к главному – собственно пространству операций. Мы реализуем его как иерархию классов, каждый из которых представляет собой нечто, что мы назовем «контекст операций» (Operation Context). Публичными методами (API) этих классов будут операции бизнес-логики, а свойства и приватные методы помогут в формировании абстракций. В соответствии с принятым ранее разделением, в нашей программе появятся классы StaffOperationContext и AccountingOperationContext, унаследованные от базового BaseOperationContext. Уложить вспомогательные члены в иерархию операций окажется проще, чем в иерархию объектов.
Этим кодом мы ломаем принципы ООП: наш метод FireEmployee относится к своему классу StaffOperationContext как «является», а не «содержится»: то есть увольнение сотрудника становится частным случаем кадровой операции (наследником), а не ее элементом (членом). Компенсацией будет обретение здравого смысла. Некорректное высказывание «увольнение сотрудника является членом объекта 'сотрудник'» ��ы заменяем на корректное «увольнение сотрудника является кадровой операцией». Корректность высказываний дает надежду на построение корректной модели.
Проблема детерминации (какой метод куда положить) не кажется разрешенной по умолчанию, но она разрешима. В своем единственном разработанном «по Витгенштейну» приложении я опирался на интерфейс. Имея на фронт-энде около десяти экранов, я разбил логику на десять контекстов операций с базовым контекстом наверху иерархии.
Можно по-разному классифицировать операции, важен сам принцип: объектно-ориентированное программирование мы заменяем на операционно-ориентированное. Еще раз повторюсь: речь идет исключительно о бизнес-логике – мире программы. Использовать классы среды и порождать их наследников ничто не мешает.
Не имея широкой айтишной эрудиции, могу предположить, что подобные мысли уже посещали многих философов и программистов. Вероятно, они были даже представлены в форме текстов – независимо и многократно. Сам я с подобными изысканиями не встречался, и мне потребовалось два года, чтобы прийти самостоятельно к этому удачному – как мне кажется – сочетанию объектно-ориентированного и функционального программирования (так представляется внешне).
Описанным способом я реализовал бизнес-логику в своем последнем на сегодняшний день проекте. В результате полного рефакторинга (проект достался мне по наследству) код сократился на 70% и стал невероятно дружественным в отношении любых — даже весьма значительных — правок. Опыт вышел удачным: предлагаю попробовать.
Здесь и далее я буду рассматривать общекнижный пример с сотрудниками предприятия, писать будем на чем-то СИ-подобном. Наследовать класс Сотрудник (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% и стал невероятно дружественным в отношении любых — даже весьма значительных — правок. Опыт вышел удачным: предлагаю попробовать.
