Глава 6.8. Классы и объекты

6.8.1. Классы, объекты и методы

После краткого знакомства с модулями мы можем теперь перейти к подробному описанию их применения для создания объектно-ориентированных программ. К объектному программированию относятся по-разному: одни как к единственному верному способу программирования, другие как к модной причуде. Автор считает, что объектный подход позволяет быстро и четко формализовать постановку задачи на языках программирования, поэтому дальнейшее описание создания модулей и работы с ними построено именно на фундаменте этого подхода.

Как известно, объектно-ориентированное программирование основано на двух базовых понятиях: классы и объекты. Класс — это абстракция, описывающая некие свойства (данные класса и методы работы с ними). Объект — это конкретный экземпляр класса, реализующий его свойства.

PERL не содержит ни классов, ни объектов в традиционном их понимании. Тем не менее, объектное программирование на нем вполне возможно, если руководствоваться тремя правилами, сформулированными создателями этого языка:

  1. Класс — это модуль, создающий объекты и обеспечивающий доступ к их методам.
  2. Объект — это ссылка на класс, "освященная" специальной функцией bless().
  3. Метод — это подпрограмма, первым аргументом которой является ссылка на объект или имя модуля.

Давайте рассмотрим эти правила более подробно.

6.8.2. Создание классов

Мы сказали, что класс в PERLе — это просто модуль с определенными свойствами. Поэтому начнем с создания модуля, на примере которого будем демонстрировать вводимые далее понятия. В качестве примера возьмем класс Browser, описывающий Веб-обозреватели, которым мы пользовались в п. 3.7.1 при описании JavaScript и создадим его на языке PERL. Назовем этот пакет Info::Browser и соответственно разместим его в файле Info/Browser.pm.

В модуле Info::Browser мы не будем пользоваться модулем Explorer, поскольку он не экспортирует никаких символов. Прежде всего, мы должны определить структуру данных нашего класса. Чаще всего структура классов реализуется безымянными ассоциативными массивами, в которых поля данных индексируются их названиями. Для модуля Info::Browser мы используем ассоциативный массив с тремя полями данных: строка NAME (название обозревателя), число VERSION (номер версии) и массив OPTIONS из двух логических значений (первое значение указывает, разрешен ли вызов Java-аплетов, второе — разрешены ли куки).

Далее нам нужен метод, создающий объекты данного класса. Такие методы называются конструкторами и могут носить любое имя. Однако, большинство программистов на PERLе называют их либо именем new(), либо именем класса. Наш конструктор выглядит так:

sub new {
  my $self  = {};           # ссылка на безымянный пустой ассоциативный массив
  $self->{NAME}    = undef; # инициализация его полей неопределенными значениями
  $self->{VERSION} = undef;
  $self->{OPTIONS} = [];    # ссылка на безымянный пустой массив
  bless($self);             # $self становится объектом
  return $self;
}

Здесь в комментариях нуждается только функция bless. Она имеет две формы

bless ref, class
bless ref

Эта функция сообщает содержимому ссылки ref, что оно является объектом класса class. Если class опущен, то используется текущий пакет. Функция bless возвращает ссылку ref, поэтому оператор return в нашем конструкторе не обязателен и введен только для ясности.

Тепеь мы готовы написать полный текст модуля Info::Browser. Помимо конструктора он содержит три метода name(), version() и options(), которые обеспечивают доступ к полям данных наших объектов. Эти методы следуют такому соглашению: если они вызваны без аргументов, то возвращают текущее значение поля, а если с аргументом, то заносят его в соответствующее поле данных.

package Info::Browser;
use strict;

sub new {
  my $self  = {};
  $self->{NAME}    = undef;
  $self->{VERSION} = undef;
  $self->{OPTIONS} = [];
  bless($self);
  return $self;
}
sub name {
  my $self = shift;
  if (@_) { $self->{NAME} = shift }
  return $self->{NAME};
}
sub version {
  my $self = shift;
  if (@_) { $self->{VERSION} = shift }
  return $self->{VERSION};
}
sub options {
  my $self = shift;
  if (@_) { @{ $self->{OPTIONS} } = @_ }
  return $self->{OPTIONS};
}

1;  # для функций require или use

Следующий пример показывает, как создать объект класса Info::Browser и инициализировать его поля:

use Browser;
$myBrowser = Browser->new();
$myBrowser->name("Opera");
$myBrowser->version(5);
$myBrowser->options(1, 0);
printf "%s %d : ", $myBrowser->name, $myBrowser->version;
printf "Java %s, ", $myBrowser->options->[0] ? "enabled" : "disabled";
printf "Cookies %s.\n", $myBrowser->options->[1] ? "enabled" : "disabled";

Обратите внимание, что пользователю нашего модуля не нужны подробности его реализации. Он просто вызывает конструктор, а затем обращается к его методам.

Созданный нами модуль вполне работоспособен, но его конструктор имеет два недостатка. Во-первых, он вызывается только как метод класса, но не как метод объекта. Иными словами, мы не можем написать

$x = Browser->new();
$y = $x->new();

Во-вторых, функция bless в нем жестко привязана к классу Info::Browser, поскольку мы вызываем ее без второго аргумента. Если мы в дальнейшем захотим создавать классы, являющиеся потомками Info::Browser, то должны вызывать bless с двумя аргументами, помня, что любому методу класса этот класс передается в качестве первого аргумента. С учетом этих замечаний наш конструктор принимает вид:

sub new {
  my $proto = shift;                 # извлекаем имя класса или указатель на объект
  my $class = ref($proto) || $proto; # если указатель, то взять из него имя класса
  my $self  = {};
  $self->{NAME}    = undef;
  $self->{VERSION} = undef;
  $self->{OPTIONS} = [];
  bless($self, $class);              # гибкий вызов функции bless
  return $self;
}

Все приведенные выше методы работали с полями данных объекта. Разумеется, возможности методов этим не ограничены. Мы можем, например, добавить к нашему классу такой метод:

sub showObject {
  my $self = shift;
  printf "%s %d : ", $self->name, $self->version;
  printf "Java %s, ", $self->options->[0] ? "enabled" : "disabled";
  printf "Cookies %s.\n", $self->options->[1] ? "enabled" : "disabled";
}

6.8.3. Деструкторы объектов

Большинство языков программирования, поддерживающих создание объектов через конструкторы, поддерживают и деструкторы объектов. Деструктор — это метод, который автоматически вызывается исполняющей средой, когда происходит удаление объекта. В отличие от конструктора он обязан иметь единственное возможное имя, а именно DESTROY. Причина этого состоит именно в том, что он чаще всего вызывается автоматически системой сборки мусора, которая требует, чтобы все деструкторы назывались одинаково.

Хотя деструкторы объектов в PERLе и предусмотрены, писать их приходится крайне редко, опять-таки благодаря наличию системы сборки мусора. По сути дела, единственная ситуация, в которой написание деструктора необходимо, это объекты, содержащие прямую или опосредованную ссылку на себя. Такой объект никогда не будет автоматически удален (во всяком случае, пока программа не завершится), и, если мы озабочены оптимизацией памяти, то должны явно вызвать деструктор этого объекта.

6.8.4. Данные класса

До сих пор все данные в нашем классе были данными объекта, т. е. относились к конкретному экземпляру класса. Однако, во многих случаях бывают нужны переменные, относящиеся к классу в целом. В нашем примере такой переменной могла бы быть переменная $Count, содержащая количество существующих экземпляров класса. С ее использованием начало модуля выглядит так:

package Browser;
use strict;

my $Count = 0;

sub new {
  my $proto = shift;
  my $class = ref($proto) || $proto;
  $Count++;
  my $self  = {};
  $self->{NAME}    = undef;
  $self->{VERSION} = undef;
  $self->{OPTIONS} = [];
  bless($self, $class);
  return $self;
}
sub DESTROY {
  $Count--;
}
sub total {
  return $Count;
}

Как мы видим, добавились метод total(), возвращающий значение $Count, и деструктор объектов, необходимый для правильного подсчета существующих объектов.

Данные и методы, относящиеся к классу в целом, а не к его экземплярам, называются статическими.

6.8.5. Наследование

Все объектно-ориентированные системы программирования включают в себя понятие наследования. Наследование означает, что создаваемый класс может быть объявлен потомком уже существующего класса. Потомок класса наследует все его свойства, но может дополнительно иметь собственные свойства или изменять унаследованные свойства.

PERL не поддерживает наследование классов. Вместо этого каждый пакет содержит переменную @ISA, управляющую наследованием методов. Если мы вызываем метод класса или объекта, которого нет в пакете данного класса, то PERL просматривает пакеты, перечисленные в @ISA, в поисках метода с таким именем. Рассмотрим такой пакет:

package Info::System;
use Info::Browser;
@ISA = qw(Info::Browser);
1;

Этот короткий модуль создает новый класс Info::System, который наследует все методы класса Info::Browser. Например, мы могли бы написать такую программу:

use Info::System;
$myBrowser = Info::System->new();
$myBrowser->name("Netscape");
$myBrowser->version(6);
$myBrowser->options(1, 1);
$myBrowser->showObject;
print "Total: ".Info::System->total()."\n";

Посмотрим, что происходит при вызове метода Info::System->new(). Поскольку в пакете Info::System такого метода нет, PERL ищет его в массиве @ISA и находит в пакете Info::Browser. Затем он вызывает этот метод, передавая ему в качестве первого аргумента имя класса Info::System, т. е. данный оператор заменяется на Info::Browser::new("Info::System"). После этого срабатывает функция bless, вторым аргументом которой является имя переданного класса, как описано выше. В результате мы получаем ссылку на вновь созданный объект класса Info::System, что нам и требовалось.

Теперь мы можем добавить к классу Info::System его собственные методы, например:

sub osname {
  my $self = shift;
  if (@_) { $self->{OSNAME} = shift }
  return $self->{OSNAME};
}

Больше того, мы можем переопределить в Info::System часть методов класса Info::Browser, например:

sub showObject {
  my $self = shift;
  printf "%s %d on %s: ", $self->name, $self->version, $self->osname;
  printf "Java %s, ", $self->options->[0] ? "enabled" : "disabled";
  printf "Cookies %s.\n", $self->options->[1] ? "enabled" : "disabled";
}

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

6.8.6. Класс UNIVERSAL

Истинные ценители PERLа считают, что вся прелесть этого языка в его умолчаниях, и потому пишут на нем так, чтобы непосвященный ничего не понял. Даже не разделяя этого подхода, следует признать, что знание умолчаний PERLа часто является полезным. Одним из таких полезных действий по умолчанию является следующее: PERL всегда неявно добавляет в конец массива @ISA имя модуля UNIVERSAL. Иными словами, все классы PERLа наследуют методы класса UNIVERSAL. Таких методов три: can, isa и VERSION.

Метод can

Синтаксис: ссылка->can(метод)
Аргументы: метод — строковое выражение
Результат: ссылка на подпрограмму

Если объект, на который указывает ссылка, имеет заданный метод, то метод can возвращает ссылку на этот метод. В противном случае он возвращает undef. Пример:

$sub = $obj->can('print');

Метод isa

Синтаксис: ссылка->isa(класс)
Аргументы: класс — строковое выражение
Результат: логическое значение

Метод isa возвращает истину, если ссылка указывает на класс или его наследника, и ложь в противном случае. Пример:

$has_io = $fd->isa("IO::Handle");

Метод VERSION

Синтаксис: ссылка->VERSION(версия?)
Аргументы: версия — плавающее выражение
Результат: номер версии

Метод VERSION возвращает значение глобальной переменной $VERSION в пакете, на который указывает ссылка. Если задана версия и она больше, чем значение переменной $VERSION, то программа завершается по фатальной ошибке с соответствующей диагностикой. Примеры:

$his_vers = $obj->VERSION();
Some_Module->VERSION(3.0);

Обычно этот метод явно не вызывается; его вызывает функция use() при проверке версии. Для того, чтобы обеспечить проверку версии своего модуля, нужно просто добавить в него строку вида

our $VERSION = '1.1';

6.8.7. Безымянные подпрограммы как объекты

Реализация объектов в виде безымянных ассоциативных массивов, которой мы до сих пор пользовались, не является, разумеется, единственно возможной. Здесь приводится альтернативная реализация класса Info::Browser, основанная на безымянных подпрограммах. Она несколько сложнее и медленнее, но имеет одно существенное преимущество: данные объекта становятся полностью скрытыми от внешнего мира. Новый вариант нашего класса имеет вид:

package Info::Browser;
use strict;

my $Count = 0;

sub new {
  my $proto = shift;
  my $class = ref($proto) || $proto;
  $Count++;
  my $self = { NAME => undef, VERSION => undef, OPTIONS => []};
  my $closure = sub {
    my $field = shift;
    if (@_) { $self->{$field} = shift }
    return $self->{$field};
  };
  bless($closure, $class);
}
sub DESTROY {
  $Count--;
}
sub total {
  return $Count;
}
sub name    { &{$_[0]}("NAME", @_[1..$#_]) }
sub version { &{$_[0]}("VERSION", @_[1..$#_]) }
sub options { &{$_[0]}("OPTIONS", [@_[1..$#_]]) }
sub showObject {
  my $self = shift;
  printf "%s %d : ", $self->name, $self->version;
  printf "Java %s, ", $self->options->[0] ? "enabled" : "disabled";
  printf "Cookies %s.\n", $self->options->[1] ? "enabled" : "disabled";
}

1;

Теперь объект, возвращаемый методом new(), это не ссылка на структуру данных, а ссылка на безымянную подпрограмму, которая имеет доступ к данным объекта через локальную переменную $self. Самое интересное, что программу, которая пользуется этим классом, изменять не нужно.

6.8.8. Связывание переменных

Очень интересной возможностью языка PERL является связывание переменных с объектами. Суть этого явления состоит в том, чтобы скрыть реализацию объекта за переменной. Мы можем читать и изменять значение связанной переменной обычным образом, но при этом неявно будут вызываться соответствующие методы объекта, которые могут быть сколь угодно сложными.

Для того, чтобы использовать связывание, мы должны оформить свой класс специальным образом. Это оформление зависит от типа связываемой переменной (скаляр, массив, ассоциативный массив, ссылка или описатель файла). Дистрибутив PERL содержит соответствующие базовые классы Tie::Scalar, Tie::Array и пр., облегчающие создание связываемых классов. Здесь мы приведем пример только связывания скалярной переменной; для других типов техника будет аналогичной.

Наш пример OutFile скрывает за связанной переменной запись в текстовый файл. При связывании переменной с классом OutFile вызывается его конструктор, который создает файл с заданным именем. При удалении связи этот файл закрывается. При каждом присваивании связанной переменной нового значения это значение выводится в файл как новая строка. Соответствующий пакет OutFile.pm будет иметь вид:

package OutFile;

# Конструктор класса
sub TIESCALAR {
  my $class = shift;
  my $filename = shift;
  open(OUT, ">$filename") or die "Cannot create $filename: $!\n";
  bless {FileHandle => OUT, Value => 0}, $class;
}

# Чтение значения связанной переменной
sub FETCH {
  my $self = shift;
  return $self->{Value};
}

# Задание нового значения связанной переменной
sub STORE {
  my $self = shift;
  my $value = shift;
  my $handle = $self->{FileHandle};
  print $handle "$value\n";
  $self->{Value} = $value;
}

# Деструктор класса
sub DESTROY {
  my $self = shift;
  my $handle = $self->{FileHandle};
  close $handle;
}

1;

Теперь мы можем написать программу, использующую класс OutFile. Для этого нам потребуются встроенные функции PERL, создающие и удаляющие связь переменных с объектами. Существуют три функции работы со связанными переменными: tie(), tied() и untie().

Функция tie устанавливает связь переменной с классом. Она имеет вид

tie var, class, arguments

Эта функция связывает переменую var с классом class. При этом вызывается конструктор класса, которому передается список аргументов arguments. Результатом функции является ссылка на созданный объект. В дальнейшем мы можем получить эту ссылку в любой момент вызовом функции

tied var

(Если функции tied передана несвязанная переменная, то она возвращает неопределенное значение.) Для разрыва связи переменной с объектом используется функция

untie var

При разрыве связи неявно вызывает деструктор объекта, связанного с переменной.

Теперь мы можем написать пример использования переменной, связанной с классом OutFile:

use OutFile;

my $test;
tie $test, 'OutFile', 'myfile.txt';
$test = 'First';
$test = 'Second';
$test = 'Third';
untie $test;

Выполнив эту программу, мы найдем в текущем каталоге новый файл MYFILE.TXT, содержащий следующие строки:

First
Second
Third

Чаще всего связывание переменных используется для установления неявной связи ассоциативных массивов с базами данных, но эта тема выходит за пределы нашего справочника.