Принципы проектирования класса
Если от объектной технологии ожидать только одного преимущества, то следовало бы указать на проектирование интерфейсов. В самом начале мы определяли ОО-технологию как архитектурную технику производства программных систем, построенных из модулей, согласованных по интерфейсу. Теперь у нас есть техническая база, позволяющая пользоваться наилучшими ОО-механизмами для построения модулей с привлекательными интерфейсами. Поскольку успех класса определяется тем, насколько он привлекателен для своих клиентов, то наше внимание будет уделяться не столько внутренней реализации, а тому, насколько прост его интерфейс, легок в обучении, прост для запоминания, способен выдержать проверку временем и изменениями.
В задачу нашего исследования входит ряд важных вопросов: должны ли функции допускать побочные эффекты, много ли аргументов должно быть у компонентов, чем отличаются связанные понятия операнда и опции (option), следует ли сосредоточиваться на размерах класса, как сделать абстрактные структуры активными, роль выборочного экспорта, как документировать класс, что делать в особых случаях?
Из нашего обсуждения будет ясно, что проектировщик класса должен как опытный мастер довести свое изделие до совершенства, полируя до блеска, делая его привлекательным для клиентов, насколько это только возможно. Этот дух рассмотрения классов как тщательно сработанных инженерных изделий, совершенных по замыслу, исполнению и всегда остающимися таковыми, является всеобъемлющим качеством хорошо определенной объектной технологии. По очевидным причинам он особенно проявляется при конструировании библиотек классов.
Оригинальные конструкторские разработки вначале проверяются на машинах, участвующих в гонках Formula 1, прежде чем они станут достоянием автомобильной промышленности. Доказав применимость при разработке успешной библиотеки повторно используемых компонентов, рассматриваемые идеи обеспечат преимущество и любому ОО-ПО независимо от того, предполагалось ли его повторное использование с самого начала.
Побочные эффекты в функциях
Первый вопрос, исследованием которого мы займемся, оказывает глубокое влияние на стиль нашего проектирования. Законно ли для функций - подпрограмм, возвращающих результат, - иметь еще и побочный эффект, то есть изменять нечто в их окружении?
Наш ответ - нет. Но почему? Обоснование требует понимания роли побочных эффектов, осознания различий между "хорошим" и "плохим" побочным эффектом. Рассмотрим этот вопрос в свете наших знаний о классах - их происхождения от АТД, понятия абстрактной функции и роли инварианта класса.
Команды и запросы
Напомним используемую терминологию. Компоненты, характеризующие класс разделяются на команды и запросы. Команды модифицируют объекты, а запросы возвращают информацию о них. Команды реализуются процедурами, а запрос может быть реализован либо атрибутом - тогда в момент запроса возвращается значение соответствующего поля экземпляра класса, либо функцией - тогда происходит вычисление значения по алгоритму, заданному функцией. Процедуры и функции называются подпрограммами.
В определении запроса не сказано, могут ли изменяться объекты в момент запроса. Для команд ответ очевиден - да, поскольку в этом и состоит их назначение. Для запросов вопрос имеет смысл только в случае их реализации функциями, поскольку доступ к атрибуту ничего не меняет. Изменение объектов, выполняемое функцией, называется ее побочным эффектом (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 (см. лекцию 7 курса "Основы объектно-ориентированного программирования").
Присваивание атрибуту, ставшее причиной побочного эффекта, может находиться в самой функции или встроено глубже - в другой подпрограмме, вызываемой функцией. Вот полное определение:
Определение: конкретный побочный эффект Функция производит конкретный побочный эффект, если ее тело содержит:
|
Термин "конкретный" будет пояснен ниже. В последующем определении мы второе предложение сформулируем как "вызов подпрограммы, создающей (рекурсивно) конкретный побочный эффект". Определение побочного эффекта будет расширено и не будет, как теперь, относиться только к функциям. Но выше приведенное определение на практике предпочтительнее, хотя по разным причинам его можно считать либо слишком строгим, либо слишком слабым:
- Определение кажется слишком строгим, поскольку любой вызов процедуры рассматривается как создающий побочный эффект, в то время как можно написать процедуру, ничего не меняющую в мире объектов. Такие процедуры могут менять нечто в окружении: печатать страницу, посылать сообщения в сеть, управлять рукой робота. Мы будем рассматривать это как своего рода побочный эффект, хотя программные объекты при этом не меняются.
- Определение кажется слишком слабым, поскольку оно игнорирует случай функции f, вызывающей функцию g с побочным эффектом. Соглашение состоит в том, что в этом случае сама f считается свободной от побочного эффекта. Это допустимо, поскольку правило, которое будет выработано в процессе нашего рассмотрения, будет запрещать все побочные эффекты определенного вида, так что нет необходимости в независимой сертификации каждой функции.
Преимущество этого соглашения в том, что для определения статуса побочного эффекта достаточно анализировать тело только самой функции. Достаточно тривиально, имея анализатор языка, встроить простой инструментарий, который для каждой функции независимо определял бы, обладает ли она конкретным побочным эффектом в соответствии с данным определением
Ссылочная прозрачность
Почему нас волнуют побочные эффекты функций? Ведь в природе ПО заложено изменение вещей в процессе выполнения.
Если позволить функциям, подобно командам, изменять объекты, то мы потеряем многие из их простых математических свойств. Как отмечалось при обсуждении АТД (см. лекцию 6 курса "Основы объектно-ориентированного программирования"), математики знают, что их операции над объектами не меняют объектов (Вычисление |21/2| не меняет числа 2). Эта неизменяемость является основным отличием мира математики и мира компьютерных вычислений.
Неизменяемость объектов имеет важное практическое следствие, известное как ссылочная прозрачность (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.
Функции без побочных эффектов можно рассматривать в программных текстах как термы в обычном математическом смысле. Мы будем поддерживать четкое различение команд, изменяющих объекты, но не возвращающих результатов, и запросов, обеспечивающих информацией об объектах, но не изменяющих их.
Это же правило неформально можно выразить так: " задание вопроса не меняет ответ ".