/
classes-and-objects.pod6
570 lines (392 loc) · 33.6 KB
/
classes-and-objects.pod6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
=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