Лекция 14. Объектное программирование
Лекция посвящена разработке программ на Perl с использованием объектного подхода. Это общепринятая современная технология программирования, позволяющая бороться со сложностью создаваемых программ путем классификации объектов и моделирования их поведения.
Цель лекции: научиться писать программы на Perl с применением технологии объектного программирования. Освоить способы описания классов и приемы работы с объектами, включая свойства и методы классов.
Технология объектно-ориентированного программирования представляет из себя современный подход к созданию информационных систем, основанный на применении программных объектов, моделирующих состояние и поведение реальных сущностей. Не углубляясь в теорию, можно дать такие неформальные определения основным терминам, применяемым в объектно-ориентированном программировании:
[x]. Класс (class) - это именованное описание для однотипных сущностей их неотъемлемых характеристик (атрибутов) и их поведения (методов). Примеры классов: "Личность", "Хоббит", "Маг".
[x]. Объект (object) - это сущность, относящаяся к определенному классу, хранящая набор конкретных значений данных и предоставляющая для их обработки методы, предусмотренные классом. Примеры объектов: "хоббит по имени Фродо Бэггинс", "маг по имени Гэндальф". Синоним термина "объект": экземпляр (instance) класса.
[x]. Атрибут (attribute) - описание характеристики объекта, значение которой будет храниться в объекте. Примеры атрибутов: "имя", "рост". Набор конкретных значений атрибутов является текущим состоянием (state) объекта. Пример значения атрибута: "имя - Гэндальф". Синонимы термина "атрибут": свойство (property), переменная объекта (object variable), переменная экземпляра (instance variable), данные-элементы (member data).
[x]. Метод (method) - это действие объекта, изменяющее его состояние или реализующее другое его поведение. Пример методов: "назвать свое имя", "стать невидимым". Синонимы термина "метод": операция (operation), функция-элемент (member function).
[x]. Инкапсуляция (encapsulation) - это (1) объединение в объекте набора данных и методов работы с ними; (2) принцип ограничения доступа к данным объекта, когда работать с ними можно только через его методы (скрытие данных).
[x]. Наследование (inheritance) - это создание нового класса на основе существующего с целью добавить к нему новые атрибуты или изменить его поведение. Пример наследования: на основании класса "Личность" создаются его подклассы "Хоббит", "Маг", "Эльф" и "Человек", каждый из которых обладает свойствами и поведением "Личности", но добавляет собственные свойства и меняет поведение.
[x]. Полиморфизм (polymorphism) - это различное поведение объектов, принадлежащих к различным классам, при обращении к одинаково названному методу. Пример полиморфизма: в ответ на призыв "К оружию!" гном схватит боевой топор, эльф приготовит лук и стрелы, а хоббит спрячется за дерево.
Но программистам, пишущим на Perl, можно не запоминать эти труднопроизносимые термины. Объектный подход к программированию реализуется в Perl изящно и легко - при помощи уже известных нам понятий и конструкций.
В нынешней версии Perl нет специальных синтаксических конструкций для выражения идей объектно-ориентированной технологии. Поэтому Perl нельзя назвать объектно-ориентированным языком, но он поддерживает объектный подход при разработке программ. Для создания программ с использованием объектов применяются имеющиеся в языке средства, которые сводятся к нескольким простым соглашениям:
[x]. Класс - это пакет, в котором описаны методы, реализующие поведение создаваемых объектов.
[x]. Объект - это переменная (или замыкание), на которую указывает ссылка, связанная с именем пакета.
[x]. Метод - это подпрограмма из пакета, доступ к которой происходит по ссылке на объект, которую он получает в качестве первого аргумента.
[x]. Атрибуты объекта хранятся в динамически создаваемых переменных, чаще всего - в анонимных хэшах.
[x]. Наследование - это поиск методов, не найденных в текущем пакете, в пакетах, перечисленных в специальном массиве @ISA.
Теперь рассмотрим примеры описания классов средствами языка Perl и приемы работы с объектами.
Класс описывается в виде одноименного пакета, в котором размещаются определения методов, реализующих поведение объектов этого класса. Описания одного или нескольких классов сохраняются в виде модуля. Как минимум один из методов класса отвечает за создание объектов класса. Такой метод называется конструктором (constructor) и обычно носит имя new (или его имя совпадает с именем класса). Для хранения атрибутов объекта очень часто применяется анонимный хэш, ключи которого задают имена атрибутов. Первым аргументом конструктор получает имя класса, которое он использует для преобразования ссылки на анонимный хэш в ссылку на объект указанного класса. Это "магическое" превращение выполняется с помощью встроенной функции bless ("благословить"), благодаря которой каждый созданный объект помечается принадлежащим к определенному классу. После этого при обращении к методам объекта они отыскиваются в пакете с таким же именем. Вот как происходит превращение объекта "ссылка" в объект определенного класса:
my $class = 'Hobbit'; # имя класса в виде строки
my $object = { }; # ссылка на анонимный хэш,
# где будут храниться данные объекта,
bless($object, $class); # "благословляется" указывать
# на объект класса $class
Для примера опишем класс "Личность" (Person), сохранив его в файле Person.pm. Начало описания класса будет выглядеть так:
package Person; # класс - это пакет
sub new { # метод-конструктор объектов
my $class = shift; # 1-й параметр ссылка на имя класса
my $self = {}; # контейнер для атрибутов объекта
$self->{name} = ''; # начальные значения атрибутов
bless($self, $class); # "благословить" объект ссылки
return $self; # вернуть ссылку на созданный объект
}
Затем в описании класса обычно определяются методы для доступа к атрибутам объекта. Для примера определим метод для доступа (accessor) к атрибуту 'name' ("имя") и метод для изменения его значения (modifier).
sub say_name { # метод доступа (accessor) к атрибуту name
my ($self) = @_; # получить ссылку на объект
return $self->{name}; # вернуть значение атрибута
}
sub give_name { # метод изменения (modifier) атрибута name
my $self = $_[0]; # 1-й аргумент: ссылка на объект
$self->{name} = $_[1]; # 2-й аргумент: новое значение
}
1; # истинное значение требуется для use
__END__ # конец описания класса
В классе описываются методы для работы с атрибутами объектов класса, причем часто один метод используется для чтения и для изменения значения атрибута. В примере опишем метод для чтения и записи (mutator) свойства 'height' ("рост"):
sub height { # метод чтения и записи атрибута height
my $self = shift; # извлечь ссылку на объект
$self->{height} = shift # присвоить новое значение,
if @_; # если передан аргумент
return $self->{height}; # вернуть значение атрибута
}
Обратите внимание, что описание класса значительно проще, чем описание традиционного модуля. Для работы с классом не требуется никаких списков экспортирования имен. Вместо этого описываются методы, которые можно рассматривать как сервисы, предоставляемые классом для взаимодействия с каждым из конкретных экземпляров класса. Набор методов для управления поведением объекта называют его интерфейсом. Для работы с объектами класса достаточно знать этот интерфейс, не вдаваясь в детали реализации поведения объектов.
В программе, в которой применяются объекты описанного класса, мы увидим вполне знакомую нотацию, когда подпрограммы вызываются при помощи ссылочных переменных и операции ->. В объектной терминологии это называется обращением к методам объектов (или отправка сообщения объекту). Приведем пример создания двух объектов одного класса, каждый из которых обладает собственными значениями свойств:
# способ обращения к методам через ссылки на объекты
use Person; # будем использовать этот класс
# создать объект класса,
my $hobbit = Person->new(); # вызвав его конструктор
# задать значение свойства, обратившись к методу объекта
$hobbit->give_name('Фродо Бэггинс');
# создать другой объект
my $dwarf = Person->new; # () не обязательны
$dwarf->give_name('Гимли'); # задать значение свойства
# запросить значения свойств, обратившись к методам
print $hobbit->say_name(), ' ', $dwarf->say_name, " ";
Взаимодействие с объектом строится на обращении к его методам. Обращение к методу происходит при помощи ссылки на экземпляр конкретного объекта, и при этом первым аргументом в метод автоматически передается ссылка на этот объект. Например:
$hobbit->give_name('Бильбо Бэггинс'); # соответствует вызову:
Person::give_name($hobbit, 'Бильбо Бэггинс');
Внутри метода ссылка на экземпляр объекта используется для доступа к данным этого экземпляра и обращения к другим методам. Для обращения к конструктору используется имя класса, так как во время работы конструктора уже существует класс, а экземпляр объекта только должен быть создан конструктором.
Если к ссылке на объект класса Person применить функцию ref(), то она вернет значение не 'HASH', как можно было бы предположить, а 'Person'! Это результат "благословения" объекта ссылки функцией bless().
print "Класс объекта: '", ref($hobbit), "' "; # 'Person'
Кроме нотации с оператором "стрелка" ->, традиционно используемой при работе со ссылками, для доступа к методам применяются синтаксические конструкции с использованием косвенных объектов. При использовании этого стиля имя метода стоит перед именем класса или ссылкой на объект, после которой идет список аргументов, иногда заключаемый в круглые скобки. Использование косвенных объектов может сделать текст программы более наглядным и понятным. Приведем пример обращения к объектам в новом стиле:
# способ обращения к методам через косвенные объекты
use Person; # используем класс Person
my $magician = new Person; # "этот маг - новая личность"
give_name $magician 'Гэндальф'; # "назовем мага 'Гэндальф'"
my $name = say_name $magician; # "назови себя, маг"
print $name, " ";
В качестве иллюстрации к сказанному на рис. 14.1 изображены языковые конструкции, применяемые при работе с объектами, и их взаимосвязи.
Рис. 14.1.Конструкции объектного программирования в Perl
Наследование - это мощный механизм конструирования нового класса, позволяющий уточнить существующий класс, изменить его поведение родительского класса или добавить к нему новые свойства. В Perl это делается легко и просто: нужно упомянуть имя родительского класса в специальном массиве @ISA текущего класса. Исполняющая система, не найдя вызванного метода в текущем модуле, продолжает его поиск в пакетах, перечисленных в массиве @ISA. Приведем пример описания класса Wizard, производного от класса Person:
package Wizard; # класс "Маг"
our @ISA = qw(Person); # является подклассом Person
use Person; # и использует пакет Person
# ... описание методов класса Wizard...
1; # вернуть истину для use
Смысл наследования - в создании подклассов, изменяющих поведение базового класса. Для этого в дочерних классах описываются новые методы или переопределяются существующие. В качестве примера опишем для класса Wizard новый метод для работы со свойством 'magic' ("тип магии" - белая или черная):
sub magic { # магия - вот что отличает волшебника
my $self = shift; # извлечь ссылку на объект
$self->{magic} = shift if @_; # изменить значение
return $self->{magic}; # вернуть значение
}
Кроме того, переопределим конструктор объектов класса new() так, чтобы он принимал два аргумента для инициализации свойств 'name' и 'magic'. Для создания объекта воспользуемся конструктором родительского класса, затем зададим начальные значения свойств, и, наконец, "дадим благословение" объекту ссылки быть магом:
sub new { # конструктор объектов
my $class = $_[0]; # имя класса в 1-м аргументе
my $self = new Person; # маг - это личность
$self->{name} = $_[1]; # задать имя из 2-го аргумента
$self->{magic} = $_[2]; # и тип магии из 3-го
bless($self, $class); # "благословить" мага
return $self; # вернуть ссылку на объект
}
Вызывающая программа, использующая производный класс, будет выглядеть следующим образом:
use Wizard; # подключить производный класс
# создать нового черного мага - Сарумана
my $wizard = new Wizard('Саруман', 'black');
my $name = say_name $wizard; # "назови себя, маг"
print $name, ' ', $wizard->magic(); # 'Саруман black'
print ref($wizard); # тип объекта ссылки - 'Wizard'
Естественно, что у объекта класса Wizard можно вызывать не только методы собственного класса, но и любые методы, унаследованные из родительского класса Person.
В классе может быть описан специальный метод, автоматически вызываемый исполняющей системой при уничтожении каждого объекта. Такой метод называется деструктор (destructor), и он должен иметь зарезервированное имя - DESTROY. Деструктор вызывается при освобождении памяти, занимаемой объектом: это происходит при выходе из блока, где был создан объект, при удалении последней ссылки на объект функцией undef($object) или при окончании программы. Приведем пример шуточного деструктора для класса Person, который при удалении объекта направляет прощание в поток STDERR, называя имя объекта методом say_name():
sub DESTROY {
warn('Прощайте, я ухожу... ' . shift->say_name);
}
Деструктор может использоваться, если при окончании работы с объектом нужно выполнить какие-то завершающие действия: например, удалить динамически созданные структуры или сохранить данные объекта в файле. Конструктор в этом случае может считывать сохраненные значения из файла, чтобы присвоить объектам начальные значения.
Анонимные хэши - это самый распространенный, но не единственный способ хранить значения атрибутов объекта. Для этого может применяться массив или даже скалярная переменная, лишь бы при создании объекта в конструкторе это хранилище значений было связано с именем класса функцией bless(). Недостатком этого подхода можно считать то, что ограничение доступа к свойствам достигается лишь на уровне соглашения пользоваться только методами объекта. И поскольку существует возможность изменить значение атрибута напрямую, это может нарушить корректную работу программы. Ведь в методе изменение состояния объекта сопровождается необходимыми проверками, чего не происходит при непосредственном изменении атрибута. Тем более, что в некоторых случаях атрибуты вообще должны быть доступны только для чтения (read-only attribute). Например, при использовании хэша для хранения атрибутов вполне возможно такое некорректное присваивание:
$hobbit->{magic} = 'пёстрая'; # добавлен ошибочный атрибут
Для того чтобы надежно обеспечить ограничение доступа к данным, которые хранятся в объекте, применяются замыкания. Чтобы показать, как можно организовать полностью закрытые атрибуты (private attributes) с помощью замыканий, напишем класс Private::Person. В новой версии класса значения атрибутов также хранятся в анонимном хэше, но при создании объекта возвращается ссылка не на него, а на анонимную подпрограмму доступа к данным. Этой функции будет передаваться имя атрибута (и, возможно, новое значение), а она будет возвращать значение атрибута, используя имя атрибута как ключ поиска в анонимном массиве. Это выглядит так:
package Private::Person; # класс "Личность"
sub new { # прототипом может быть
my $invocant = shift; # класс или объект
my $class = ref($invocant) || $invocant;
my $self = { # значения атрибутов:
NAME => '', # имя и
HEIGHT => 0.0 # рост
};
my $closure = sub { # функция доступа к данным
my $field = shift; # по имени атрибута
$self->{$field} = shift if @_; # изменим и
return $self->{$field}; # вернем значение
}; # объектом будет
bless($closure, $class); # ссылка на функцию
}
# метод доступа к атрибуту name
sub name {
my $self = shift; # ссылка на объект-функцию
&{$self}("NAME", @_); # доступ к скрытому значению
}
# метод доступа к атрибуту height
sub height { # то же, что выше, но несколько короче:
&{ $_[0] }("HEIGHT", @_[1 .. $#_ ] )
}
1;
Методы доступа к свойствам объектов получают первым аргументом ссылку на функцию доступа к значениям атрибутов и вызывают ее, передавая ей имя поля и остальные свои аргументы. Приведем пример создания объектов нового класса и обращения к их методам. В вызывающей программе все выглядит так, как будто данные по-прежнему хранятся в анонимном массиве:
package main; # вызывающая программа
use Private::Person; # использовать этот класс
my $elf = Private::Person->new; # создать объект и
$elf->name("Леголас"); # задать значения
$elf->height(189); # его атрибутам
# получить доступ к значениям атрибутов объекта
print $elf->name, ' ', $elf->height, ' ';
print ref($elf), " "; # тип референта: 'Private::Person'
Из примера видно, что имя класса может быть составным, отражая иерархию классов. Поскольку классы - это пакеты, хранящиеся в файле-модуле, то все, что говорилось в предыдущей лекции об именовании модулей, относится и к классам.
Обратите также внимание на то, что конструктор класса Private::Person определен так, что он может вызываться с использованием либо имени класса, либо ссылки на существующий объект. Это проверяется в следующей строке:
my $class = ref($invocant) || $invocant;
Если первым аргументом передана ссылка на объект, то определяется имя его класса, иначе считается, что передано имя класса. Поэтому в программе можно создавать однотипные объекты, обращаясь к методу new() существующего объекта. Например, так:
my $hobbit = Private::Person->new; # вызов с именем класса
$hobbit->name("Bilbo Baggins");
my $frodo = $hobbit->new; # вызов со ссылкой на объект
$frodo->name("Frodo Baggins");
В классе могут быть определены методы, не предназначенные для работы с конкретными объектами. Такие методы называются методами класса или статическими методами. Для обращения к ним, так же как для обращения к конструктору, используется имя класса, а не ссылка на объект. Часто эти методы обслуживают данные, общие для всех объектов класса (то есть объявленные глобально на уровне класса). Подобные данные называются атрибутами класса. В качестве примера опишем класс Magic::Ring, где метод класса count() будет использоваться для доступа к значению атрибута класса $Magic::Ring::count, в котором будет храниться количество созданных волшебных колец.
package Magic::Ring; # класс "Магическое Кольцо"
sub new { # конструктор
my ($class, $owner) = @_; # имя класса и значение атрибута
$Magic::Ring::count++; # сосчитать новое Кольцо
bless({owner => $owner}, $class); # "благословить" хэш
}
sub owner { # метод чтения и записи атрибута owner
my $self = shift; # извлечь ссылку на объект
$self->{owner} = shift if @_; # изменить значение атрибута
return $self->{owner}; # вернуть значение атрибута
}
$Magic::Ring::count = 0; # атрибут класса: число Колец
sub count { # метод класса
return $Magic::Ring::count;
}
1; # конец описания класса Magic::Ring
В программе, использующей класс Magic::Ring, создается набор объектов. При каждом обращении к конструктору увеличивается счетчик созданных магических колец $Magic::Ring::count.
package main;
use Magic::Ring; # использовать класс "Магическое Кольцо"
my @rings = ();
for (1..3) { # "Три кольца - премудрым эльфам..."
push @rings, new Magic::Ring('эльф');
}
for (1..7) { # "Семь колец - пещерным гномам..."
push @rings, new Magic::Ring('гном');
}
for (1..9) { # "Девять - людям Средиземья..."
push @rings, new Magic::Ring('человек');
}
# "А Одно - всесильное - Властелину Мордора..."
push @rings, new Magic::Ring('Саурон');
# Сколько всего было сделано колец?
print Magic::Ring->count, " "; # будет выведено: 20
В стандартную библиотеку модулей Perl входит модуль Class::Struct, который облегчает жизнь программистам по описанию классов, предоставляя для объявления класса функцию struct(). Эта функция генерирует описание класса в указанном пакете, включая методы для доступа к атрибутам класса. Причем помимо имени атрибута она позволяет задавать его тип с помощью разыменовывающего префикса: скаляр ($), массив (@), хэш (%), ссылка на подпрограмму (&) или объект. Насколько просто и удобно пользоваться функцией struct, можно судить по такому примеру:
use Class::Struct; # подключаем стандартный модуль
# описываем класс Performer ("Исполнитель")
struct Performer => { # атрибуты класса:
name => '$', # "имя" - скаляр
country => '$', # "страна" - скаляр
artists => '%', # "артисты" - хэш
};
my $performer = new Performer; # создаем исполнителя
$performer->name('Pink Floyd'); # задаем значения атрибутов
$performer->country('Great Britain');
# заполняем атрибут-хэш:
$performer->artists('David Gilmour', 'гитары, вокал');
$performer->artists('Roger Waters', 'бас-гитара, вокал');
$performer->artists('Nick Mason', 'ударные');
$performer->artists('Richard Wright', 'клавишные');
# описываем класс Album ("Альбом")
struct Album => { # атрибуты класса:
title => '$', # "название" - скаляр
year => '$', # "год выхода" - скаляр
tracks => '@', # "композиции" - массив
performer => 'Performer', # "исполнитель" - объект
};
my $album = Album->new; # создаем альбом
$album->title('Dark Side of the Moon');
$album->year(1973);
# заполняем атрибут-массив:
$album->tracks(0, 'Breathe');
$album->tracks(1, 'Time');
# и так далее...
$album->performer($performer); # задаем атрибут-объект
Чтобы добавить к полученному описанию класса дополнительный метод, достаточно описать его в соответствующем пакете. Вот пример добавления метода Album::print и его использования в главной программе:
package Album; # переключаемся на нужный пакет
sub Album::print { # и описываем дополнительный метод
my $self = shift;
printf("%s '%s' (%d) ",
$self->performer->name, $self->title, $self->year);
foreach my $artist (keys%{$self->performer->artists}) {
printf(" %s - %s ",
$artist, $self->performer->artists($artist));
}
}
package main; # переключаемся на основную программу
$album->print; # и вызываем метод объекта
В заключение рассмотрим несколько распространенных приемов для работы с классами и объектами.
Функции bless() не обязательно передавать имя класса: если второго аргумента нет, она помечает объект ссылки именем текущего пакета. Поскольку bless() возвращает значение своего первого аргумента, а подпрограмма возвращает последнее вычисленное значение, то минимальный конструктор может выглядеть так:
sub new { # конструктор экземпляров класса
my $self = {}; # контейнер для атрибутов объекта
bless($self); # "благословить" объект ссылки
} # и вернуть ссылку (1-й аргумент bless)
При создании объекта удобно сразу задавать начальные значения его атрибутов, передавая аргументы конструктору. Если для инициализации атрибутов использовать хэш, то атрибуты можно задавать в любом порядке, а в конструкторе можно определить значения по умолчанию для незаданных атрибутов. Например, так:
my $language = Programming::Language->new(
NAME => 'Perl', # имя
VERSION => '5.8.7', # версия
AUTHOR = 'Larry Wall' # автор
);
Весьма полезно иметь в классе метод, который преобразовывает значения атрибутов объекта в строку. Такой метод обычно называется as_string() или to_string() и может применяться для отладочной печати состояния объекта. А если его определить в классе-"прародителе", то его можно будет применять к объектам всех унаследованных классов. Если использовать анонимный хэш для хранения значений атрибутов, то такой метод может выглядеть так:
sub to_string { # преобразование значений атрибутов в строку
my $self = shift;
my $string = '{ ';
foreach (keys %{$self}) {
$string .= "$_: '$self->{$_}' ";
}
$string .= '}';
return $string;
}
Благодаря тому, что Perl - это динамический язык, в нем легко создать класс, в котором свойства объектов добавляются во время выполнения программы. Для этого в классе описываются универсальные методы для работы со свойствами объекта, а затем в ходе выполнения задаются нужные свойства. Например, так:
package Human; # класс "Человек"
our @ISA = qw(Person); # это подкласс класса Person
use Person;
sub set { # универсальный метод изменения атрибутов объекта
my ($self, $name, $new_value) = @_;
my $old_value = $self->{$name};
$self->{$name} = $new_value;
return $old_value;
}
sub get { # универсальный метод доступа к атрибутам объекта
my ($self, $name) = @_;
return $self->{$name};
}
1;
package main; # главная программа
use Human; # подключить класс
my $hero = Human->new; # создать героя-человека
$hero->set ('имя', 'Арагорн'); # дать ему имя
$hero->set ('оружие', 'меч'); # и вооружить
В этой лекции мы научились работать с объектами. Объектный подход реализован в Perl весьма своеобразно, но понятно и эффективно. Использование этой технологии дает программисту возможность создавать приложения, соответствующие современным требованиям. А сочетание объектного программирования с динамической природой языка позволяет реализовывать оригинальные и эффективные решения.