Next Previous Contents

Unix Review Column 14 -- Еще раз об объектах

Randal Schwartz

Май 1997

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

В моем предыдущем выпуске я начал обзор Объектно-ориентированных возможностей Perl. В качестве иллюстрации я создал класс, названный ``Car'', и дал этому классу ``методы'' позволяющие пассажирам входить, выходить и учитываться. Хотя вы можете читать этот выпуск, не прочитав предыдущий выпуск, но я вам настоятельно рекомендую сначала прочитать прочитать его. (Если у вас есть доступ к internet, то вы можете прочитать предыдущий выпуск по адресу http://www.stonehenge.com/merlyn/UnixReview/

Так, что продолжая с того, места там где я вас оставил в прошлый раз, давайте возьмем набор транспортных средств:


        my $myCar = new Car;
        my $myTruck = new Truck;

Теперь, это должно создать новые объекты Car и Truck. Теперь, эти транспортные средства будут похожи во многих свойствах, но будут отличаться в других свойствах. Мы представим это, используя ``наследование'' от общего класса ``Vehicle'', например вот так:


        BEGIN {
          @Car::ISA = "Vehicle";
          @Truck::ISA = "Vehicle";
        }

Причиной того, что я поместил этот код в блок BEGIN, поскольку необходимо, чтобы этот код был выполнен до того как я вызову оператор new, даже если, я помещу эти операторы в конце программы. Обычно, все настройки @ISA должны происходить до выполнения первых ``настоящих'' строк кода.

Но что здесь делается? Как вы видите, нет метода Car::new. Вместо этого Perl будет искать этот метод в классе Vehicle. Аналогично, нет метода Truck::new. Вместо этого, у нас имеется метод &Vehicle::new:


        sub Vehicle::new {
          bless my $self = {}, shift;
          my %parms = @_;
          $self->{Passengers} = $parms{Passengers} || {};
          $self;
        }

Заметьте, что это код очень похож на код Car::new из предыдущего выпуска. Первая строка создает анонимный ассоциативный массив, присваивая его переменной $self, и ассоциируя его с правильным пакетом. Заметьте, что имя пакета получается как первый параметр, в этом случае получаемом с помощью оператора shift. Вторая строка создает именованный ассоциативный массив %parms из оставшихся аргументов данного метода.

Третья строка отличается от строки метода Car::new. Я позволяю передавать список пассажиров, которые в начале будут посажены в машину, например вот так:


        $myCar = new Car Passengers => qw(Fred Dino);

Если такой список не был указан, то машина начинает свое движение пустой. Последняя строка возвращает созданную машину.

Так, что этот метод может использоваться и для класса Car и для класса Truck, для создания соответственно Car и Truck. Однако существует различее между Car и Truck, так, что мы получим немного проблем из-за ничего. Давайте добавим дополнительное поведение к этим транспортным средствам, и затем начнем различать их.

Сначала, давайте снова добавим методы enter, leave и jump из предыдущего выпуска:


        $myCar->enter(qw(Fred Barney));
        $myTruck->enter(qw(Wilma Betty));
        $myTruck->jump(To => $myCar, People => qw(Wilma));
        $myCar->leave(qw(Fred Barney Wilma));

Обычно говоря, Fred и Barney едут в машине, а Wilma и Betty трясутся в грузовике. Внезапно Wilma покидает грузовик и пересаживается в машину, а затем уговаривает Fred и Barney вместе с ней покинуть машину. (Betty все еще остается одна в грузовике). Давайте взглянем на то, как это будет реализовываться:


        sub Vehicle::enter {
          my $self = shift;
          for (@_) {
            $self->{Passengers}->{$_} = "seat";
          }
          $self;
        }

В этом коде, объект сохраняется в переменной $self, а затем для каждого из оставшихся аргументов, мы добавляем объект в хеш Passengers с именем в качестве ключа и номером места в качестве значения (также как и в прошлом выпуске). Заметьте, что этот метод определяется в классе Vehicle, делая его доступным для классов Car и Truck. Аналогичным образом определяется мето leave:


        sub Vehicle::leave {
          my $self = shift;
          for (@_) {
            delete $self->{Passengers}->{$_};
          }
          $self;
        }

И в этом коде, отличие между методами enter и leave заключается в том, что они делают с хешем Passengers. Метод jump является немного пугающим, но все равно достаточно легким для чтения:


        sub Vehicle::jump {
          my $self = shift;
          my %parms = @_;
          my $dest_vehicle = $parms{To} ||
            new {ref $self};
          my $people = $parms{People} ||
            [keys %{$self->{Passengers}}];
          $people = [$people] unless ref $people;
          $self->leave(@$people);
          $dest_vehicle->enter(@$people);
          $dest_vehicle;
        }

Первым делом, объект сохраняется в переменной $self, а оставшиеся аргументы формируют ассоциативный массив %parms. Выбирается транспортное средство назначения. Если существует запись в ассоциативном массиве для ключа To, то выбирается это значение, в противном случае создается новое транспортное средство.

Но какого типа должно быть транспортное средство? Произвольное допущение, соединенное с годами программирования, позволяет мне считать, что новое транспортное средство должно быть того же размера и формы, что и средство, которое покидает пассажир. Мы можем получить тип вызывая ref $self, результат которого передается как ``объект'' для вызова new. (Было бы неплохо так создавать объекты в реальном мире? Но я отклонился от темы...)

Далее следует назначенный пассажир, который переходит из машины в машину. Это будет либо список, указанный параметром People, либо все пассажиры, находящиеся в машине. Заметьте, что здесь ожидается, что $people будет ссылкой на список, но если кто-то передаст скаляр, то у нас позже будут проблемы, так что строка, которая начинается с $people =, если необходимо, превращает простое имя в одно-элементный анонимный список.

После того, как определены исходное транспортное средство ($self), люди (@$people) и устройство в которое будет производится переход ($dest_vehicle) мы можем начать перемещение людей. Сначала людей просят покинуть исходное транспортное средство, а затем просят войти в требуемое транспортное средство. В заключение, возвращается новое транспортное средство (которое может быть тем же самым, что и переданное как параметр To, или совершенно новым объектом Car или Truck).

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


        sub Vehicle::passengers {
          my $self = shift;
          sort keys %{self->{Passengers}};
        }

Почему бы не дать пользователям классов Car и Truck знать, что $self->{Passengers} является хешем, ключами которого являются текущие пассажиры? Хорошо, я мог бы выбрать этот способ, если бы я был ленив, или если бы производительность была бы критическим фактором. Однако, я был привязан к этому интерфейсу навсегда, даже если бы позже я обнаружил, что легче было бы список пассажиров реализовать в виде списка, а не ассоциативного массива. Однако, лучше предоставить методы для доступа, поддеpживающие pаботу пользователей не напpямую.

Эти классы транспортных средств имеют вопиюще малую обработку ошибок. Давайте добавим такую проверку, и покажем, почему классы Car и Truck являются различными, в одно и тоже время:


        sub Vehicle::enter {
          my $self = shift;
          if (@_ + keys %{$self->{Passengers}} > $self->seats) {
            die "Cannot add @_ to $self";
          }
          for (@_) {
            $self->{Passengers}->{$_} = "seat";
          }
          $self;
        }

Давайте взглянем на то, что происходит здесь. Я добавляю длину списка аргументов (в массиве @_) к количеству ключей, для того, чтобы посмотреть, не больше ли это число числа, возвращаемого при вызову метода seats. Если число больше, то в машину пытается сесть больше людей, чем имеется мест, и происходит прекращение выполнения программы.

Но что такое метод seats? Я его еще не определил, но он будет выглядеть примерно так:


        sub Vehicle::seats { 2 }

Это не слишком сложно. Теперь и грузовики и легковые машины, все имеют по 2 места, также как и все другие классы, унаследованный от Vehicle. Но подождите --- легковые машины имеют по 4 места! Так, что просто добавим этот факт:


        sub Car::seats { 4 }

Когда вызывается метод $myCar->seats, то он возвращает значение 4 (поскольку мы нашли seats прямо в Car). Но когда вызывается $myTruck->seats, то метода Truck::seats не существует, так что Perl произведет поиск в путях наследования, для того, чтобы найти значение по умолчанию для количества мест в Vehicle! Для отработки, мы могли бы определить еще несколько типов:


        BEGIN {
          @Motorcycle::ISA = "Vehicle";
          @Van::ISA = "Vehicle";
          @Unicycle::ISA = "Vehicle";
        }
        sub Van::seats { 8 }
        sub Unicycle::seats { 1 }

И заметьте, что я не определил метод seats для класса Motorcycle, поскольку значение по умолчанию (2) является правильным.

То, что вы увидели здесь, является примером наследования и перегрузки методов. Наследование выражается в том, что значение seats для 2-местных транспортных средств получается прямо из объекта Vehicle. Перезагрузка выражается в том, что seats для транспортных средств с другим количеством мест, перекрывает значение, определенное в классе Vehicle.

Я надеюсь, что вы насладились этой краткой прогулкой в страну объектов, и вы смогли увидеть некоторые возможности. Наслаждайтесь!


Next Previous Contents