учебники, программирование, основы, введение в,
агентство эскорт услуг. | Проститутки метро Цветной бульвар

 

Принципы проектирования класса

Побочные эффекты в функциях
Первый вопрос, исследованием которого мы займемся, оказывает глубокое влияние на стиль нашего проектирования. Законно ли для функций - подпрограмм, возвращающих результат, - иметь еще и побочный эффект, то есть изменять нечто в их окружении?
Наш ответ - нет. Но почему? Обоснование требует понимания роли побочных эффектов, осознания различий между "хорошим" и "плохим" побочным эффектом. Рассмотрим этот вопрос в свете наших знаний о классах - их происхождения от АТД, понятия абстрактной функции и роли инварианта класса.
Команды и запросы
Напомним используемую терминологию. Компоненты, характеризующие класс разделяются на команды и запросы. Команды модифицируют объекты, а запросы возвращают информацию о них. Команды реализуются процедурами, а запрос может быть реализован либо атрибутом - тогда в момент запроса возвращается значение соответствующего поля экземпляра класса, либо функцией - тогда происходит вычисление значения по алгоритму, заданному функцией. Процедуры и функции называются подпрограммами.
В определении запроса не сказано, могут ли изменяться объекты в момент запроса. Для команд ответ очевиден - да, поскольку в этом и состоит их назначение. Для запросов вопрос имеет смысл только в случае их реализации функциями, поскольку доступ к атрибуту ничего не меняет. Изменение объектов, выполняемое функцией, называется ее побочным эффектом (side effect). Функция с побочным эффектом помимо основной роли - возвращения ответа на запрос, меняя объект, играет одновременно и дополнительную роль, которая часто является фактически основной. Но следует ли допускать побочные эффекты?
Формы побочного эффекта
Определим, какие конструкции могут приводить к побочным эффектам. Операциями, изменяющими объекты, являются: присваивание a := b, попытка присваивания a = b, инструкция создания create a. Если цель a является атрибутом, то выполнение операции присвоит новое значение его полю для объекта, соответствующего цели текущего вызова подпрограммы.
Нас будут интересовать только такие присваивания, в которых a является атрибутом; если же a - это локальная сущность, то его значение используется только в момент выполнения подпрограммы и не имеет постоянного эффекта, если a - это Result, присваивание вычисляет результат функции, но не действует на объекты.
Заметим, что, применяя принципы скрытия информации, мы при проектировании ОО-нотации тщательно избегали любых косвенных форм модификации объектов. В частности, синтаксис исключает присваивания в форме obj.attr := b, чья цель должна быть достигнута через вызов obj.set_attr (b), где процедура set_attr (x:...) выполняет присваивание атрибуту attr := x (см.курса "Основы объектно-ориентированного программирования").
Присваивание атрибуту, ставшее причиной побочного эффекта, может находиться в самой функции или встроено глубже - в другой подпрограмме, вызываемой функцией. Вот полное определение:


Определение: конкретный побочный эффект
Функция производит конкретный побочный эффект, если ее тело содержит:
  • присваивание, попытку присваивания или инструкцию создания, чьей целью является атрибут;
  • вызов процедуры.

Термин "конкретный" будет пояснен ниже. В последующем определении мы второе предложение сформулируем как "вызов подпрограммы, создающей (рекурсивно) конкретный побочный эффект". Определение побочного эффекта будет расширено и не будет, как теперь, относиться только к функциям. Но выше приведенное определение на практике предпочтительнее, хотя по разным причинам его можно считать либо слишком строгим, либо слишком слабым:

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

Преимущество этого соглашения в том, что для определения статуса побочного эффекта достаточно анализировать тело только самой функции. Достаточно тривиально, имея анализатор языка, встроить простой инструментарий, который для каждой функции независимо определял бы, обладает ли она конкретным побочным эффектом в соответствии с данным определением
Ссылочная прозрачность
Почему нас волнуют побочные эффекты функций? Ведь в природе ПО заложено изменение вещей в процессе выполнения.
Если позволить функциям, подобно командам, изменять объекты, то мы потеряем многие из их простых математических свойств. Как отмечалось при обсуждении АТД (см.курса "Основы объектно-ориентированного программирования"), математики знают, что их операции над объектами не меняют объектов (Вычисление |21/2| не меняет числа 2). Эта неизменяемость является основным отличием мира математики и мира компьютерных вычислений.


Некоторые подходы в программировании стремятся к этой неизменяемости - Lisp в его так называемой "чистой" форме, языки функционального программирования, например язык FP, предложенный Бэкусом, другие аппликативные языки. Но в практической разработке ПО изменения объектов являются основой вычислений.

Неизменяемость объектов имеет важное практическое следствие, известное как ссылочная прозрачность (referential transparency) и определяемое следующим образом:


Определение: ссылочная прозрачность
Выражение e является ссылочно-прозрачным, если возможно заменить любое его подвыражение эквивалентным значением без изменения значения e.

Если x имеет значение 3, мы можем использовать x вместо 3, и наоборот, в любом ссылочно-прозрачном выражении. (Только академики Лапуты из "Путешествий Гулливера" Свифта игнорировали ссылочную прозрачность, - они всегда носили с собой вещи, предъявляя их при каждом упоминании.) Ссылочную прозрачность называют также "заменой равного равным".
При наличии функций с побочным эффектом ссылочная прозрачность исчезает. Предположим, что класс содержит атрибут и функцию:
attr: INTEGER
sneaky: INTEGER is do attr := attr + 1 end
Значение sneaky при ее вызове всегда 0; но 0 и sneaky не являются взаимозаменяемыми, например:
attr := 0; if attr /= 0 then print ("Нечто странное!") end
ничего не будет печатать, но напечатает "Нечто странное!" при замене 0 на sneaky.
Поддержка ссылочной прозрачности в выражениях важна, поскольку позволяет строить выводы на основе программного текста. Одна из центральных проблем конструирования ПО четко сформулирована Э. Дейкстрой ([Dijkstra 1968]). Она состоит в сложности динамического поведения (миллионы различных вычислений даже для простых программ), порождаемого статическим текстом программы. Поэтому крайне важно сохранить проверенную форму вывода, обеспечиваемую математикой. Потеря ссылочной прозрачности означает и потерю основных свойств, которые настолько укоренились в нашем сознании и практике, что мы и не осознаем этого. Например, n + n не эквивалентно 2* n, если n задано функцией, подобной sneaky:
n: INTEGER is do attr := attr + 1; Result := attr end
Если attr инициализировать нулем, то 2* n возвратит 2, в то время как n + n вернет 3.
Функции без побочных эффектов можно рассматривать в программных текстах как термы в обычном математическом смысле. Мы будем поддерживать четкое различение команд, изменяющих объекты, но не возвращающих результатов, и запросов, обеспечивающих информацией об объектах, но не изменяющих их.
Это же правило неформально можно выразить так: "задание вопроса не меняет ответ".
Объекты как машины
Следующий принцип выражает этот запрет в более точной форме:


Принцип: Разделение Команд и Запросов
Функции не должны обладать абстрактным побочным эффектом.

Заметьте, к этому моменту мы определили только понятие конкретного побочного эффекта, но пока можно игнорировать разницу между абстрактным и конкретным побочными эффектами.
Только командам (процедурам) будет разрешено обладать побочным эффектом. Фактически мы не только допускаем, но и ожидаем изменения объектов командами, что и отличает императивный подход от аппликативного, полностью свободного от побочных эффектов.
Из этого обсуждения следует, что объекты можно рассматривать как машины с ненаблюдаемым внутренним состоянием и двумя видами кнопок - командными, изображенными на рисунке в виде прямоугольников, и кнопками запросов, отображаемыми кружками. Метафору "машины", как обычно, следует принимать с осторожностью.
При нажатии командной кнопки машина изменяет состояние, она начинает гудеть, щелкать и работает, пока не придет в новое стабильное состояние. Увидеть состояние (открыть машину) невозможно, но можно нажать кнопку с запросом. Состояние при этом не изменится, но в ответ появится сообщение, показанное в окне дисплея в верхней части нашей машины. Для запросов булевского типа предусмотрены две специальные кнопки над окном сообщения: одна из них загорается, когда запрос имеет значение true, другая - false. Многократно нажимая кнопки запросов, мы всегда будем получать одинаковые ответы, если в промежутке не нажимать командные кнопки (задание вопроса не меняет ответ).
Команды, как и запросы, могут иметь аргументы. В нашей машине для их ввода предназначен слот, показанный слева вверху.
Наш рисунок основан на примере объекта list, интерфейс которого описан в предыдущих лекциях и будет еще обсуждаться подробнее в данной лекции. Команды включают start (курсор передвигается к первому элементу), forth (продвижение курсора к следующей позиции), search (передвижение курсора к следующему вхождению элемента, введенного в верхний левый слот). Запросы включают item (показ на дисплее панели значения элемента в позиции курсора), index (показ текущей позиции курсора). Заметьте разницу между понятием "курсора", связанного с внутренним состоянием и, следовательно, напрямую не наблюдаемым, и понятиями item или index, более абстрактными, задающими официально экспортируемую информацию о состоянии.
Функции, создающие объекты
Следует ли рассматривать создание объекта как побочный эффект? Ответ - да, если целью создания является атрибут a, то инструкция create a изменяет значение поля объекта. Ответ - нет, если целью является локальная сущность подпрограммы. Но что если целью является результат самой функции - create Result или в общей форме create Result.make (...)? Такая инструкция не должна рассматриваться как побочный эффект, она не меняет объектов и не нарушает ссылочной прозрачности.
С позиций математика можно полагать, что все интересующие нас объекты в прошлом, настоящем и будущем уже описаны в Великой Книге Объектов и инструкция создания просто получает один их этих готовых объектов, ничего не меняя. Так что вполне законно и допустимо, чтобы функция создавала, инициализировала и возвращала такие объекты.


Эти же рассуждения применимы и для второй формы создания объектов - процедуры make, которая тоже не создает побочного эффекта, а возвращает уже созданный объект.

Чистый стиль для интерфейса класса
Из принципа Разделения Команд и Запросов следует стиль проектирования, вырабатывающий простой, понятный при чтении программный текст, способствующий надежности, расширяемости и повторному использованию.
Как вы могли заметить, этот стиль отличается от доминирующей сегодня практики, в частности от стиля программирования на языке C, предрасположенного к побочным эффектам. Игнорирование разницы между действием и значением - не просто свойство общего C-стиля (иногда кажется, что C-программисты не в силах противостоять искушению, получая значение, что-нибудь не изменить при этом). Все это глубоко встроено в язык, в такие его конструкции, как x++, означающую возвращение значения x, а затем его увеличение на 1; нимало не смущающую конструкцию ++x, уменьшающую x до возвращения значения; Эти конструкции сокращают несколько нажатий клавиш: y = x++ эквивалентно y = x; x := x+1. Целая цивилизация фактически построена на побочном эффекте.
Было бы глупо полагать бездумным стиль побочных эффектов. Его широкое распространение говорит о том, что многие находят его удобным, чем частично объясняется успех языка C и его потомков. Но то, что было привлекательным в прошлом веке, когда популяция программистов возрастала каждые несколько лет, когда важнее было сделать работу, не задумываясь о ее долговременном качестве, - не может подходить инженерии программ двадцать первого столетия. Мы хотим, чтобы ПО совершенствовалось вместе с нами, чтобы оно было понятным, управляемым, повторно используемым, и ему можно было бы доверять. Принцип Разделения Команд и Запросов является одним из требуемых условий достижения этих целей.
Строгое разделение команд и запросов при запрете побочных эффектов в функциях особенно важно при построении больших систем, где ключом успеха является сохранение полного контроля над каждым межмодульным взаимодействием.
Если вы пользовались противоположным стилем, то на первых порах ограничение может показаться довольно строгим. Но, получив практику, я думаю, вы быстро осознаете его преимущества.
В предыдущих лекциях этот принцип Разделения применялся повсюду. Вспомните, в наших примерах интерфейс для всех стеков включал процедуру remove, описывающую операцию выталкивания (удаление элемента из вершины стека), и функцию item, возвращающую элемент вершины. Первая является командой, вторая - запросом. При других подходах обычно вводят подпрограмму (функцию) pop, удаляющую элемент из стека и возвращающую его в качестве результата. Этот пример, надеюсь, ясно показывает выигрыш в ясности и простоте, получаемый при четком разделении двух аспектов.
Другие следствия принципа могут показаться более тревожными. При чтении ввода многие пользуются функциями, подобными getint, - имя взято из C, но ее эквиваленты имеются во многих языках. Эта функция читает очередной элемент из входного потока и возвращает его значение, очевидно, она обладает побочным эффектом:

  • если дважды вызвать getint (), то будут получены два разных ответа;
  • вызовы getint () + getint () и 2 * getint () дают разные результаты (если сверхусердный "оптимизирующий" компилятор посчитает первое выражение эквивалентным второму, то вы пошлете его автору разгневанный отчет об ошибке, и будете правы).

Другими словами, мы потеряли преимущества ссылочной прозрачности - рассмотрение программных функций как их математических аналогов с кристально ясным взглядом на то, как можно строить выражения из функций и что означают эти выражения.
Принцип Разделения возвращает ссылочную прозрачность. Это означает, что мы будем отделять процедуру, передвигающую курсор к следующему элементу, и запрос, возвращающий значение элемента, на который указывает курсор. Пусть input имеет тип FILE; инструкция чтения очередного целого из файла input будет выглядеть примерно так:
input.advance
n := input.last_integer
Вызвав last_integer десять раз подряд, в отличие от getint, вы десять раз получите один и тот же результат. Вначале это может показаться непривычным, но, вкусив простоту и ясность такого подхода, вам уже не захочется возвращаться к побочному эффекту.


В этом примере, как и в случае x++, традиционная форма явно выигрывает у ОО-формы, если считать, что целью является уменьшение числа нажатий клавиш. Объектная технология вообще не обязательно является оптимальной на микроуровне (игра, в которой выигрывают языки типа APL или современные языки сценариев типа PERL). Выигрыш достигается на уровне глобальной структуры за счет повторного использования, за счет таких механизмов, как универсальность (параметризованные классы), за счет автоматической сборки мусора, благодаря утверждениям. Все это позволяет уменьшить общий размер текста системы намного больше, чем уменьшение числа символов в отдельной строчке. Мудрость локальной экономии зачастую оборачивается глобальной глупостью.

Генераторы псевдослучайных чисел: упражнение
В защиту функций с побочным эффектом приводят пример генератора псевдослучайных чисел, возвращающего при каждом вызове случайное число из последовательности, обладающей определенными статистическими свойствами. Последовательность инициализируется вызовом в форме:
random_seed (seed)
Здесь seed задается клиентом, что позволяет при необходимости получать одну и ту же последовательность чисел. Каждое очередное число последовательности возвращается при вызове функции:
xx := next_random ()
Но и здесь нет причин делать исключение и не ввести дихотомию команда/запрос. Забудем о том, что мы видели выше и начнем все с чистого листа. Как описать генерирование случайных чисел в ОО-контексте?
Как всегда, в объектной технологии зададимся вопросом - зачастую первым и единственным:
Что является абстракцией данных?
Соответствующей абстракцией здесь не является "генерирование случайного числа" или "генератор случайных чисел" - обе они функциональны по своей природе, фокусируясь на том, что делает система, а не на том, кто это делает.
Рассуждая дальше, рассмотрим в качестве кандидата понятие "случайное число", но и оно все же не является правильным ответом. Вспомним, что абстракция данных должна сопровождаться командами и запросами, довольно трудно придумать, что можно делать с одним случайным числом.


Понятие "случайное число" приводит к тупику. При изучении общих правил выявления классов уже говорилось, что ключевой шаг состоит в отсеве кандидатов. И опять-таки мы видим, что не все многообещающие существительные документа требований ведут к нужным классам. Можно не сомневаться, что данный термин обязательно встретится в любом документе, описывающем рассматриваемую проблему.

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

  • команды: make - инициализация некоторым начальным значением seed; forth - передвинуть курсор к следующему элементу последовательности;
  • запросы: item - возвращает элемент в позиции курсора.

Для получения новой последовательности rand клиенты будут использовать create rand.make (seed), для получения следующего значения - rand.forth, для получения текущего значения - xx := rand.item.
Как видите, нет ничего специфического в интерфейсе последовательности случайных чисел за исключением аргумента seed в процедуре создания. Добавив процедуру start, устанавливающую курсор на первом элементе (которую процедура make может вызывать при создании последовательности), мы получаем каркас отложенного класса COUNTABLE_SEQUENCE, описывающего произвольную бесконечную последовательность. На его основе можно построить, например, последовательность простых чисел, определив класс PRIMES - наследника COUNTABLE_SEQUENCE, чьи последовательные элементы являются простыми числами. Другой пример - последовательность чисел Фибоначчи.


Эти примеры противоречат часто встречающемуся заблуждению, что на компьютерах нельзя представлять бесконечные структуры. АТД дает ключ к их построению - структура полностью определяется аппликативными операциями, число которых конечно (здесь их три - start, forth, item) плюс любые дополнительные компоненты, добавляемые при желании. Конечно, любое выполнение будет всегда создавать только конечное число элементов этой бесконечной структуры.

Класс COUNTABLE_SEQUENCE и его потомки, такие как PRIMES, являются частью универсальной иерархии ([M 1994]) информатики.
Абстрактное состояние, конкретное состояние
Из дискуссии о ссылочной прозрачности, казалось бы, следует желательность запрета конкретного побочного эффекта у функций. Такое правило имело то преимущество, что его можно было бы встроить непосредственно в язык, так как компилятор может легко обнаруживать наличие конкретного побочного эффекта у функций.
К сожалению, это неприемлемое ограничение. Принцип Разделения Команд и Запросов запрещает только абстрактные побочные эффекты, к объяснению которых мы и переходим. Дело в том, что некоторые конкретные побочные эффекты не только безвредны, но и полезны. Есть два таких вида.
Первая категория включает функции, модифицирующие состояние по ходу выполнения. Они изменяют видимые компоненты, но, заканчивая свою работу, все приводят в порядок, восстанавливая исходное состояние. Рассмотрим в качестве примера класс, описывающий целочисленный список с курсором и функцию, вычисляющую максимальный элемент списка:
max is
-- Максимальное значение элементов списка
require
not empty
local
original_index: INTEGER
do
original_index := index
from
start; Result := item
until is_last loop
forth; Result := Result.max (item)
end
go (original_index)
end
Для прохода по списку алгоритму необходимо перемещать курсор поочередно ко всем элементам, так что функция, вызывающая такие процедуры, как start, forth и go, полна побочными эффектами, но, начиная свою работу с курсором в позиции original_index, она и заканчивает свою работу в этой же позиции, благодаря вызову процедуры go. Но ни один компилятор в мире не может обнаруживать, что подобные побочные эффекты только кажущиеся, а не реальные.
Побочные эффекты второго приемлемого типа могут реально изменять состояние объектов, воздействуя на невидимые клиентам свойства. Для более глубокого понимания концепции полезно вернуться к обсуждению понятий абстрактной функции и инвариантов реализации, рассматриваемых вкурса "Основы объектно-ориентированного программирования", в частности стоит взглянуть на рисунки, соответствующие этим понятиям.
Мы видели, что программный (конкретный) объект является реализацией абстрактного объекта и что два конкретных объекта могут быть реализациями одного и того же абстрактного объекта. Например, два различных представления стека могут задавать один и тот же стек. Конкретные стеки могут использовать массивы с маркером вершины count и одинаковыми элементами ниже count. Но они могут быть массивами разной размерности и иметь разные элементы, расположенные за count. С точки зрения математика каждый конкретный объект принадлежит области определения абстрактной функции a, и мы можем иметь c1 c2 хотя a(c1) = a(c2).
Для нас это означает, что функция, модифицирующая конкретный объект, безвредна, если соответствующий абстрактный объект при этом не изменился. Предположим, например, что функция над стеками содержит операцию:
representation.put (some_value, count + 1)
(с гарантией, что емкость массива, по меньшей мере, равна count + 1). Тогда побочный эффект затронет область выше той, что отведена стеку, и в этом нет ничего плохого.
Конкретный побочный эффект, изменяющий конкретное состояние объекта c, является абстрактным побочным эффектом, если он также изменяет абстрактное состояние, другими словами, изменяет значение a(c) (другое определение, непосредственно используемое, появится чуть позже). Если побочный эффект не затрагивает абстрактного состояния - он безвреден.


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

ОО-подход хорош тем, что допускает "умные" реализации, допускающие изменения состояния "за сценой". Ниже мы увидим разумный и полезный пример применения этой техники.
Так как не для каждого класса определение основывается на полностью специфицированном АТД, то необходимо рабочее определение абстрактного побочного эффекта. Сделать это нетрудно. На практике АТД определяется интерфейсом, предлагаемым классом своим клиентам (отраженным, например, в краткой форме класса). Побочный эффект будет действовать на абстрактный объект, если он изменяет результат какого-либо из запросов, доступных клиентам. Вот определение:


Определение: абстрактный побочный эффект
Абстрактным побочным эффектом является такой конкретный эффект, который может изменить значение несекретного запроса.

Это и есть то понятие, которое используется в Принципе Разделения - принципе, запрещающем абстрактные побочные эффекты в функциях.
Определение ссылается на "несекретные", а не на экспортируемые запросы. Причина в том, что между статусами "секретный" (закрытый) и "экспортируемый" допускается статус выборочно экспортируемых запросов. Как только запрос "несекретный" - экспортируемый какому-либо из клиентов за исключением NONE, - мы полагаем, что изменение его результата является абстрактным побочным эффектом.

Стратегия

Абстрактные побочные эффекты, в отличие от конкретных, не могут легко обнаруживаться компиляторами. В частности, недостаточно проверить, что сама функция сохраняет значения всех несекретных атрибутов - эффект может быть непрямым и зависит от вызовов других процедур и функций. Другая причина в том, что, как в примере max некоторые побочные эффекты могут в конечном итоге исчезать. Многие из компиляторов способны выдавать предупреждение, если функция модифицирует экспортируемый атрибут.
Поэтому Принцип Разделения Команд и Запросов является методологическим предписанием, а не языковым ограничением. Это не снижает его важности.
Каждый ОО-разработчик должен применять этот принцип без исключения. Я следую ему многие годы и не пишу функций с побочным эффектом. Наша фирма ISE применяет его во всех своих продуктах. Конечно, для тех, где используется язык C, этот принцип нельзя выдержать полностью, но и там мы применяем его всюду, где можно. Он помогает нам добиваться лучших результатов - в инструментарии и библиотеках, допускающих повторное использование, при расширениях и масштабировании.
Возражения
Важно рассмотреть два общих возражения, приводимых при отказе от стиля, свободного от побочных эффектов.
Первое связано с обработкой ошибок. Часто функция с побочным эффектом в действительности является процедурой, а ее результат задает статус ошибок, которые могли возникнуть при работе процедуры. Но есть лучшие способы справиться с этой проблемой. Соответствующая ОО-техника позволяет клиенту после выполнения операции сделать запрос о ее статусе, представленном соответствующим атрибутом, как в следующем примере:
target.some_operation (...)
how_did_it_go := target.status
Заметьте, техника возвращения функцией статуса в качестве результата немного хромает. Она позволяет преобразовать процедуру в функцию, но хуже работает, когда подпрограмма сама является функцией, - что тогда делать с ее результатом? Возникают проблемы, когда статус задается не одним индикатором; в таких случаях нужно возвращать структуру, что близко к приведенной схеме, либо использовать глобальные переменные, что приводит к новым проблемам, особенно для больших систем, где многие модули могут включать состояние ошибки.
Второе возражение связано с общим недоразумением, например, считается, что интерфейс списка с курсором несовместим с параллельным доступом к объектам. Эта вера широко распространена (где бы я не читал лекции - в Санта-Барбаре, Сиэтле, Сингапуре, Санкт-Петербурге - всегда найдется человек, задающий подобный вопрос).
Недоразумение связано с тем, что в параллельном контексте имеется некоторая операция get доступа к буферу - параллельному аналогу очереди. Такая функция выполняется без прерываний и в нашей терминологии реализует как вызов item, так и remove. Элемент возвращается в качестве результата функции, а удаление из буфера является побочным эффектом. Но использование подобных примеров в качестве аргументов в защиту функций get-стиля смешивает два понятия. Что нам действительно необходимо в параллельном контексте - это способ, дающий клиенту исключительный доступ к заготовленному элементу для выполнения некоторых операций. Имея такой механизм, можно защитить клиента, когда он выполняет последовательно операции:
x := buffer.item; buffer.remove
где гарантируется, что элемент, полученный при выполнении первой операции, будет действительно удален второй операцией. Такой механизм необходим вне зависимости от того, допускаем ли мы побочный эффект в функциях. Он, например, может понадобиться при выполнении операций удаления двух соседних элементов:
buffer.remove; buffer.remove
Гарантирование того, что удаляются два соседних элемента, никак не связано с побочными эффектами функций.
Позже в этой книге (см.) вопрос о параллельности будет подробно изучаться, там мы рассмотрим простой и элегантный подход к распределенным вычислениям, полностью совместимый с Принципом Разделения Команд и Запросов, который фактически поможет нам в достижении цели.
Законные побочные эффекты: пример
Закончим обсуждение побочных эффектов рассмотрением законного побочного эффекта - функции, не меняющей абстрактного состояния, но изменяющей конкретное состояние, и по вполне разумным причинам. Этот пример достаточно представителен и представляет некоторый шаблон проектирования.
Рассмотрим реализацию комплексных чисел. Как и в случае с точками, обсуждаемом в предыдущих лекциях, возможны два представления - декартово (с координатами x и y) и полярное (с расстоянием r и углом q). Какое из них выбрать? Простого ответа нет. Если, как обычно, обратиться к АТД, то разные применимые операции - сложение, вычитание, умножение и деление - и запросы для получения значений x, y, r и q эффективно выполняются для разных представлений (декартово представление лучше для сложения и умножения, полярное - для умножений и делений).
Можно было бы позволить клиенту решать, какое выбрать представление, но это делает классы трудными в использовании и нарушает принцип скрытия информации от клиента, которому нет дела до представления.
Альтернативой является одновременное хранение двух представлений. Но это приводит к издержкам производительности. Предположим, что клиенту требуются только операции умножения и деления. В этом случае операции используют только полярное представление, но мы бы каждый раз вычисляли бы x и y, выполняя бесполезные и дорогие вычисления.
Лучшее решение состоит в отказе от априорного выбора, выполняя выбор при необходимости. Мы практически ничего не проигрываем по памяти - все атрибуты нам все равно нужны, к ним добавятся только два булевых атрибута, указывающих на выбор текущего представления, но это позволит избежать лишних вычислений.
Пусть наш класс включает следующие операции:
class COMPLEX feature
... Объявления компонентов:
infix "+", infix "-", infix "*", infix "/",
add, subtract, multiply, divide,
x, y, rho, theta, ...
end
Запросы x, y, rho и theta представляют экспортируемые функции, возвращающие вещественные значения. Они всегда определены (исключая theta для комплексного числа 0). Помимо инфиксных функций "+" и других предполагаем процедуру add и другие. Вызов: z1 + z2 дает новое комплексное число, вызов z1.add (z2) изменяет z1. На практике могут понадобиться только функции или только процедуры.
Наш класс включает следующие секретные (закрытые) атрибуты:
cartesian_ready: BOOLEAN
polar_ready: BOOLEAN
private_x, private_y, private_rho, private_theta: REAL
Не все четыре вещественных атрибута необходимы постоянно, фактически только два являются текущими. Более точно, следующий инвариант реализации должен быть включен в класс:
invariant
cartesian_ready or polar_ready
polar_ready implies (0 <= private_theta and private_theta <= Two_pi)
-- cartesian_ready implies (private_x and private_y являются текущими)
-- polar_ready implies (private_rho and private_theta являются текущими)
Последние два предложения выражены неформально в форме комментария.
В каждый момент по крайней мере одно из представлений является текущим. Любая из операций, запрашиваемая клиентом, должна использовать наиболее подходящее для нее представление, что может потребовать вычислений, если представление не является текущим. В качестве побочного эффекта операции другое представление перестает быть текущим.
Две закрытые процедуры доступны для проведения изменений представления:
prepare_cartesian is
-- Сделать доступным декартово представление
do
if not cartesian_ready then
check polar_ready end
-- Поскольку инвариант требует, чтобы одно
-- из двух представлений было текущим
private_x := private_rho * cos (private_theta)
private_y := private_rho * sin (private_theta)
cartesian_ready := True
-- Здесь cartesian_ready и polar_ready равны true:
-- Оба представления являются текущими
end
ensure
cartesian_ready
end
prepare_polar is
-- Сделать доступным полярное представление
do
if not polar_ready then
check cartesian_ready end
private_rho := sqrt (private_x ^ 2 + private_y ^ 2)
private_theta := atan2 (private_y, private_x)
polar_ready := True
-- Здесь cartesian_ready и polar_ready равны true:
-- Оба представления являются текущими
end
ensure
polar_ready
end
Функции cos, sin, sqrt и atan2 берутся из стандартной математической библиотеки, atan2(y, x) вычисляет arctangent(y/x).
Нам также нужны процедуры создания - make_cartesian и make_polar:
make_cartesian (a, b: REAL) is
-- Инициализация: abscissa a, ordinate b
do
private_x := a; private_y := b
cartesian_ready := True; polar_ready := False
ensure
cartesian_ready; not polar_ready
end
и симметрично для make_polar.
Экспортируемые операции пишутся просто, начнем, например, с процедуры, имеющей варианты в зависимости от операции:
add (other: COMPLEX) is
-- Добавляет значение other
do
prepare_cartesian; polar_ready := False
private_x := x + other.x; private_y = y + other.y
ensure
x = old x + other.x; y = old y + other.y
cartesian_ready; not polar_ready
end
Заметьте, в постусловии важно использовать x и y, а не private_x и private_y, которые могут не быть текущими перед вызовом.
divide (z: COMPLEX) is
-- Divide by z.
require
z.rho /= 0
-- Численное выражение дает более реалистичное предусловие
do
prepare_polar; cartesian_ready := False
private_rho := rho / other.rho
private_theta = (theta - other.theta) \\ Two_pi
-- \\ - остаток от деления
ensure
rho = old rho / other.rho
theta = (old theta - other.theta) \\ Two_pi
polar_ready; not cartesian_ready
end
Аналогично для вычитания и умножения - subtract и multiply. (Предусловие и постусловие могут быть слегка адаптированы для учета особенностей операций с плавающей точкой.) Варианты функций следуют тому же образцу:
infix "+" (other: COMPLEX): COMPLEX is
-- Сумма текущего числа и other
do
create Result.make_cartesian (x + other.x, y + other.y)
ensure
Result.x = x + other.x; Result.y = y + other.y
Result.cartesian_ready
end
infix "/" (z: COMPLEX): COMPLEX is
-- Частное от деления текущего комплексного числа на z
require
z.rho /= 0
do
create Result.make_polar (rho / other.rho, (theta - other.theta) \\ Two_pi)
ensure
Result.rho = rho / other.rho
Result.theta = (old theta - other.theta) \\ Two_pi
Result.polar_ready
end
Аналогично для infix "-" и infix "**".


Обратите внимание на последние предложения в постусловиях этих функций - cartesian_ready и polar_ready должны экспортироваться самому классу, появляясь в предложениях в форме feature {COMPLEX}; они не экспортируются никакому другому классу.

Но где здесь побочные эффекты? В последних двух функциях они непосредственно не видны. Все дело в x, y, rho и theta - они являются хитроумными создателями побочных эффектов. Вычисление x или y приведет к изменению представления (вызовется prepare_cartesian), если не подготовлено декартово представление. Все симметрично для rho и theta. Вот примеры для x и theta:
x: REAL is
-- Abscissa
do
prepare_cartesian; Result := private_x
end
theta: REAL is
-- Angle
do
prepare_polar; Result := private_theta
end
Функции y и rho подобны. Все эти функции вызывают процедуру, которая может включить изменение состояния. В отличие от add и его собратьев, однако, они не делают предыдущее представление неверным, когда вычисляется новое представление. Например, если x вызывается в состоянии с ложным значением cartesian_ready, оба представления (все четыре вещественных атрибута) станут текущими. Все это потому, что функциям разрешается производить побочные эффекты только на конкретных объектах, но не на ассоциированных абстрактных объектах. Выразим это свойство более формально: вычисление z.x или другой функции может изменять конкретный объект, связанный с z, скажем от c1 до c2, но всегда с гарантией того, что
a(c1) = a(c2)
где a - абстрактная функция. Объекты c1 и c2 могут быть различными, но они представляют один и тот же математический объект - комплексное число.
Такие побочные эффекты безвредны. Они действуют только на секретные атрибуты и, следовательно, не могут быть обнаружены клиентами.
ОО-подход поощряет такие гибкие, адаптирующиеся схемы, выбирающие наилучшее представление, соответствующее потребностям текущего момента. Пока реализация действует на конкретное состояние, не затрагивая абстрактного, ее функции не нарушают Принципа Разделения и не создают угрозу ссылочной прозрачности.

 

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