Переменные, присваивание и ссылки
9.2. Атрибуты
Есть два вида переменных (сущностей, которым можно присваивать значения). Первый вид мы уже видели — это локальные переменные, включая Result. Второй вид, который начинаем изучать, — атрибуты. Он не вполне нов, неявно мы с ним встречались под маркой полей объекта, когда рассматривали создание объекта. Но теперь мы можем дополнить наше понимание этой концепции и указать место атрибутов среди сущностей и других созданий в нашем ОО-питомнике.
Поля, методы, запросы, функции, атрибуты
Мы уже видели при обсуждении создания объекта, что он существует во время выполнения в памяти компьютера как набор полей, часть из которых является ссылками, другие относятся к основным ("развернутым") типам:
Подобно любому другому свойству объекта, эти поля должны приходить из спецификации генерирующего класса. Действительно, каждое поле приходит из запроса, и даже более точно — атрибута.
Вернемся к началу. Метод, как вы знаете, является командой или запросом. Запрос, в отличие от команды, возвращает результат. Запрос может, в свою очередь, быть либо функцией, либо атрибутом. Это функция, если результат получается в результате вычисления функции. Например, класс LINE имеет запрос:
south_end: STATION — Конечная станция на южной стороне do if not is_empty then Result := metro_stops.first.station end end Это функция. Но в том же классе существует другой запрос без алгоритма (do ... end части): index: INTEGER — Индекс текущей станции на линии
Это атрибут. Включение его в класс означает, что каждый экземпляр класса будет иметь поле заданного типа — INTEGER, содержащее текущее значение index для станции:
Присваивание атрибуту
Как указано в комментарии, index в классе LINE — это индекс позиции "курсора". Курсор — это абстрактный механизм, позволяющий клиентам последовательно анализировать станции на линии, передвигаясь вперед и назад. Одной из команд, манипулирующей курсором, является команда start, устанавливающая курсор на первой станции (известной как south_end):
start — Установить курсор станции на первом элементе. do index := 1 ... Другие операторы... ensure on_first: index = 1 end
Клиент может вызвать этот метод для конкретной линии, например:
Line8.start
Результатом этого вызова будет установка значения index для соответствующего экземпляра класса LINE. Если до вызова поле объекта имело значение 8, как на предыдущем рисунке, то вызов переустановит его в 1, не затрагивая другие поля объекта.
Вызов Line8.start является квалифицированным вызовом start, выполненный клиентом. Обычно возможен и неквалифицированный вызов start, выполняемый другими методами класса LINE.
Скрытие информации: модификация полей
Две другие процедуры класса также изменяют значение атрибута index:
forth —Передвинуть курсор к следующему элементу require not_after: not after do index := index + 1 ensure moved_right: index = old index + 1 end go_i_th (i: INTEGER) — Передвинуть курсор к элементу с номером i require not_over_left: i >= 0 not_over_right: i <= count + 1 do index := i ensure set: index = i end
Все три процедуры позволяют клиентам устанавливать поле index для любого конкретного объекта, как например
Line8.start Line8.forth Line8.go_i_th (5)Листинг 9.1.
Крайне важно понимать, что для клиентов такие вызовы процедур являются единственным способом модифицировать это поле. Не допускается для этих целей использовать присваивание — проверьте, если хотите, и посмотрите, что скажет компилятор в ответ на попытку:
Line8.index := 98Листинг 9.2.
Прежде всего, нарушен синтаксис присваивания: цель присваивания должна быть переменной, идентификатором, в то время как Line8.index является выражением.
Причина такой синтаксической защиты понятна. Позволить клиентам непосредственно модифицировать поля объектов поставщика означало бы игнорирование принципов скрытия информации и хорошего проектирования. В самом начале мы говорили, что объект следует воспринимать как машину, иллюстрируя это рисунком, повторно воспроизводимом ниже. Клиенты могут управлять этой машиной только через операции официального интерфейса — кнопки команд и запросов.
Выполнение непосредственного присваивания your_machine.your_field:= my_value эквивалентно вскрытию машины и ковырянию в ее внутренностях. Для реальных машин это означало бы лишение гарантий, для программной машины — лишение интерфейса и связанных с ним гарантий контракта.
Заметьте ключевую разницу между ошибочным присваиванием и вызовом процедуры . Вызов связан предусловием go_ith, устанавливающим:
require not_over_left: i >= 0 not_over_right: i <= count + 1
Присваивание, в случае его разрешения, игнорировало бы предусловие.
Любая операция, которая может иметь доступ к полям объектам, их модификации, должна пройти через интерфейс, обеспечиваемый классом. Когда вы проектируете класс, ваше право и ваша обязанность — решить, что позволяется делать клиентам при работе с объектами класса. Для любого из атрибутов некоторого класса Т вы можете разрешить клиентам устанавливать значения поля, но в этом случае вы должны определить в классе соответствующую процедуру, называемую сеттером (setter), или установщиком1В таких языках, как С# и другие, такие методы со специальными свойствами называют методами – свойствами.
set_a ( x: T) — Установить значение a равным x. do a := x ensure set: a = x end
Эту процедуру клиенты могут использовать без всяких ограничений their_object.set_a (their_value). Но можно ввести предусловие, как в go_ith, которое ограничивает допустимые значения. Можно ограничить клиента и другими способами: если из класса LINE удалить метод go_ith, то клиенты могли бы изменять поле index, только используя методы start и forth. Наконец, можно вообще не давать клиентам никакого метода, позволяющего им присваивать значения полю index.
В первом случае, когда при проектировании класса решено предоставить клиентам возможность модифицировать значение поля, некоторые полагают, что синтаксис присваивания пример 9.2 предпочтительнее вызова сеттера пример 9.1. Можно ли использовать синтаксис пример 9.2, но не как присваивание, а как синтаксический сахар, упрощенную форму вызова пример 9.1?
Это и в самом деле возможно, если объявить процедуру сеттера как команду присваивания для связанного запроса, a или index. Для этого достаточно изменить объявление запроса следующим образом: a: T assign set_a или index: INTEGER assign go_ith. Тогда запись obj.a:= v является синтаксически корректной, но означает не присваивание, а вызов obj.set_a (v). Другие детали будут даны в дальнейших обсуждениях.
Независимо от синтаксиса, статическое семантическое правило одно и то же: единственный способ модифицирования объекта извне — через процедуру "сеттер". Для осознания фундаментальности такого подхода рассмотрите следующие два события в эволюции системы.
- В какой-то момент вы решили — хотя это и не было включено в первоначальную концепцию ПО, — что каждая модификация атрибута должна фиксироваться записью в базу данных (например "1-го мая в 7.55 температура аппарата была изменена на 22 °C").
- Добавлены ограничения на возможные значения атрибута (например, температура аппарата должна находиться в пределах от -5° C до +30° C). Это ограничение может быть введено в виде предложения инварианта класса.
Для выполнения первой модификации достаточно добавить операторы (обновления базы данных) в процедуру сеттер. Вторая модификация — более деликатная, поскольку влечет добавление предусловия к сеттеру, следовательно, отражается на каждом клиенте, работающем с полем. Так как каждый клиент использует сеттер, выявить таких клиентов нетрудно, что позволяет обновить их в соответствии с новыми требованиями, принуждая выполнять предусловие.
Если бы допускалось прямое присваивание, то для вас наступили бы тяжелые времена, пришлось бы разбираться со всеми клиентами, разыскивая модификации полей. Хуже того, для новых клиентов было бы невозможно реализовать новую политику регистрации изменений в базе данных или проверять, что значения соответствуют установленным границам.
Обобщим это обсуждение введением простого принципа.
Почувствуй методологию
Единственным способом, которым клиент устанавливает значения полей объекта, должен быть вызов (в любой синтаксической форме) экспортируемых сеттер-процедур.
Это ключевое правило современного проектирования является непосредственным следствием принципа скрытия информации. При программировании на Eiffel это принуждение происходит естественно. Для других языков программирования, где применяются другие политики управления доступом к атрибутам, это должно стать важным методологическим правилом.