| |||
![]()
|
![]() ![]() |
![]()
Code Reuse (повторное использование кода в ООП) Коллекция ссылок по разным языкам программирования оформилась в небольшую статью. Многие языки здесь не упомянуты, либо потому, что не имеют отношения к ООП (как Prolog), либо потому, что автор их не знает (Perl), либо по обеим причинам сразу (Haskell). За множество ценных замечаний по черновику огромное спасибо Олегу. Комментарии приветствуются. Все разработчики знают, что «Code reuse» — это Билет в Программистский Рай, Где Ничего Не Надо Писать Заново, и вообще ничего не надо писать, потому что всё уже есть. Любую программу в этом раю можно создать, совместив несколько готовых блоков. Все успешные менеджеры знают, как выгоден «Code reuse». Нахального программиста можно выгнать в три шеи или извести мизерным окладом, а написанный им код останется работать, поддерживаемый его более скромными коллегами. Налицо чёткая корпоративная этика и экономия средств. Повторное использование кода (1) не обязывает к ООП и (2) не всегда ему сопутствует.
Но сейчас речь идёт об ООП, а именно — о средствах компоновки и расширения классов (и не только) для создания новых классов. Композиция — это объединение объектов, которые общаются между собой с помощью только внешних интерфейсов (black box reuse). Друг для друга объекты являются «чёрными ящиками».Гибкость композиции объектов обеспечивает делегирование — передача ответственности за выполнение метода связанному объекту. Есть две разновидности делегирования:
Наследование (inheritance, white box reuse) — это формирование подкласса, который имеет доступ ко всем данным и методам родительского класса и может перегружать методы. О «чёрном ящике», как при композиции, речи нет. Наследование часто считают одним из основных принципов ООП (инкапсуляция-полиморфизм-наследование). Это так, но лишь в рамках языка Simula, созданного Кристеном Нигардом в 1960-х годах, идеи которого использовались позже в C++, Java и C#. Невозможно представить эти языки без наследования, но оно в них столь востребовано не от хорошей жизни: из-за синтаксических ограничений этих языков наследование с перегрузкой виртуальных методов широко используется для передачи вызовов, а в C++ и делегатов без наследования не сделать. Иными словами, наследование используется как вспомогательное средство для делегирования. На более богатой модели Алана Кея (объекты-сообщения), реализованной в Smalltalk в 1970-х годах, очевидно, что наследование - это удобный механизм для повторного использования кода, и ничего более. Инкапсуляция и полиморфизм в этой модели ООП предполагаются абсолютными, а наследование (и даже классы) не обязательны вовсе. Есть группа т.н. прототипных (prototype-based) ОО-языков, в которых нет классов. К этой группе относятся Self, Io, Lua и JavaScript (до выхода ECMAScript 4/JavaScript 2.0) Наследование, к тому же, (a) нарушает принцип инкапсуляции, т.к. родительский класс открывает свои данные подклассу, и (b) действует без гибкости, «раз и навсегда»: после создания объекта наследованного класса его базовый класс уже не изменить. В известной книге «Банды четырёх» даётся совет предпочитать композицию наследованию. Создатель Java Джеймс Гослинг также признался, что считает наследование не лучшей техникой. (Впрочем, второй из указанных пороков присущ также и всем приёмам, изложенным далее.) При одиночном наследовании (single inheritance) у класса может быть только один «родитель». Стандартная библиотека классов и вся среда разработки Smalltalk были написаны на Smalltalk с применением только одиночного наследования. Из этого можно сделать вывод, что одиночного наследования (вкупе с делегатами-замыканиями) в принципе достаточно для создания программных комплексов любого размера; ограниченные возможности по повторному использованию кода искупаются простотой объектной модели. Множественное наследование (multiple inheritance) является плодом естественного стремления «повторно использовать» не один класс, а сразу несколько. Этот вид наследования реализуют языки C++, CLOS (объектная надстройка над Common Lisp) и Python. На фоне очевидных преимуществ проступает ряд недостатков:
Параметризованные классы — это классы, определение которых содержит неспецифицированные классы. Последние задаются в качестве параметров при непосредственном использовании параметризованного класса. Этот подход называется «обобщённым программированием» (generic programming), и успешно применяется в тандеме с ООП, а именно в C++, D, и Scala. В C# с версии 2.0 и и в Java с версии 1.5 есть т.н. группы (generics), внешне похожие на параметризованные классы. Группы предназначены не для создания новых классов, и вообще не для «code reuse», а для усиления контроля типов. Стандартные коллекции в C# и Java хранят объекты базового класса В динамически типизированных ОО-языках обобщённое программирование теряет смысл: все классы в них и так параметризованы настолько, что никогда не требуют конкретизировать классы используемых объектов на стадии компиляции. Иными словами, параметризованные классы — это издержки контроля типов, целесобразность которого является предметом многолетней «holy war». Шаблоны C++ и D заслуживают отдельного рассмотрения тем, что дают компилятору возможность сразу выдавать эффективный машинный код. Преждевременная оптимизация (корень всех зол) в C++ дошла до того, что шаблоны являются полным по Тьюрингу «языком в языке», на котором можно написать любую программу, например процедуру вычисления чисел Фибоначчи или игру «Жизнь». В языке D шаблоны также являются отдельным языком, но совпадают с D по синтаксису. Скомпилированный программный код не содержит шаблонов. Обычные конструкции C++ вроде Безотносительно проблем статически типизированных языков, композиция и наследование имеют один принципиальный изъян, состоящий в использовании класса как единицы «code reuse», тогда как класс является в первую очередь генератором объектов. Объекты должны быть большими, а единицы повторно используемого кода — маленькими; чтобы разрешить это противоречие, имеет смысл завести отдельные сущности, предназначенные сугубо для повторного использования. Примесь (mixin) — как раз такая сущность. Примесь представляет собой набор данных и методов, который можно «подмешать» к какому-либо классу. Сама по себе примесь не может служить генератором объектов; в методах примеси могут использоваться данные и другие методы, отсутствующие в ней (соответственно, они должны присутствовать в классе, к которому добавляется примесь). Использование примесей, как и наследование, нарушает инкапсуляцию. По аналогии с «абстрактным суперклассом», от которого необходимо наследоваться, примесь можно считать абстрактным подклассом, который необходимо отнаследовать от какого-либо суперкласса. Такой подход позволяет удобно совместить новую концепцию с классическим наследованием. Примеси реализованы в Ruby и Scala. В Python и C++ примеси доступны как частный случай множественного наследования. В CLOS примеси есть, но их отличие от классов чисто условное и состоит в вызове Разумеется, в класс можно добавлять и более одной примеси. Что касается «проблемы ромба» при множественном «подмешивании»: в Ruby, если «подмешиваемые» модули включают другие модули, ромбовидная диаграмма примесей может возникнуть, с последующим конфликтом состояния. В Python и C++ здесь та же ситуация, что и с множественным наследованием, а в Scala ромбовидные иерархии примесей попросту запрещены. В CLOS в каждом классе древовидная иерархия его суперклассов (и примесей) принудительно раскладывается в линию. Это устраняет «проблему ромба», но создаёт другую, не менее серьёзную: когда в класс добавляется несколько примесей, они идут не все сразу, а по очереди. Каждая следующая примесь наследуется от совокупности класса и предыдущих примесей. Это означает, что:
Штрих (trait) — это абстрактный подкласс без переменных, то есть примесь, не имеющая состояния, или, по словам авторов, «единица поведения». Штрих имеет доступ к методам класса, но не к его данным. Как и примеси, штрихи могут наследоваться. Запрет на доступ к данным класса восстанавливает инкапсуляцию, а запрет на состояние убирает разом все проблемы с конфликтами имён переменных и с конфликтами состояний при ромбовидном наследовании. Конфликты имён методов в штрихах обязаны разрешаться явно с помощью (a) переименования методов в наследованном классе или штрихе и (b) явного переопределения конфликтующего метода в наследованном классе или штрихе. Порядок наследования штрихов не имеет значения. Штрихи были впервые реализованы в диалекте Smalltalk под названием Squeak. Они присутствуют в Scala, а в C++ и Python их можно, как и примеси, реализовать через множественное наследование. Вероятно, штрихи появятся как особый элемент языка и в Python 3000. В завершение — небольшая сводная таблица.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
![]() |
![]() |