Во время построения объектной модели задачи мы выделили отдельные объекты, которые для обмена данными друг с другом используют интерфейс — внешние свойства и методы. При этом все внутренние данные и детали внутреннего устройства объекта должны быть скрыты от «внешнего мира». Такой подход позволяет:
• обезопасить внутренние данные (поля) объекта от изменений (возможно, разрушительных) со стороны других объектов;
• проверять данные, поступающие от других объектов, на корректность, тем самым повышая надёжность программы;
• переделывать внутреннюю структуру и код объекта любым способом, не меняя его внешние характеристики (интерфейс); при этом никакой переделки других объектов не требуется.
Скрытие внутреннего устройства объектов называют инкапсуляцией («помещение в капсулу»).
Заметим, что в объектно-ориентированном программировании инкапсуляцией также называют объединение данных и методов работы с ними в одном объекте.
Разберём простой пример. Во многих системах программирования есть класс, описывающий свойства «пера», которое используется при рисовании линий в графическом режиме. Назовём этот класс ТРеп, в простейшем варианте он будет содержать только одно поле Color, которое определяет цвет. Будем хранить код цвета в виде символьной строки, в которой записан шестнадцатеричный код составляющих модели RGB. Например, 'FFOOFF' — это фиолетовый цвет, потому что красная (R) и синяя (В) составляющие равны FF16 = 255, а зелёной составляющей нет вообще. Класс можно объявить так: type ТРеп = class
Color: string; end;
По умолчанию все члены класса (поля и методы) открытые, общедоступные (англ. public). Те элементы, которые нужно скрыть, в описании класса помещают в «частный» раздел (англ. private), например, так:
type ТРеп = class private
FColor: string; end;
В этом примере поле FColor закрытое. Имена всех закрытых полей далее будем начинать с буквы «F» (от англ. field — поле). К закрытым полям нельзя обратиться извне (это могут делать только методы самого объекта), поэтому теперь невозможно не только изменить внутренние данные объекта, но и просто узнать их значения. Чтобы решить эту проблему, нужно добавить к классу ещё два метода: один из них будет возвращать текущее значение поля FColor, а второй — присваивать полю новое значение. Эти методы доступа назовем getColor (в переводе с англ. — получить Color) и setColor (в переводе с англ. — установить Color):
type TPen=class private
FColor: string; public
function getColor: string; procedure setColor(newColor: string); end;
Обратите внимание, что оба метода находятся в секции public (общедоступные).
Что же улучшилось по сравнению с первым вариантом (когда поле было открытым)? Согласно принципам ООП, внутренние поля объекта должны быть доступны только с помощью методов. В этом случае внутреннее представление данных может как угодно отличаться от того, как другие объекты «видят» эти данные.
В простейшем случае метод getColor можно написать так:
function TPen.getColor: string; begin
Result:=FColor; end;
В методе setColor мы можем обрабатывать ошибки, не разрешая присваивать полю недопустимые значения. Например, установим, что символьная строка с кодом цвета, передаваемая нашему объекту, должна состоять из шести символов. Если эти условия не выполняются, будем записывать в поле FColor код чёрного цвета ’000000':
procedure TPen.setColor(newColor: string); begin
if Length(newColor)<>6 then
FColor:='000000' { если ошибка, то чёрный цвет} else FColor:=newColor end;
Теперь если pen — это объект класса TPen, то для установки и чтения его цвета нужно использовать показанные выше методы:
pen.setColor (1FFFF00'); {изменение цвета) writeln( 'цвет пера: ', pen.getColor );
{получение цвета}
Итак, мы скрыли внутренние данные, но одновременно обращение к свойствам стало выглядеть довольно неуклюже: вместо pen. Color : = ' FFFF00' теперь нужно писать pen . setColor ('FFFF00') . Чтобы упростить запись, во многие объектно-ориентированные языки программирования ввели понятие свойства (англ. property), которое внешне выглядит как переменная объекта, но на самом деле при записи и чтении свойства вызываются методы объекта.
Свойство — это способ доступа к внутреннему состоянию объекта, имитирующий обращение к его внутренней переменной.
Свойство color в нашем случае можно определить так:
type TPen = class private
FColor: string; function getColor: string; procedure setColor(newColor: string); public
property color: string read getColor
write setColor;
end;
Здесь методы getColor и setColor перенесены в раздел private, т. е. закрыты от других объектов. Однако есть общедоступное свойство color строкового типа:
property color: string read getColor
write setColor;
При чтении этого свойства (англ. read) вызывается метод getColor, а при записи нового значения (англ. write) — метод setColor. В программе можно использовать это свойство так:
pen.color:='FFFF001; {изменение цвета}
writeln( 'цвет пера: ', pen.Color );
{получение цвета}
Поскольку приведённая выше функция getColor просто возвращает значение поля FColor и не выполняет никаких дополнительных действий, можно было вообще удалить метод getColor и объявить свойство так:
property color: string read FColor write setColor;
В этом случае при чтении выполняется прямой доступ к полю.
Таким образом, с помощью свойства color другие объекты могут изменять и читать цвет объектов класса TPen. Для обмена данными с «внешним миром» важно лишь то, что свойство color — символьного типа, и оно содержит 6-символьный код цвета. При этом внутреннее устройство объектов TPen может быть любым, и его можно менять как угодно. Покажем это на примере.
Хранение цвета в виде символьной строки неэкономно и неудобно, так как большинство стандартных функций используют числовые коды цвета. Поэтому лучше хранить код цвета как целое число, и поле FColor сделать целого типа:
FColor: integer;
При этом необходимо поменять методы getColor и setColor, которые непосредственно работают с этим полем:
function ТРеп.getColor: string;
begin
Result:=IntToHex(FColor,6);
end;
procedure TPen.setColor(newColor: string);
begin
if Length(newColor)<>6 then
FColor:=0 {если ошибка, то чёрный цвет} else begin
FColor:=StrToInt(1 $'+newColor); end;
end;
Для перевода числового кода в символьную запись используется функция IntToHex, входящая в библиотеку FreePascal (модуль SysUtils). Её второй параметр — количество цифр, которое будет в шестнадцатеричном числе. Обратный перевод выполняет функция StrToInt. Для того чтобы указать, что число записано в шестнадцатеричной системе, перед ним добавляют символ $.
В этом примере мы принципиально изменили внутреннее устройство объекта: заменили строковое поле на целочисленное. Однако другие объекты даже не «догадаются» о такой замене, потому что сохранился интерфейс — свойство color по-прежнему имеет строковый тип. Таким образом, инкапсуляция позволяет как угодно изменять внутреннее устройство объектов, не затрагивая интерфейс. При этом все остальные объекты изменять не требуется.
Иногда не нужно разрешать другим объектам менять свойство, т. е. требуется сделать свойство «только для чтения» (англ. read-only). Пусть, например, мы строим программную модель автомобиля. Как правило, другие объекты не могут непосредственно менять его скорость, однако могут получить информацию о ней — «прочитать» значение скорости. При описании такого свойства слово write и название метода записи не указывают вообще:
type ТСаг = class private Fv: real;
public
property v: real read Fv; end;
Таким образом, доступ к внутренним данным объекта возможен, как правило, только с помощью методов. Применение свойств (property) очень удобно, потому что позволяет использовать ту же форму записи, что и при работе с общедоступной переменной объекта.
При использовании скрытия данных длина программы чаще всего увеличивается, однако мы получаем и важные преимущества. Код, связанный с объектом, разделён на две части: общедоступную часть (секция public) и закрытую (private). Объект
взаимодействует с другими объектами только с помощью своих общедоступных свойств и методов (интерфейс) — рис. 7.8.
Поэтому при сохранении интерфейса можно как угодно менять внутреннюю структуру данных и код методов, и это никак не будет влиять на другие объекты. Подчеркнём, что всё это становится действительно важно, когда разрабатывается большая программа и необходимо обеспечить её надёжность.
Вопросы и задания
1. Что такое интерфейс объекта?
2. Что такое инкапсуляция? Каковы её цели?
3. Чем различаются секции public и private в описании классов? Как определить, в какую из них поместить свойство или метод?
4. Почему рекомендуют делать доступ к полям объекта только с помощью методов?
5. Что такое свойство? Зачем во многие языки программирования введено это понятие?
6. Можно ли с помощью свойства обращаться напрямую к полю объекта, не используя метод?
7. Почему методы доступа, которые использует свойство, делают закрытыми?
8. Зачем нужны свойства «только для чтения»? Приведите примеры.
9. Подумайте, в каких ситуациях может быть нужно свойство «только для записи» (которое нельзя прочитать). Как ввести такое свойство в описание класса? Приведите примеры.
Подготовьте сообщение
а) «Инкапсуляция в языке Си»
б) «Инкапсуляция в языке Javascript»
в) «Инкапсуляция в языке Python»