(Перевод)
Последние несколько лет я провел в изучении и экспериментах со многими языками программирования. В частности, я начал использовать Scala как основной язык, стараюсь использовать функциональный стиль везде где это возможно. Меня также весьма заинтересовал Haskell (чистый функциональный язык) и Clojure (современный диалект Лиспа).
Таким образом, я постепенно отказываюсь от объектно-ориентированной парадигмы, несмотря на то, что использовал в основном её последние 17 лет моей профессиональной деятельности. У меня появляется чувство, что объекты это то, что мешает нам писать лапидарный, структурированный и повторно используемый код.
Когда я задумываюсь на эту тему, я понимаю что в моём
Обещания ООП
В начале девяностых объекты были новыми и интригующими. Приверженцы этой новой парадигмы обещали возможность создания повторно используемых классов и паттернов, и это звучало заманчиво. Возможность комбинировать эти классы в многократно используемые и настраиваемые бизнес компоненты казалась Святым Граалем индустрии ПО. Разработчики новых языков, таких как C++ и позднее Java, поддержали пропаганду ООП как способа делать классное ПО.
Бизнес компоненты нельзя использовать повторно
Вскоре стало понятно, что возможность создавать повторно используемые бизнес компоненты это заблуждение. Каждый бизнес отличается от всех остальных, даже в одной отрасли. Каждый похожий проект работает по слишком специфичной бизнес логике.
Единственный способ сделать повторно используемые бизнес компоненты на этом уровне — сделать их сверх-настраиваемыми путём добавления таких штук как движки правил и встраиваемые языки. Такую модель я бы не стал называть моделью компонента. Скорее моделью очередного экземпляра bloatware. Обещание о повторном использовании ушло, люди либо покупали огромные bloatware системы(неудачники), или разрабатывали свои специальные бизнес объекты в своих собственных проектах.
С паттернами не сделать структурированное ПО
Следующая вещь которую мы поняли — надежды на паттерны проектирования не оправдываются. Паттерны не приводят к хорошей структурированности. Напротив, они приводят к чрезмерному усложнению программ, трудностям в понимании кода и дороговизне сопровождения. Некоторые паттерны стали даже анти-паттернами (например, паттерн singleton приводит к невозможности создания модульного тестирования).
Лишь недавно программисты научились использовать паттерны осмысленно. Намного проще писать код так, как вы понимаете модель сами. А не пытаться абстрагировать его к какому-то обобщённому паттерну из книги Александреску.
Фреймворки классов и компонентов не очень эффективны для повторного использования
Ещё одно обещание парадигмы объектов — развитые фреймворки тесно связанных классов, используемые вместе друг с другом. Парадигма обещала значительно упростить построение приложений из повторно используемых компонент, скрывая абсолютно все технические сложности и тонкости реализации. Имею в виду такие штуки как EJB. Однако опыт показал, что эти фреймворки не работают. Они просто слишком громоздкие и мешают людям делать то, что они хотят сделать.
Такие тяжелые фреймворки быстро умирают и замещаются более легковесными библиотеками и наборами легковесных библиотек. Стало понятно, что легче строить программы из наборов не так тесно связанных классов, которые вы можете скомпоновать вместе в зависимости от потребностей. Вам действительно не нужна тесная интеграция разных классов.
Наследование приводит к хрупкому ПО
Возможность наследовать интерфейс и реализацию является одним из основных принципов ООП. Можно выделить общий код и поведение в базовый класс, при этом будущие абстракции могут использовать этот код и добавлять логику на его основе.
К сожалению, это не работает. Каждый подкласс немного отличается от своих родителей. Что ведёт к множеству изменений в дочернем классе. Или к попыткам сделать базовый класс более общим. В конечном итоге мы получаем хрупкий код. В котором маленькое изменение в базовом классе ведёт к поломке большинства, если не всех, порождённых классов.
Нарушение инкапсуляции
Другой базовый принцип ооп — возможность инкапсулировать состояние и предоставлять поведенческие методы для работы с состоянием. К сожалению, получилось так, что в большинстве случаев нам нужны данные объекта целиком, а не методы работы с ними.
Например, нелепо просить объект отрендерить себя в HTML. Знание о рендеринге HTML в этом случае размазывается по проекту и небольшое изменение в коде рендеринга влечёт за собой изменение множества классов. Правильнее будет передать объект в отдельный комонент рендеринга HTML. И этот компонент прежде всего извлечёт все интересующие его значения из объекта.
Появились даже антипаттерны, которые позволяют создавать объекты с инкапсулируемым состоянием без поведенческих методов (Java Beans, Data Transfer Objects и т.п.). Если мы делаем такие объекты, почему бы не использовать просто структуры вместо объектов?
Изменяемое состояние — причина проблем
Ещё одно кажущееся преимущество инкапсуляции — возможность изменять состояние объекта без влияния на компоненты, которые используют этот объект. Однако, любой разработчик большого объектно-ориентированного проекта может рассказать истории о переборе огромного объёма кода, чтобы найти место, в котором состояние объекта меняется на ошибочное, что приводит к сбою в программе в абсолютно другом месте (обычно в момент, когда вы выводите или сохраняете состояние объекта).
Чем больше мы проявляем интерес к неизменяемому состоянию и stateless сервисам, тем больше убеждаемся, что там этих проблем нет. Другим преимуществом неизменяемого состояния является простота создания параллельных систем для повышения эффективности использования современных многоядерных процессоров. Это намного легче и безошибочнее, чем пытаться работать с потоками, блокировками и потокобезопасными структурами данных.
Жизнь без объектов
Итак, можно ли отбросить свой 17 летний опыт и задуматься о жизни без объектов? Я пока не уверен, что дошёл до полного просветления на этот счёт. Но использование мультипарадигменного языка Scala позволяет мне обойти многие ограничения ООП.
Например, наличие в Scala такой фичи как mixin traits позволяет полностью отказаться от наследования реализации. Использование немутабельных значений и коллекций делает код более простым в понимании и отладке.
Функции как обобщённые абстракции и классы типов для расширения поведения позволяют писать хорошо структурированный и повторно используемый код. Принцип single responsibilities в этом случае выполняется идеально.
Фактически, я понял что стараюсь как можно больше использовать простые объекты в форме case classes для представления структур данных. Методов у таких классов мало, они предназначены только для облегчения работы с данными. Для группировки функций я использую mixin traits. Компоненты, которые я проектирую, просто собирают различные связанные по смыслу функции, которые преобразуют данные из одной формы в другую.
Возможно, я намного дальше ушёл от чистого ООП чем мне кажется. С уверенностью могу сказать, что теперь я пишу более компактный, более понятный и лучше структурированный код чем прежде.
(Примечание переводчика: в этой статье хотелось бы акцентировать внимание на отношение опытного ООП программиста к ООП. Я не знаком с языком Scala, а соответственно и со специфичными для Scala терминами.
Всё чаще я вижу ситуации, когда опытные программисты рассказывают о том, что разочаровались в ООП.
По поводу возможностей Scala. Видимо, они действительно покрывают весь спектр случаев, с которыми приходится иметь дело в ООП. Хотелось бы добавить, что возможности Erlang или Haskell делают это ничуть не хуже.
)
Автор: tranquil