учебники, программирование, основы, введение в,

 

От ФП к ООП

Общее представление о декомпозиции программ
Сборка программы из автономно развивающихся компонентов опирается на формулировку достигаемой ими цели, понимание которой гарантирует не только корректность полученного результата, но и рациональность его использования. Формулировать цели частей программы — процесс нетривиальный. В его основе лежат весьма различные подходы к классификации понятий.
ООП объединяет в рамках единой методики организации программ классификацию на базе таких понятий как класс объектов, структура данных и тип значений. Тип значений обычно отражает спектр низкоуровневых реализационных средств, учет которых обеспечивает эффективность кода программы, получаемого при компиляции. Структура данных обеспечивает конструктивность построений, гарантирует доступ к частям, из которых выстроено данное любой сложности. Класс объектов характеризуется понятным контекстом, в котором предполагается их корректная обработка. Обычно контекст содержит определения, структуру объектов и их свойства.
Текст программы одновременно представляет и динамику управления процессами, и схему информационных потоков, порождаемых при исполнении программы. Кроме того, последовательность написания программы и ее модификации по мере уточнения решаемой задачи могут соответствовать логике, существенно отличающейся от процесса выбора системных и реализационных решений. Функциональное программирование скрывает сложность таких деталей управления процессами путем сведения его к общим функциям, преобразующим любые аргументы в определенные результаты. Модификации осуществляются в процессе изменения состава ветвей непосредственно в определении каждой функции.
ООП структурирует множество частных методов, используемых в программе, в соответствии с иерархией классов объектов, обрабатываемых этими методами, реализуемыми с помощью функций и процедур, в предположении, что определяемые в программе построения могут локально видоизменяться при сохранении основных общих схем информационной обработки. Это позволяет выполнять модификации объявлением новых подклассов и дописыванием методов обработки объектов отдельных классов без радикальных изменений в ранее отлаженном тексте программы.
При анализе задач, решаемых в терминах объектов, некоторая деятельность описывается так, что постепенно продумывается все, что можно делать с объектом данного класса. Потом в программе достаточно лишь упоминать методы обработки объекта. Если методов много, то они структурированы по иерархии классов, что позволяет автоматизировать выбор конкретного метода. На каждом уровне иерархии можно немного варьировать набор методов и структуру объектов. Таким образом, описание программы декомпозируется на интерфейс и реализацию, причем интерфейс скрывает сложность реализации так, что можно обозревать лишь необходимый для использования минимум средств работы с объектом.
Типичная гипотеза при программировании работы с объектами:
Объект не изменен, если на него не было воздействий из программы.


Но реальность зачастую требует понимания и учета более сложных обстоятельств, что может существенно продлить время жизни программы или ее компонентов. В таком случае удобно предоставлять объектам большую свободу, сближающую их с понятием субъекта, описание которого содержит все, что он может делать. Программа может давать ему команды-сообщения и получать ответы-результаты.
Связь методов с классами объектов позволяет вводить одноименные методы над разными классами объектов (полиморфизм), что упрощает представление логики управления: на уровне текста программы можно не выражать распознавание принадлежности объекта классу, это сделает система программирования. Таким образом обычно реализовано сложение, одинаково изображаемое для чисел, строк, векторов, множеств и т.п. Фактически субъектом является суперкласс, объединяющий классы объектов, обрабатываемые одноименными методами, т.е. функциями одного семейства. Так, при организации сложения можно считать, что существует суперкласс «слагаемые», которое умеют складываться с другими слагаемыми. При этом они используют семейство функций, реализующих сложение. В зависимости от полноты семейства результат может быть получен или не получен. Семейство легко пополняется добавлением одноименных функций с новыми комбинациями типов параметров.
Дальнейшее развитие подходов к декомпозиции программ связано с выделением отдельных проекций и шагов при решении сложных задач. Понятие проекции связано с различием точек зрения, позволяющим описывать решение всей задачи, но отражать в описании только видимые детали. По мере изменения точек зрения могут проступать новые детали, до тех пор, пока дальнейшая детализация не утрачивает смысл, т.е. улучшение трудно заметить или цена его слишком высока. Так, представление символьной информации в Лиспе выделено в отдельную проекцию, независимую от распределения памяти, вычисление значений четко отделено от компиляции программ, понятие связывания имен с их определениями и свойствами не зависит от выбора механизмов реализации контекстов исполнения конструкций программы.
Понятие шага обычно связывается с процессом раскрутки программ, оправданным в тех случаях, когда целостное решение задачи не может гарантировать получение приемлемого результата в нужный срок — это влечет за собой непредсказуемо большие трудозатраты.
Удобный подход к организации программ «отдельная работа отдельно программируется и отдельно выполняется» успешно показал себя при развитии операционной системы UNIX как работоспособный принцип декомпозиции программ. Но существуют задачи, например реализация систем программирования, в которых прямое следование такому принципу может противоречить требованиям к производительности. Возможен компромисс «отдельная работа программируется отдельно, а выполняется взаимосвязано с другими работами», что требует совмещения декомпозиции программ с методами сборки — комплексации или интеграции программ из компонентов. Рассматривая комплексацию как еще одну «отдельную» работу, описываемую, например, в терминах управления процессами, можно констатировать, что эта работа больше сказывается на требованиях к уровню квалификации программиста, чем на объеме программирования. При достаточно объективной типизации данных и процессов, возникающих при декомпозиции и сборке программ определенного класса, строят библиотеки типовых компонентов и разрабатывают компонентные технологии разработки программных продуктов - Corba, COM/DCOM, UML, .Net. Одна из проблем применения таких компонентов - их обширность.
При реализации экспериментальных языков и систем программирования цель применения раскрутки — минимизация трудозатрат, основанная на учете формальной избыточности средств языков программирования. Можно выделить небольшое ядро, на основе которого методично программируется все остальное. Каждый шаг реализации по схеме раскрутки должен обеспечивать:
  • уменьшение трудоемкости последующих шагов,
  • отладку прототипов сложных компонентов,
  • подготовку демонстрационного материала.

Выбор конретных шагов можно соотнести с декомпозицией определения языка программирования на синтаксические и семантические, функциональные и машинно-ориентированные, языково-ориентированные и системные аспекты. При такой декомпозиции можно на первых шагах как бы «снять» синтаксическое и семантическое разнообразие языка, как имеющее чисто технический характер. Именно в этом смысл выделения элементарного Лиспа. Такая методика может быть успешна при освоении любого класса задач, информацию о котором можно представить в виде частично формализуемых текстовых и графовых форм.
Дальнейшие шаги раскрутки можно упорядочить по актуальности реализации компонентов, обеспечивающих положительную оценку системы пользователем. Это позволяет развить представление о принципах декомпозиции программ более созвучно ООП: «отдельная работа обнаруживается независимо от остальных работ». Наиболее устойчивая и значимая классификация работ по реализации системы программирования может быть установлена как обеспечение механизмов надежного функционирования информационных систем — «отдельная работа — это отдельное средство повышения надежности программирования». Ряд таких средств можно выделить в любом языке программирования: вычисления, статическое и динамическое управление процессами, логика выбора хода обработки информации, дисциплина именования памяти и доступа к расположенным в ней данным, правила укрупненных воздействий на блоки данных и иерархию процессов, диагностика и обработка событий — перечень открытый.

ООП на Лиспе
При переходе от обычного стандартного программирования с ООП связывают радикальное изменение способа организации программ. Это изменение произошло под давлением роста мощности оборудования. ООП взламывает традиционное программирование по многим направлениям. Вместо создания отдельной программы, оперирующей массой данных, приходится разбираться с данными, которые сами обладают поведением, а программа сводится к простому взаимодействию новой категории данных — «объекты».
Чтобы сравнить дистанцию с функциональным программированием, рассмотрим самодельный встроенный в Лисп объектно-ориентированный язык (ОО-язык), обеспечивающий основы ООП. Встраивание ОО-языка — идеальный пример, показывающий характерное применение функционального программирования, при котором типичные понятия ООП отображаются в фундаментальные абстракции.
При организации наследования следует пояснить разницу между моделями обобщенных функций и обмена сообщениями.

  • объекты обладают свойствами,
  • посылают сообщения,
  • наследуют свойства и методы от предков.

Рассмотрим программу вычисления площадей ряда геометрических фигур, таких как круги, прямоугольники, звезды и пр. Ее запись в ОО-стиле поначалу может выглядеть непринципиально отличающейся от обычной программы.
Но допустим, понадобилось ввести новый класс — раскрашенные круги. Если методы раскраски произвольных фигур уже существовали, то можно унаследовать две особенности:

  1. от круга наследуем понятие «радиус», а от раскрашенных — «цвет»;
  2. можно не вводить метод «раскраска круга».

Практически ООП — это организация программ в терминах методов, классов, экземпляров и наследования. Почему стоит писать программы таким способом? Основной выигрыш — программы легче изменять. Если мы хотим изменить способ манипулирования каким-либо объектом некоторого класса, то мы изменяем лишь метод этого класса. Если мы хотим сделать нечто подобное объекту, но отличающееся в отдельных чертах, мы можем создать подкласс объектов, а уже для подкласса поменять его отдельные черты. Если программа написана тщательно, то можно добиться того, чтобы все такие модификации производились даже без просмотра ранее написанного исходного текста программы.
В Лисп есть разные способы размещать коллекции свойств. Один из них — представлять объекты как хэш-таблицы и размещать свойства как входы в нее. В приведен блестящий пример реализации ООП на базе хэш-таблиц. Тогда отдельное свойство можно получать из таблицы в форме:
(gethash 'color obj)   
Поскольку функции являются данными объекта, мы можем размещать их точно так же, как и свойства. Это значит, что мы должны завести еще методы, позволяющие вызывать данный метод объекта как исполнение свойства данного объекта.
(funcall (gethash 'move obj) obj 10)   
Мы можем определить под эту идею синтаксис как в Smalltalk
(defun tell (obj message &rest args)
(apply (gethash messmage obj) obj args))   
что позволяет сказать объекту, чтобы он переместился на 10 шагов, в форме:
(tell obj 'move 10)   
Фактически успех наследования обеспечивает единственная особенность Лиспа: все это работает благодаря реализации рекурсивной версии GETHASH. Таким образом, определение данной функции нам сразу даст все три основные черты ООП.
Посмотрим, во что это обойдется в исходном примере.
Мы должны создать два объекта, один — потомок другого.
В объект «круги» мы помещаем методы для всех кругов. Для начала это функция одного аргумента — объекта, посылающего сообщение:
(setf (gethash 'area circle-class)
#' (lambda (x)
(* pi (expt (rget ' radius x) 2))))    
Теперь можно спрашивать о площади круга, она будет вычисляться согласно методу, определенному для класса. Мы используем rget при чтении свойства и tell — при вызове метода.
(rget 'radius our-cicle)
(tell our-circle 'area)   
Прежде чем улучшать эту программу, надо проверить, что же получилось. Легкость результата — это трюк, но не программистский трюк, а концептуальный. Не будем забывать о том, что Лисп по своей природе уже был ОО-языком, или даже чем-то более общим изначально. Все что нам понадобилось — это создать новый фасад для уже существующих в Лиспе абстракций.

Множественное наследование

До сих пор речь шла о простом наследовании — объект имел только одного предка. Но можно получать и множественное наследование построением списка свойств предков со слегка измененной rget. При простом наследовании, когда нам надо выбрать некоторое свойство объекта, мы сразу рекурсивно ищем его предшественников. Если собственно объект не содержит искомую информацию, мы обозреваем его предка и т.д. При множественном наследовании мы делаем примерно то же самое, но работа усложняется тем, что предшественники объекта могут образовывать граф вместо дерева.
При реализации такой идеи не следует проверять объект ранее его последователей (a b c d):

        (d)
        / \   
    (b)    (c) 
        \ /
        (a)       

Если это сделать прямым сцеплением списков, то результат слишком неэффективен.

Определяемые объекты

Первое улучшение — функция создания объектов. Такая функция может быть не видна пользователю. Если мы создадим данную функцию, можно будет строить объекты и объявлять его предков за один шаг. Мы получим список предшествования объектов в процессе их создания вместо дорогостоящей их реорганизации при поиске свойства. Стратегия — поддерживать список всех существующих объектов, и в нем помечать списки изменяемых предшественников. Это тоже обременительно, но без потери гибкости основная нагрузка переносится в нечасто повторяющуюся область.

Функциональный синтаксис

Другое улучшение — синтаксис сообщений можно варьировать без оглядки на традицию префиксных записей. Определение методов может достичь предельной гибкости благодаря возможности генерировать определения функциональных объектов с помощью DEFMACRO.

Экземпляры

До сих пор не требовалось особо различать классы, объекты и их вхождения. Логически удобно все обрабатывать по общей схеме, но весьма непроизводительно всякий раз просматривать сотни предшественников, если известно, что достаточно менее десятка. Такую оптимизацию выполняет механизм экземпляров. Экземпляр имеет одного предка — его класс. Его обработка не требует списка предшественников. Это не ведет к потере гибкости, т.к. при необходимости можно переопределить состояние объекта и как бы конвертировать экземпляр в класс.

Векторная реализация

Реализация ООП с помощью хэш-таблиц обладает слегка парадоксальной окраской: гибкость у нее больше, чем надо, и за большую цену, чем можно позволить. Уравновесить это может подобная реализация на базе простых векторов. Этот переход показывает, как функциональное программирование дает новое качество «на лету». В опорной реализации фактически не было реализационного разделения объектов на экземпляры и классы. Экземпляр — это был просто класс с одним-единственным предком. При переходе к векторной реализации разделение на классы и экземпляры становится реальным. В ней становится невозможным превращать экземпляры в классы простым изменением свойства.

Еще одна реализация

Более прозрачная модель ООП получается на базе обычных списков свойств, заодно иллюстрирующая глубинное родство ФП и ООП:
Остается лишь слегка подкорректировать определение Лисп-интерпретатора, заодно используя необязательные параметры, осовобождающие от простейших вспомогательных определений, например от обязательного вхождения накопителей типа ассоциативного списка.

Средства ООП в CLOS на базе стандарта Clisp

Показанный пример работает по первому аргументу (выбор подходящего метода рассчитан на то, что достаточно разобраться с одним аргументом), CLOS делает это на всех аргументах, причем с рядом вспомогательных средств, обеспечивающих гибкий перебор методов и анализ классов объектов.

Классы и экземпляры объектов

    (defclass ob () (f1 f2 ...))     

Это означает, что каждое вхождение объекта будет иметь поля-слоты f1 f2 ... (Слот — это поле записи или списка свойств.) Чтобы сделать представителя класса, мы вызываем общую функцию:

    (setf с (make-instance 'ob))    

Чтобы задать значение поля, используем специальную функцию:

    (setf (slot-value c) 1223)    

До этого значения полей были не определены.

Свойства слотов

Простейшее определение слота — это его имя. Но в общем случае слот может содержать список свойств. Внешне свойства слота специфицируются как ключевые параметры функции. Это позволяет задавать начальные значения. Можно объявить слот совместно используемым.

    :allocation :class    

Изменение такого слота будет доступно всем экземплярам объектов класса. Можно задать тип элементов, заполняющих слот, и сопроводить их строками, выполняющими роль документации.

Суперкласс

Нет необходимости все новые слоты создавать в каждом классе.
Пример: ОО-определение Лисп-компилятора.
CLOS, естественно, использует модель обобщенных функций, но мы написали независимую модель, используя более старые представления, тем самым показав, что концептуально ООП — это не более чем перефразировка идей Лиспа. ООП — это одна из вещей, которую Лисп изначально умеет делать. Для функционального стиля программирования в переходе к ООП нет ничего неожиданного. Это просто небольшая конкретизация механизмов перебора ветвей функциональных объектов.
Более интересный вопрос, что же нам еще может дать функциональный стиль и лисповская традиция реализации систем программирования?
Ответу на этот вопрос посвящены три следующие лекции.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика