Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 51511334d5
Fetching contributors…

Cannot retrieve contributors at this time

571 lines (392 sloc) 34.362 kb
=begin pod
=CHAPTER Классы и Объекты
Следующая программа показывает как может выглядеть обработчик
зависимостей в Perl 6. Она демонстрирует использование пользовательских
конструкторов, приватных и публичных атрибутов, методов, а также
некоторые аспекты сигнатур. Кода в примере приведено не много,
тем не менее он интересен и местами полезен.
=begin code
class Task {
has &!callback;
has Task @!dependencies;
has Bool $.done;
method new(&callback, Task *@dependencies) {
return self.bless(*, :&callback, :@dependencies);
}
method add-dependency(Task $dependency) {
push @!dependencies, $dependency;
}
method perform() {
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
}
my $eat =
Task.new({ say 'eating dinner. NOM!' },
Task.new({ say 'making dinner' },
Task.new({ say 'buying food' },
Task.new({ say 'making some money' }),
Task.new({ say 'going to the store' })
),
Task.new({ say 'cleaning kitchen' })
)
);
$eat.perform();
=end code
=head1 Приступая к изучению классов
X<|class>
X<|classes>
X<|state>
X<|has>
X<|classes, has>
X<|behavior>
X<|classes, behavior>
Perl 6, как и много других языков, использует ключевое слово C<class> для определения нового класса. Следующий затем блок, как и любой другой блок, может содержать произвольный код, однако классы обычно содержат определения состояний и поведения.
Код примера содержит атрибуты (состояния), определяемые с помощью ключевого слова C<has>, и поведения, определяемые с помощью C<method>.
X<|type object>
X<|defined>
X<|.defined>
Определение класса создает I<объект-тип>, который по умолчанию становиться
доступным внутри текущего пакета ( аналогично переменным, определенным с
помощью C<our> ). Этот объект-тип является "пустым экземпляром" класса.
С ними мы встречались в предыдущих главах. Например, каждый из типов C<Int>
и C<Str> относятся к объектам-типам одного из встроенных в Perl 6 классов.
Код примера в начале главы демонстрирует, как имя класса C<Task> может
использоваться в роли ссылки в дальнейшем, например для создания экземпляров класса вызывая метод C<new>.
Объекты-типы не определены в том смысле, что они возвращают значение C<False>
если вызвать их метод C<.defined>. Эту особенность можно использовать чтобы узнать является ли какой-либо объект объектом-типом или нет:
=begin code
my $obj = Int;
if $obj.defined {
say "Ordinary, defined object";
} else {
say "Type object";
}
=end code
=head1 Могу ли я обладать состоянием?
X<|attributes>
X<|classes, attributes>
X<|encapsulation>
X<|classes, encapsulation>
В примере, первые три строки в блоке класса:
has &!callback;
has Task @!dependencies;
has Bool $.done;
определяют атрибуты ( в других языках они могут называться I<полями>
или I<хранилищем экземпляра>). Атрибуты действительно являются
индивидуальным местом хранения данных для каждого созданного объекта.
Так же как переменные созданные с помощью C<my> не могут быть доступны
извне области их видимости так и атрибуты объектов доступны только
внутри класса. Данная особенность является основой объекто-ориентированного проектирования и называется I<инкапсуляцией>.
Первая строка среди атрибутов определяет память для хранения callback
-- небольшого куска кода. Он будет вызван для выполнения задачи, которую представляет экземпляр класса C<Task>.
=begin code
has &!callback;
=end code
X<|sigils, ampersand>
X<|twigils>
X<|twigils, !>
Сигил C<&> указывает, что атрибут представляет собой нечно вызываемое I<(invocable)>. Символ C<!> является твигилом I<(twigil)>, или выражаясь иначе - вторым сигилом. Твигил является частью имени переменной. В данном случае твигил C<!> подчеркивает что данный атрибут является приватным I<(собсвенным, private)>, то есть недоступен вне класса.
Определение второго атрибута класса C<Task> также содержит приватный твигил:
=begin code
has Task @!dependencies;
=end code
Однако этот атрибут представляет собой массив элементов и поэтому необходим
сигил C<@>. Каждый из элементов представляет собой задачу, а все вместе
очередность задач, которая является условием завершения текущей. Кроме
того тип атрибута сообщает, что элементы массива могут быть только
экземплярами класса C<Task> ( или класса, производного от него ).
Третий атрибут хранит статус готовности задачи:
=begin code
has Bool $.done;
=end code
X<|twigils, .>
X<|twigils, accessors>
X<|accessor methods>
X<|classes, accessors>
Этот скалярный атрибут ( сигил C<$> ) имеет тип C<Bool>. Вместо твигила C<!>
используется твигил C<.>. В то время как инкапсуляция атрибутов является в
Perl 6 полноценной, язык позволяет избавиться от необходимости явно создавать методы доступа к атрибутам из вне I<(accessor methods)>. Замена C<!> на
C<.> помимо определения атрибута C<$!done> определит метод доступа C<done>.
То есть результат будет тот же, как если вы написали бы следующий код:
=begin code
has Bool $!done;
method done() { return $!done }
=end code
Обратите внимание, что это отличается от определения публичного атрибута,
как это позволяют некоторые языки: и вы действительно получаете недоступную
снаружи переменную и метод, без необходимости писать этот метод вручную I<(просто заменив C<!> на C<.>)>. Подобный способ хорош пока вам не понадобится более сложные действия, чем просто возврат значения.
Стоит заметить что твигил C<.> создает метод с доступом к атрибуту только в режиме чтения. Чтобы пользователи этого объекта могли сбросить статус готовности задачи (для выполнения ее например повторно) можно изменить определение атрибута на следующее:
=begin code
has Bool $.done is rw;
=end code
X<traits, is rw>
Свойство C<is rw> приводит к генерации метода доступа c возможностью модифицировать значение атрибута.
=head1 Методы
X<|methods>
X<|classes, methods>
В то время как атрибуты определяют состояние объекта, методы определяют его поведение.
Пропустим временно специальный метод C<new> и рассмотрим второй метод C<add-dependency>, который добавляет новую задачу к списку зависимостей для текущей задачи.
=begin code
method add-dependency(Task $dependency) {
push @!dependencies, $dependency;
}
=end code
X<|invocant>
В большинстве случаев, определение метода похоже на определение C<sub>. Однако
имеется два важных отличия. Во первых, определение подпрограммы как метода
добавляет ее к списку методов текущего класса. Благодаря этому у любого экземпляра
класса C<Task> можно вызвать необходимый метод с помощью оператора вызова C<.>. Во вторых,
метод сохраняет своего инвоканта в специальной переменной C<self>.
Рассматриваемому метод передается один параметр, экземпляр класса C<Task>, который затем
добавляется к содержимом атрибута инвоканта C<@!dependencies>.
Следующий метод содержит основную логику обработки зависимостей:
=begin code
method perform() {
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
=end code
Он вызывается без параметров и использует атрибуты объекта. Сначала анализируется
атрибут C<$!done>, который является признаком выполненной задачи. Если этот атрибут
содержит значение "истина", то задача выполнена и никаких действий не производится.
X<|operators, .>
Иначе метод выполняет все зависимости для задачи, используя конструкцию C<for> для
поочередного обхода всех элементов в атрибуте C<@!dependencies>. Этот цикл во время
итерации размещает каждый элемент ( экземпляр класса C<Task> ) в topic переменной C<$_>.
При использовании оператора вызова метода C<.> без явного указания инвоканта используется
текущая topic переменная. Таким образом производится вызов метода C<.perform()> у каждого
объекта C<Task>, хранящихся в атрибуте C<@!dependencies> текущего инвоканта.
После того, как все зависимые задачи завершены, наступает время выполнить текущую
задачу C<Task>. Это производится с помощью прямого вызова атрибута C<&!callback>
( после атрибута указываются круглые скобки). В итоге атрибуту C<$!done> присваивается
значение C<True> ("Истина") и это гарантирует, что при последующем вызове метода
C<perform> этого объекта никаких повторных действий производится не будет.
=head1 Конструкторы
X<|constructors>
В отношении конструкторов Perl 6 является более либеральным, чем большинство языков программирования. Главное чтобы конструктор возвращал экземпляр класса. Кроме того, конструкторами
в Perl 6 являются обычные методы. Любой класс наследует конструктор с именем C<new> от базового класса C<Object>. Этот метод C<new> может быть переопределен, например, как в следующем коде:
=begin code
method new(&callback, Task *@dependencies) {
return self.bless(*, :&callback, :@dependencies);
}
=end code
X<|objects, bless>
X<|bless>
В то время, как конструкторы в языках подобных C# и Java устанавливают состояние уже предварительно созданных объектов, конструкторы в Perl 6 непосредственно создают объекты. Наиболее простой путь создать объект - это вызвать метод C<bless>, который наследуется от C<Object>. В качестве параметров метод C<bless> ожидает позиционный параметр, так называемого "кандидата", и набор именованных параметров с начальными
значениями для каждого из атрибутов объекта.
В конструкторе из примера позиционные параметры преобразуются в именованные. Благодаря этому конструктор оказывается лаконичным и простым для использования. Первый параметр конструктора C<new> представляет собой callback ( непосредственно действие, из которого состоит задача ). Остальными параметрами являются экземпляры класса C<Task>.
Конструктор захватывает их в массив и передает затем как именованный параметр в C<bless> ( заметьте, что для в форме именованного параметра C<:&callback> непосредственно имя параметра тоже что и имя переменной за вычетом сигила, т.е. C<callback> ).
=head1 Использование нашего класса
После того как класс создан, можно создавать его экземпляры. Благодаря нашему конструктору можно просто описать задачи и их зависимости. Для создания задачи без зависимостей достаточно использовать следующий код:
=begin code
my $eat = Task.new({ say 'eating dinner. NOM!' });
=end code
Ранее рассказывалось, что после определении класса C<Tast> появляется так называемый
объект-тип. Он является своеобразным "пустым экземпляром" класса, а именно экземпляром без определенного состояния. Для него возможно вызывать какие угодно методы, кроме тех которые пытаются получить доступ к состоянию объекта ( например к свойствам ). Так C<new>, например, создает новый объект, а не модифицирует или пытается прочесть состояние существующего объекта.
К сожалению, обеда не происходит волшебно неожиданно. Он имеет зависимые задачи:
=begin code
my $eat =
Task.new({ say 'eating dinner. NOM!' },
Task.new({ say 'making dinner' },
Task.new({ say 'buying food' },
Task.new({ say 'making some money' }),
Task.new({ say 'going to the store' })
),
Task.new({ say 'cleaning kitchen' })
)
);
=end code
Обратите внимание на уровни отступов. Такое форматирование позволяет сделать
нагляднее структуру зависимостей для задач.
Наконец, вызов метода C<perform> приводит к рекурсивному вызову методов C<perform>
для зависимых задач. В итоге на экран будет введен следующий результат:
making some money
going to the store
buying food
cleaning kitchen
making dinner
eating dinner. NOM!
=head1 Наследование
Объектно Ориентированное Программирование определяет концепцию наследования как
механизм для повторного использования кода. Perl 6 поддерживает как наследование
одного класса от другого, так и множественное наследование ( от нескольких классов ). Когда класс наследуется от другого, диспетчер методов следует по цепочке наследования, чтобы определить метод для вызова. Это поведение одинаково как для определенных стандартным способом, с помощью ключевого слова C<method>, так и для генерируемых методов для доступа к свойствам объекта I<(attribute accessors)>.
=begin code
class Employee {
has $.salary;
method pay() {
say "Here is \$$.salary";
}
}
class Programmer is Employee {
has @.known_languages is rw;
has $.favorite_editor;
method code_to_solve( $problem ) {
say "Solving $problem using $.favorite_editor in "
~ $.known_languages[0] ~ '.';
}
}
=end code
Теперь любой объект типа C<Programmer> может использовать методы и аксессоры ( методы для доступа к атрибутам ), определенные в классе C<Employe>, так словно они определены в классе C<Programmer>.
=begin code
my $programmer = Programmer.new(
salary => 100_000,
known_languages => <Perl5 Perl6 Erlang C++>,
favorite_edtor => 'vim'
);
$programmer.code_to_solve('halting problem');
$programmer.pay();
=end code
=head2 Переопределение унаследованных методов
Классы могут перекрывать методы и атрибуты родительских классов определяя свои собственные. Следующий пример демонстрирует как в классе C<Baker> переопределяется метод cook класса C<Cook>.
=begin code
class Cook is Employee {
has @.utensils is rw;
has @.cookbooks is rw;
method cook( $food ) {
say "Cooking $food";
}
method clean_utensils {
say "Cleaning $_" for @.utensils;
}
}
class Baker is Cook {
method cook( $confection ) {
say "Baking a tasty $confection";
}
}
my $cook = Cook.new(
utensils => (<spoon ladel knife pan>),
cookbooks => ('The Joy of Cooking'),
salary => 40000);
$cook.cook( 'pizza' ); # Cooking pizza
my $baker = Baker.new(
utensils => ('self cleaning oven'),
cookbooks => ("The Baker's Apprentice"),
salary => 50000);
$baker.cook('brioche'); # Baking a tasty brioche
=end code
Диспетчер методов в процессе определения метода для вызова останавливается на
методе C<cook> класса C<Baker> и прекращает дальнейший просмотр родительских классов.
=head2 Множественное наследование
Как было сказано ранее, класс может наследоваться от множества классов. В таких случаях диспетчер методов будет просматривать родительские классы в поисках метода для вызова. В Perl 6 используется алгоритм C3 для линеаризации иерархии множественного наследования, что дает значительное улучшение данной функциональности в сравнении с Perl 5.
=begin code
class GeekCook is Programmer is Cook {
method new( *%params ) {
push( %params<cookbooks>, "Cooking for Geeks" );
return self.bless(%params);
}
}
my $geek = GeekCook.new(
books => ('Learning Perl 6'),
utensils => ('blingless pot', 'knife', 'calibrated oven'),
favorite_editor => 'MacVim',
known_languages => <Perl6>
);
$geek.cook('pizza');
$geek.code_to_solve('P =? NP');
=end code
Теперь все методы из классов C<Programmer> и C<Cook> доступны в C<GeekCook>.
Множественное наследование является хорошей концепцией и полезно знать о такой возможности.
Понимание ее работы полезно при изучении других замечательных концепций OOП, таких как роли.
Подробнее о ролях речь пойдет в соответствующей главе.
=head1 Интроспекция
Интроспеция N<Интроспекция (англ. type /introspection/) в программировании -
возможность в некоторых объектно-ориентированных языках определить тип и
структуру объекта во время выполнения программы. Эта возможность
особенно заметна в языке Objective C, однако имеется во всех языках,
позволяющих манипулировать типами объектов как объектами первого класса.
Интроспекция может использоваться для реализации полиморфизма. В Java эта возможность называется рефлекцией> процесс сбора информации об объектах в программе во время ее выполнения.
Возьмем объект C<$o> класса C<Programmer> и, основываясь на содержимом классов из предыдущей секции, зададим несколько вопросов:
=begin code
if $o ~~ Employee { say "It's an employee" };
if $o ~~ GeekCook { say "It's a geeky cook" };
say $o.WHAT;
say $o.perl;
say $o.^methods(:local).join(', ');
=end code
Результат будет выглядеть следующим образом:
=begin code
It's an employee
Programmer()
Programmer.new(known_languages => ["Perl", "Python", "Pascal"], favorite_editor => "gvim", salary => "too small")
code_to_solve, known_languages, favorite_editor
=end code
Первые два теста представляют собой умное сопоставление (smart-match) объекта с именами классов. Если объект того же класса или наследуется от указанного, результатом является истина. Таким образом образом C<$o> является объектом класса C<Employee> или унаследованным от него, но не C<GeekCook>.
Метод C<.WHAT> возвращает объект-тип, ассоциированный с объектом C<$o>, который сообщает точный класс. В данном примере - C<Programmer>.
C<$o.perl> возвращает исполняемую строку кода Perl, которая воссоздает оригинальный объект C<$o>. Данный код в основном не является работающим N<например замыкания таким образом не могут быть воспроизведены. Если вы не знаете что такое замыкания не волнуйтесь. В существующей на сегодня реализации есть проблемы с отображением цикличных структур, но в ближайшем они будут решены.>, но очень полезен для отладки простых объектов.
Наконец, C<$o.^methods(:local)> выводит список доступных для вызовов методов объекта C<$o>. Именованный параметр C<:local> ограничивает этот список методами определенными в классе C<Employee> и исключает унаследованные.
Синтаксис вызова метода с C<.^> вместо одиночной точки подразумевает, что метод на самом деле вызывается у I<мета класса>, управляющего свойствами класса C<Employee> или любого другого. Этот мета класс предоставляет дополнительные способы интроспекции:
=begin code
say $o.^attributes.join(', ');
say $o.^parents.join(', ');
=end code
Интроспекция полезна при отладке, а также при изучении языка или новых библиотек. В случаях когда требуется установить тип возвращаемого функцией объекта используется C<.WHAT>, а также код воссоздания, возвращаемый C<.perl>. В дополнение результат C<^.methods> расскажет, что вы можете делать с объектом.
Но есть другие применения интроспекции. Например, процедурам сериализации объектов необходима знать об атрибутах объекта.
=head1 Упражнения
B<1.> Метод C<add-dependency> в классе C<Task> позволяет создавать циклические зависимости
в графе зависимостей между задачами. Это значит, что если вы будете продвигаться по ссылкам между задачами, то вернетесь к исходной. Таким образом образуется бесконечный цикл и метод C<perform> класса C<Task> никогда не завершиться. Покажите как создать такой граф.
B<Ответ:> Вы можете создать две задачи, а затем "замкнуть" их с помощью метода C<add-dependency> следующим образом:
=begin code
my $a = Task.new({ say 'A' });
my $b = Task.new({ say 'B' }, $a);
$a.add-dependency($b);
=end code
Метод C<perform> никогда не завершиться, потому что он первым делом будет вызывать C<perlform> своих зависимостей. А так как C<$a> и C<$b> зависят друг от друга, никто из них не завершить вызовы callbacks. Программа завершится вследствие нехватки памяти, так и не напечатая
на экране C<'A'> or C<'B'>.
B<2.> Есть ли способ определить наличие цикла во время вызова C<perform>? Есть ли способ предотвратить появление такого цикла с помощью C<add-dependency>?
B<Answer:> Чтобы обнаружить повторный вызов метода C<perform> во время его выполнения, достаточно дополнить состояние объекта статусом старта вызова метода C<perform>:
=begin code
augment class Task {
has Bool $!started = False;
method perform() {
if $!started++ && !$!done {
die "Cycle detected, aborting";
}
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
}
=end code
Еще один способ избежать циклических вызовов - это во время вызова метода C<add-dependency> проверять добавляемые объекты на предмет присутствия их в зависимостях уже добавленных объектов ( собственно это и есть причина циклических вызовов). Данное решение потребует создание вспомогательного метода C<depends-on>, который проверяет находится ли указанная задача в зависимостях напрямую или транзитивно.
Обратите внимание как используются C<»> и C<[||]>, чтобы код обхода зависимостей получился эффективным и лаконичным:
=begin code
augment class Task {
method depends-on(Task $some-task) {
$some-task === any(@!dependencies)
[||] @!dependencies».depends-on($some-task)
}
method add-dependency(Task $dependency) {
if $dependency.depends-on(self) {
warn 'Cannot add that task, since it would introduce a cycle.';
return;
}
push @!dependencies, $dependency;
}
}
=end code
B<3.> Каким образом объект C<Task> может выполнит зависимые задачи параллельно ?
(Подумайте как можно избежать коллизий в "бриллиантовых зависимостях" I<(Пер. - не встречал ранее такого выражения.)>), когда две разных зависимых задачи требуют выполнения одной).
B<Ответ:> Включение параллельного выполнения просто: достаточно заменить вызов метода C<.perform()> для C<@!dependencies;> на C<@!dependencies».perform()>. Однако в таком случае могут возникнуть "гонки" I(race conditions) в случае наличия бриллиантовых зависимостей, когда задача C<A> запускает одновременно C<B> и C<C>, а они в свою очередь запускают C<D> (C<D> запускается дважды). Решение этой проблемы такое же как и в случае с циклическими вызовами в вопросе 2 - ввести атрибут C<$!started>. Заметьте, что в случае параллельного выполнения, вызов die в одной из задач может прервать исполнение других.
=begin code
augment class Task {
has Bool $!started = False;
method perform() {
unless $!started++ {
@!dependencies».perform();
&!callback();
$!done = True;
}
}
}
=end code
=end pod
Jump to Line
Something went wrong with that request. Please try again.