-
Notifications
You must be signed in to change notification settings - Fork 8
/
chapter-17.tex
917 lines (783 loc) · 81.2 KB
/
chapter-17.tex
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
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
\chapter{Переходим к объектам: классы}
\label{ch:17}
\thispagestyle{empty}
Если обобщённые функции являются глаголами объектной системы, то классы являются
существительными. Как я упоминал в предыдущей главе, все значения в программах на Common
Lisp являются экземплярами какого-то из классов. Более того, все классы образуют иерархию,
на вершине которой находится класс \lstinline{T}.
Иерархия классов состоит из двух основных семейств классов: встроенных и определённых
пользователем. Классы, которые представляют типы данных, которые мы изучали до сих
пор,~-- такие как \lstinline{INTEGER}, \lstinline{STRING} и \lstinline{LIST}, являются встроенными. Они
находятся в отдельном разделе иерархии классов, организованные соответствующими связями
дочерних и родительских классов, и для работы с ними используются функции, которые я
описывал на протяжении всей книги. Вы не можете унаследовать от этих классов, но, как вы
увидели в предыдущем разделе, вы можете определить спе\-циа\-ли\-зи\-ро\-ван\-ные методы для них,
эффективно расширяя поведение этих классов\pclfootnote{Определение новых методов для
существующих классов может показаться странным для людей, которые использовали
статически типизированные языки, такие как C++ и Java, в которых все методы классов
должны быть определены как часть определения класса. А вот программисты, которые
имеют опыт программирования на Smalltalk и Objective C, не найдут в этой
функциональности ничего странного.}.
Но когда вы создаёте новые существительные~-- например, классы, которые использовались в
предыдущей главе для представления банковских счетов, то вам нужно определить ваши
собственные классы. Это и будет темой данной главы.
\section{DEFCLASS}
Вы можете создать собственный класс с помощью макроса \lstinline{DEFCLASS}. Поскольку
поведение класса определяется обобщёнными функциями и методами, специализированными для
класса, то \lstinline{DEFCLASS} отвечает только за определение класса как типа данных.
Класс как тип данных состоит из трёх частей: имени, отношения к другим классам и имен
слотов\pclfootnote{В~других объектно-ориентированных языках слоты могут называться полями,
переменными-членами класса или атрибутами.}. Базовая форма \lstinline{DEFCLASS} выглядит
достаточно просто.
\begin{myverb}
(defclass name (direct-superclass-name*)
(slot-specifier*))
\end{myverb}
Что такое классы, определённые пользователем?
<<Определённые пользователем классы>>~-- термин не из стандарта языка. Определёнными
пользователем классами я называю подклассы класса \lstinline{STANDARD-OBJECT}, а также
классы, у которых метакласс~-- \lstinline{STANDARD-CLASS}. Но поскольку я не собираюсь
говорить о способах определения классов, которые не наследуют \lstinline{STANDARD-OBJECT}
и чей метакласс~-- это не \lstinline{STANDARD-CLASS}, вам можно не обращать на это
внимания. Определённые пользователем~-- неидеальный термин, потому что реализация может
определять некоторые классы таким же способом. Но ещё большей путаницей будет называть эти
классы стандартными, поскольку встроенные классы (например, \lstinline{INTEGER} и
\lstinline{STRING}) тоже стандартные, если не сказать больше, потому что они определены
стандартом языка, но они не расширяют (не наследуют) \lstinline{STANDARD-OBJECT}. Чтобы
ещё больше запутать дело, пользователь может также определять классы, не наследующие
\lstinline{STANDARD-OBJECT}. В~частности, макрос \lstinline{DEFSTRUCT} тоже определяет
новые классы. Но это во многом для обратной совместимости~-- \lstinline{DEFSTRUCT}
появился раньше, чем CLOS, и был изменён, чтобы определять классы, когда CLOS добавлялся в
язык. Но создаваемые им классы достаточно ограничены, по сравнению с классами, созданными с
помощью \lstinline{DEFCLASS}. Итак, я буду обсуждать только классы, создаваемые с помощью
\lstinline{DEFCLASS}, которые используют заданный по умолчанию метакласс
\lstinline{STANDARD-CLASS}, и, за неимением лучшего термина, назову их <<определёнными
пользователем классами>>.
Так же как с функциями и переменными, вы можете использовать в качестве имени класса любой
символ\pclfootnote{Так же как и при именовании функции и переменных, это не совсем правда,
что вы можете использовать для класса любое имя,~-- вы не можете использовать имена,
определённые стандартом. В~главе~\ref{ch:21} вы увидите, как можно избежать таких конфликтов
имён.}. Имена классов находятся в собственном пространстве имён, отдельно от имен
функций и переменных, так что вы можете задать для класса то же самое имя, что и
существующие функция и переменная. Вы будете использовать имя класса в качестве аргумента
функции \lstinline{MAKE-INSTANCE}, которая создаёт новые экземпляры классов, определённых
пользователем.
Опция \lstinline{direct-superclass-names} используется для указания имён классов, от которых
будет проводиться наследование данного класса. Если ни одного класса не указано, то он
будет унаследован от \lstinline{STANDARD-OBJECT}. Все классы, указанные в данной опции, должны
быть классами, определёнными пользователем, чтобы быть увереным, что каждый новый класс
происходит от \lstinline{STANDARD-OBJECT}. \lstinline{STANDARD-OBJECT} является подклассом
\lstinline{T}, так что все классы, определённые пользователем, являются частью одной иерархии
классов, которая также содержит все встроенные классы.
На время отвлекаясь от упоминания спецификаторов слотов, запись \lstinline{DEFCLASS} для
некоторых из классов, которые мы использовали в предыдущей главе, может выглядеть
следующим образом:
\begin{myverb}
(defclass bank-account () ...)
(defclass checking-account (bank-account) ...)
(defclass savings-account (bank-account) ...)
\end{myverb}
В~разделе~\ref{sec:17-multi-inheritance} я объясню, что означает указание более чем одного
суперкласса в списке опции \lstinline{direct-superclass-names}.
\section{Спецификаторы слотов}
Большая часть \lstinline{DEFCLASS} состоит из списка спецификаторов слотов. Каждый
спецификатор определяет слот, который будет частью экземпляра класса. Каждый слот в
экземпляре является местом, которое может хранить значение, к которому можно получить
доступ через функцию \lstinline{SLOT-VALUE}. \lstinline{SLOT-VALUE} в качестве аргументов принимает
объект и имя слота и возвращает значение нужного слота в данном объекте. Эта функция
может использоваться вместе с \lstinline{SETF} для установки значений слота в объекте.
Класс также наследует спецификаторы слотов от своих суперклассов, так что набор слотов,
присутствующих в любом объекте, является объединением всех слотов, указанных в форме
\lstinline{DEFCLASS} для класса, а также указанных для всех его суперклассов.
По минимуму спецификатор слота указывает его имя, так что спецификатор может быть простым
именем. Например, вы можете определить класс \lstinline{bank-account} с двумя слотами~--
\lstinline{customer-name} и \lstinline{balance}, например вот так:
\begin{myverb}
(defclass bank-account ()
(customer-name
balance))
\end{myverb}
Каждый экземпляр этого класса содержит два слота: один для хранения имени клиента, а
второй~-- для хранения текущего баланса счёта. Используя данное определение, вы можете
создать новые объекты \lstinline{bank-account} с помощью \lstinline{MAKE-INSTANCE}.
\begin{myverb}
(make-instance 'bank-account) ==> #<BANK-ACCOUNT @ #x724b93ba>
\end{myverb}
Аргументом \lstinline{MAKE-INSTANCE} является имя класса, а возвращаемым значением~-- новый
объект\footnote{В~действительности аргументом \lstinline{MAKE-INSTANCE} может быть либо имя
класса, либо объект класса, возвращаемый функциями \lstinline{CLASS-OF} или
\lstinline{FIND-CLASS}.}\hspace{\footnotenegspace}. Печатное представление объекта определяется обобщённой функцией
\lstinline{PRINT-OBJECT}. В~этом случае подходящим методом будет тот, который предоставляется
реализацией и является специализированным для \lstinline{STANDARD-OBJECT}. Поскольку не каждый объект
может быть выведен таким образом, чтобы потом быть считанным назад, то метод печати для
\lstinline{STANDARD-OBJECT} использует синтаксис \lstinline!#<>!, который заставит процедуру
чтения выдать ошибку, если она попытается прочитать его. Оставшаяся часть представления
зависит от реализации, но обычно оно похоже на результат, приведённый выше, включая имя
класса и некоторое значение, например адрес объекта в памяти. В~главе~\ref{ch:23} вы
увидите пример того, как определить метод для \lstinline{PRINT-OBJECT}, чтобы некоторые классы
можно было вывести в более информативной форме.
Используя данное определение \lstinline{bank-account}, новые объекты будут создаваться со
слотами, которые не связаны со значениями. Любая попытка получить значение для
несвязанного значения приведёт к выдаче ошибки, так что вы должны задать значение до того,
как будете считывать значения.
\begin{myverb}
(defparameter *account* (make-instance 'bank-account)) ==> *ACCOUNT*
(setf (slot-value *account* 'customer-name) "John Doe") ==> "John Doe"
(setf (slot-value *account* 'balance) 1000) ==> 1000
\end{myverb}
Теперь вы можете получать значения слотов.
\begin{myverb}
(slot-value *account* 'customer-name) ==> "John Doe"
(slot-value *account* 'balance) ==> 1000
\end{myverb}
\section{Инициализация объекта}
Поскольку мы мало что можем сделать с объектом, который имеет пустые слоты, было бы хорошо
иметь возможность создавать объекты с инициализированными слотами. Common Lisp
предоставляет три способа управления начальными значениями слотов. Первые два требуют
добавления опций в спецификаторы слотов в \lstinline{DEFCLASS}: с помощью опции
\lstinline{:initarg} вы можете указать имя, которое потом будет использоваться как
именованный параметр при вызове \lstinline{MAKE-INSTANCE}, и переданное значение будет
сохранено в слоте. Вторая опция~-- \lstinline{:initform}~-- позволяет вам указать выражение
на Lisp, которое будет использоваться для вычисления значения, если при вызове
\lstinline{MAKE-INSTANCE} не был передан аргумент \lstinline{:initarg}. В~заключение
для полного контроля за инициализацией объекта вы можете определить метод для обобщённой
функции \lstinline{INITIALIZE-INSTANCE}, которую вызывает
\lstinline{MAKE-INSTANCE}\footnote{Другим способом установки значений слотов является
использование опции \lstinline{:default-initargs} при объявлении \lstinline{DEFCLASS}.
Эта опция используется для указания выражений, которые будут вычислены для нахождения
аргументов для отдельных параметров инициализации, которые не получили значение при
вызове \lstinline{MAKE-INSTANCE}. В~текущий момент времени вам не нужно беспокоиться о
\lstinline{:default-initargs}.}\hspace{\footnotenegspace}.
Спецификатор слота, который включает опции, такие как \lstinline{:initarg} или
\lstinline{:initform}, записывается как список, начинающийся с имени слота, за которым следуют
опции. Например, если вы измените определение \lstinline{bank-account} таким образом, чтобы
позволить передавать имя клиента и начальный баланс при вызове \lstinline{MAKE-INSTANCE}, а
также чтобы установить для баланса начальное значение, равное нулю, вы должны написать:
\begin{myverb}
(defclass bank-account ()
((customer-name
:initarg :customer-name)
(balance
:initarg :balance
:initform 0)))
\end{myverb}
Теперь вы можете одновременно создавать счёт и указывать значения слотов.
\begin{myverb}
(defparameter *account*
(make-instance 'bank-account :customer-name "John Doe" :balance 1000))
(slot-value *account* 'customer-name) ==> "John Doe"
(slot-value *account* 'balance) ==> 1000
\end{myverb}
Если вы не передадите аргумент \lstinline{:balance} при вызове \lstinline{MAKE-INSTANCE}, то вызов
\lstinline{SLOT-VALUE} для слота \lstinline{balance} будет получен вычислением формы, указанной
опцией \lstinline{:initform}. Но если вы не передадите аргумент \lstinline{:customer-name}, то
слот \lstinline{customer-name} будет пустой, и попытка считывания значения из него приведёт к
выдаче ошибки.
\begin{myverb}
(slot-value (make-instance 'bank-account) 'balance) ==> 0
(slot-value (make-instance 'bank-account) 'customer-name) ==> Ошибка (error)
\end{myverb}
Если вы хотите убедиться, что имя клиента было задано при создании счёта, то вы можете
выдать ошибку в начальном выражении (\lstinline{initform}), поскольку оно будет вычислено,
только если начальное значение (\lstinline{initarg}) не было задано. Вы также можете
использовать начальные формы, которые создают разные значения при каждом запуске~--
начальное выражение вычисляется заново для каждого объекта. Для эксперементирования с
этими возможностями вы можете изменить спецификатор слота \lstinline{customer-name} и добавить
новый слот, \lstinline{account-number}, который инициализируется значением увеличивающегося
счётчика.
\begin{myverb}
(defvar *account-numbers* 0)
(defclass bank-account ()
((customer-name
:initarg :customer-name
:initform (error "Must supply a customer name."))
(balance
:initarg :balance
:initform 0)
(account-number
:initform (incf *account-numbers*))))
\end{myverb}
В~большинстве случаев комбинации опций \lstinline{:initarg} и \lstinline{:initform} будет
достаточно для нормальной инициализации объекта. Однако, хотя начальное выражение может
быть любым выражением Lisp, оно не имеет доступа к инициализируемому объекту, так что оно
не может инициализировать один слот, основываясь на значении другого. Для выполнения
такой задачи вам необходимо определить метод для обобщённой функции
\lstinline{INITIALIZE-INSTANCE}.
Основной метод \lstinline{INITIALIZE-INSTANCE}, специализированный для \lstinline{STANDARD-OBJECT},
берёт на себя заботу об инициализации слотов, основываясь на данных, заданных опциями
\lstinline{:initarg} и \lstinline{:initform}. Поскольку вы не захотите вмешиваться в этот процесс,
то наиболее широко применяемым способом является определение метода \lstinline{:after},
специализированного для вашего класса\footnote{Добавление метода \lstinline{:after} к
\lstinline{INITIALIZE-INSTANCE} является аналогом на Common Lisp определению конструктора в
Java или C++ или методу \lstinline!__init__! в Python.}\hspace{\footnotenegspace}. Например, предположим, что вы
хотите добавить слот \lstinline{account-type}, который должен быть установлен в значение
\lstinline{:gold}, \lstinline{:silver} или \lstinline{:bronze}, основываясь на начальном балансе счёта.
Вы можете изменить определение класса на следующее, добавляя слот \lstinline{account-type} без
каких-либо опций:
\begin{myverb}
(defclass bank-account ()
((customer-name
:initarg :customer-name
:initform (error "Must supply a customer name."))
(balance
:initarg :balance
:initform 0)
(account-number
:initform (incf *account-numbers*))
account-type))
\end{myverb}
После этого вы можете определить метод \lstinline{:after} для \lstinline{INITIALIZE-INSTANCE},
который установит значение слота \lstinline{account-type}, основываясь на значении, которое
было сохранено в слоте \lstinline{balance}\footnote{Одна из ошибок, которую вы могли сделать
до того, как освоились со вспомогательными методами, заключается в определении метода для
\lstinline{INITIALIZE-INSTANCE}, но без квалификатора \lstinline{:after}. Если вы сделаете это,
то получите новый основной метод, который скроет метод, вызываемый по умолчанию. Вы
можете удалить ненужный основной метод с помощью функций \lstinline{REMOVE-METHOD} и
\lstinline{FIND-METHOD}. Некоторые среды разработки могут предоставлять графический
интерфейс для выполнения данной задачи.
\begin{myverb}
(remove-method #'initialize-instance
(find-method #'initialize-instance () (list (find-class 'bank-account)))
\end{myverb}
}\hspace{\footnotenegspace}.
\begin{myverb}
(defmethod initialize-instance :after ((account bank-account) &key)
(let ((balance (slot-value account 'balance)))
(setf (slot-value account 'account-type)
(cond
((>= balance 100000) :gold)
((>= balance 50000) :silver)
(t :bronze)))))
\end{myverb}
Указание \lstinline!&key! в списке параметров требуется обязательно, чтобы сохранить
список параметров соответствующим списку параметров обобщённой функции~-- список
параметров, указанный для функции \lstinline{INITIALIZE-INSTANCE}, включает \lstinline!&key!,
чтобы позволить отдельным методам передавать собственные именованные параметры, но при
этом он не требует указания конкретных названий. Таким образом, каждый метод должен
указывать \lstinline!&key!, даже если он не указывает ни одного именованного параметра.
С другой стороны, если метод \lstinline{INITIALIZE-INSTANCE}, специализированный для
конкретного класса, указывает именованный параметр, то этот параметр становится допустимым
параметром для функции \lstinline{MAKE-INSTANCE} при создании экземпляра данного класса.
Например, если банк иногда платит процент начального баланса в качестве премии при
открытии счёта, то вы можете реализовать эту функцию, используя метод
\lstinline{INITIALIZE-INSTANCE}, который получает именованный аргумент, указывающий процент
премии, например вот так:
\begin{myverb}
(defmethod initialize-instance :after ((account bank-account)
&key opening-bonus-percentage)
(when opening-bonus-percentage
(incf (slot-value account 'balance)
(* (slot-value account 'balance) (/ opening-bonus-percentage 100)))))
\end{myverb}
Путём определения метода \lstinline{INITIALIZE-INSTANCE} вы делаете
\lstinline{:opening-bonus-percentage} допустимым аргументом функции \lstinline{MAKE-INSTANCE} при
создании объекта \lstinline{bank-account}.
\begin{myverb}
CL-USER> (defparameter *acct* (make-instance
'bank-account
:customer-name "Sally Sue"
:balance 1000
:opening-bonus-percentage 5))
*ACCT*
CL-USER> (slot-value *acct* 'balance)
1050
\end{myverb}
\section{Функции доступа}
\lstinline{MAKE-INSTANCE} и \lstinline{SLOT-VALUE} дают вам возможности для создания и работы с
эк\-земпля\-ра\-ми ваших классов. Все остальные операции могут быть реализованы в терминах этих
двух функций. Однако, как знает всякий, знакомый с принципами правильного
объектно-ориентированного программирования, прямой доступ к слотам (полям или
переменным-членам) объекта может привести к получению уязвимого кода. Проблема
заключается в том, что прямой доступ к слотам делает ваш код слишком связанным с
конкретной структурой классов. Например, предположим, что вы решили изменить определение
\lstinline{bank-account} таким образом, что вместо хранения текущего баланса в виде числа вы
храните его в виде списка списаний и помещений денег на счёт, вместе с датами этих
операций. Код, который имеет прямой доступ к слоту \lstinline{balance}, скорее всего, будет
сломан, если вы измените определение класса, удалив слот или храня список в данном
слоте. С другой стороны, если вы определите функцию \lstinline{balance}, которая осуществляет
доступ к слоту, то вы можете позже переопределить её, чтобы сохранить её поведение, даже
если изменится внутреннее представление данных. И код, который использует такую функцию,
будет продолжать нормально работать, не требуя внесения изменений.
Другим преимуществом использования функций доступа вместо прямого доступа к слотам через
\lstinline{SLOT-VALUE} является то, что их применение позволяет вам ограничить возможность
внешней модификации слота\footnote{Конечно, предоставление функции доступа в
действительности не ограничивает ничего, поскольку сторонний код все равно может
использовать \lstinline{SLOT-VALUE} для прямого доступа к слотам. Common Lisp не
предоставляет строгой инкапсуляции слотов, как это делают C++ и Java; однако если автор
класса предоставляет функции доступа и вы игнорируете их, вместо этого используя
\lstinline{SLOT-VALUE}, то вы должны лучше знать, что вы делаете. Кроме этого, имеется
возможность использования пакетной системы, которую я буду обсуждать в
главе~\ref{ch:21}, чтобы ограничить прямой доступ к некоторым слотам путём отсутствия
экспорта имён слотов.}\hspace{\footnotenegspace}. Для пользователей класса \lstinline{bank-account} может быть удобным
использование функций доступа для получения текущего баланса, но вы можете захотеть, чтобы
все изменения баланса производились через другие предоставляемые вами функции, такие как
\lstinline{deposit} и \lstinline{withdraw}. Если клиент знает, что он сможет работать с объектами
только через определённый набор функций, то вы можете предоставить ему функцию
\lstinline{balance}, но сделать так, чтобы для неё нельзя было выполнить \lstinline{SETF}, чтобы
баланс был доступен только для чтения.
В~заключение использование функций доступа делает ваш код более аккуратным, поскольку вы
избегаете использования множества менее понятных функций \lstinline{SLOT-VALUE}.
Определение функции, которая читает содержимое слота \lstinline{balance}, является тривиальным.
\begin{myverb}
(defun balance (account)
(slot-value account 'balance))
\end{myverb}
Однако если вы знаете, что будете определять подклассы для \lstinline{bank-account}, то
может быть хорошей идеей определение \lstinline{balance} в качестве обобщённой функции. Таким
образом вы можете определить разные методы для \lstinline{balance} для некоторых подклассов
или расширить её возможности с помощью вспомогательных методов. Так что вместо предыдущего
примера можете написать следующее:
\begin{myverb}
(defgeneric balance (account))
(defmethod balance ((account bank-account))
(slot-value account 'balance))
\end{myverb}
Как я только что обсуждал, вы не хотите, чтобы пользователь имел возможность устанавливать
баланс напрямую, но для других слотов, таких как \lstinline{customer-name}, вы также можете
захотеть предоставить функцию для установки их значений. Наиболее понятным способом будет
определение такой функции как \lstinline{SETF}-функции.
\lstinline{SETF}-функция является способом расширения функциональности \lstinline{SETF}, определяя
новый вид места (place), для которого известно, как устанавливать его значение. Имя
\lstinline{SETF}-функции является списком из двух элементов, где первый элемент является
символом \lstinline{setf}, а второй~-- другим символом, обычно именем функции, которая
используется для доступа к месту, значение которого будет устанавливать функция
\lstinline{SETF}. \lstinline{SETF}-функция может получать любое количество аргументов, но первым
аргументом всегда является значение, присваиваемое выбранному месту\footnote{Одним из
следствий определения \lstinline{SETF}-функции (например, \lstinline{(setf foo)}) является то, что
если вы также определяете соответствующую функцию доступа, в нашем случае это
\lstinline{foo}, то вы можете использовать все макросы, изменяющие значения, которые построены
на основе \lstinline{SETF}, такие как \lstinline{INCF}, \lstinline{DECF}, \lstinline{PUSH} и \lstinline{POP}, для
нового вида места.}\hspace{\footnotenegspace}. Например, вы можете определить \lstinline{SETF}-функцию для установки
значения слота \lstinline{customer-name} в классе \lstinline{bank-account} следующим образом:
\begin{myverb}
(defun (setf customer-name) (name account)
(setf (slot-value account 'customer-name) name))
\end{myverb}
После вычисления этой функции выражения, подобные
\begin{myverb}
(setf (customer-name my-account) "Sally Sue")
\end{myverb}
\noindent{}будут компилироваться в вызов \lstinline{SETF}-функции, которую вы только что определили со
значением <<Sally Sue>> в качестве первого аргумента и значением \lstinline{my-account} в
качестве второго аргумента.
Конечно, так же как с функциями чтения, вы,вероятно, захотите, чтобы ваша
\lstinline{SETF}-функция была обобщённой, так что вы должны её определить примерно так:
\begin{myverb}
(defgeneric (setf customer-name) (value account))
(defmethod (setf customer-name) (value (account bank-account))
(setf (slot-value account 'customer-name) value))
\end{myverb}
И конечно, вы также можете определить функцию чтения для \lstinline{customer-name}.
\begin{myverb}
(defgeneric customer-name (account))
(defmethod customer-name ((account bank-account))
(slot-value account 'customer-name))
\end{myverb}
Это позволит вам писать следующим образом:
\begin{myverb}
(setf (customer-name *account*) "Sally Sue") ==> "Sally Sue"
(customer-name *account*) ==> "Sally Sue"
\end{myverb}
Нет ничего сложного в написании этих функций доступа, но написание подобных функций вручную
просто не соответствует The Lisp Way. Так что \lstinline{DEFCLASS} поддерживает три опции для
слотов, которые позволяют вам автоматически создавать функции чтения и записи значений
отдельных слотов.
Опция \lstinline{:reader} указывает имя, которое будет использоваться как имя обобщённой
функции, которая принимает объект в качестве своего единственного аргумента. Когда
вычисляется \lstinline{DEFCLASS}, то создаётся соответствующая обобщённая функция (если
она ещё конечно не определена). После этого для данной обобщённой функции создаётся
метод, специализированный для нового класса и возвращающий значение слота. Имя функции
может быть любым, но обычно используют то же самое имя, что и имя самого слота. Так что
вместо явного задания обобщённой функции \lstinline{balance} и метода для неё, как это
было показано раньше, вы можете просто изменить спецификатор слота \lstinline{balance} в
определении класса \lstinline{bank-account} на следующее:
\begin{myverb}
(balance
:initarg :balance
:initform 0
:reader balance)
\end{myverb}
Опция \lstinline{:writer} используется для создания обобщённой функции и метода для установки
значения слота. Создаваемая функция и метод следуют требованиям для \lstinline{SETF}-функции,
получая новое значение как первый аргумент и возвращая его в качестве результата, так что
вы можете определить \lstinline{SETF}-функцию, задавая имя, такое как \lstinline{(setf customer-name)}.
Например, вы можете определить методы чтения и записи для слота
\lstinline{customer-name}, просто изменяя спецификатор слота на следующее определение:
\begin{myverb}
(customer-name
:initarg :customer-name
:initform (error "Must supply a customer name.")
:reader customer-name
:writer (setf customer-name))
\end{myverb}
Поскольку достаточно часто требуется определение обеих функций доступа, то \lstinline{DEFCLASS}
также имеет опцию \lstinline{:accessor}, которая создаёт и функцию чтения, и соответствующую
\lstinline{SETF}-функцию. Так что вместо предыдущего примера можно написать следующим образом:
\begin{myverb}
(customer-name
:initarg :customer-name
:initform (error "Must supply a customer name.")
:accessor customer-name)
\end{myverb}
В~заключение опишу ещё одну опцию, о которой вы должны знать: опция \lstinline{:documentation}
позволяет вам задать строку, которая описывает данный слот. Собирая все в кучу и добавляя
методы чтения для слотов \lstinline{account-number} и \lstinline{account-type}, определение
\lstinline{DEFCLASS} для класса \lstinline{bank-account} будет выглядеть примерно так:
\begin{myverb}
(defclass bank-account ()
((customer-name
:initarg :customer-name
:initform (error "Must supply a customer name.")
:accessor customer-name
:documentation "Customer's name")
(balance
:initarg :balance
:initform 0
:reader balance
:documentation "Current account balance")
(account-number
:initform (incf *account-numbers*)
:reader account-number
:documentation "Account number, unique within a bank.")
(account-type
:reader account-type
:documentation "Type of account, one of :gold, :silver, or :bronze.")))
\end{myverb}
\section{\texttt{WITH-SLOTS} и \texttt{WITH-ACCESSORS}}
В~то время как функции доступа делают ваш код более лёгким для сопровождения, они все ещё
достаточно многословны. И конечно, будут моменты, когда вы будете писать методы, которые
реализуют низкоуровневое поведение класса, так что вы можете осознанно осуществлять доступ
к слотам для установки значений слотов, для которых нет функций записи, или для получения
значений из слотов без использования функций чтения.
Это как раз тот случай, для которого и предназначен макрос \lstinline{SLOT-VALUE}; однако он
также достаточно многословен. Если функция или метод осуществляет доступ к одному и тому
же слоту несколько раз, то исходный код будет засорён вызовами функций доступа и
\lstinline{SLOT-VALUE}. Например, даже достаточно простой метод, такой как следующий пример,
который вычисляет пеню для \lstinline{bank-account}, если баланс снижается ниже некоторого
минимума, будет засорён вызовами \lstinline{balance} и \lstinline{SLOT-VALUE}:
\begin{myverb}
(defmethod assess-low-balance-penalty ((account bank-account))
(when (< (balance account) *minimum-balance*)
(decf (slot-value account 'balance) (* (balance account) .01))))
\end{myverb}
И если вы решите, что хотите осуществлять прямой доступ к слоту, для того чтобы
избежать вызова вспомогательных методов, то ваш код будет ещё больше замусоренным.
\begin{myverb}
(defmethod assess-low-balance-penalty ((account bank-account))
(when (< (slot-value account 'balance) *minimum-balance*)
(decf (slot-value account 'balance) (* (slot-value account 'balance) .01))))
\end{myverb}
Два стандартных макроса~-- \lstinline{WITH-SLOTS} и \lstinline{WITH-ACCESSORS}~-- могут помочь
избавиться от этого мусора. Оба макроса создают блок кода, в котором могут использоваться
простые имена переменных для обращения к слотам определённого объекта. \lstinline{WITH-SLOTS}
предоставляет прямой доступ к слотам, так же как при использовании \lstinline{SLOT-VALUE}, в то
время как \lstinline{WITH-ACCESSORS} предоставляет сокращённый способ вызова функций доступа.
Базовая форма \lstinline{WITH-SLOTS} выглядит следующим образом:
\begin{myverb}
(with-slots (slot*) instance-form
body-form*)
\end{myverb}
Каждый элемент списка \lstinline{slot} может быть либо именем слота, которое также является
именем переменной, либо списком из двух элементов, где первый аргумент является именем,
которое будет использоваться как переменная, а второй~-- именем соответствующего слота.
Выражение \lstinline{instance-form} вычисляется один раз для получения объекта, к слотам
которого будет производиться доступ. Внутри тела макроса каждое вхождение имени
переменной преобразуется в вызов \lstinline{SLOT-VALUE} с использованием объекта и имени слота
в качестве аргументов\footnote{Имена <<переменных>>, предоставляемые \lstinline{WITH-SLOTS} и
\lstinline{WITH-ACCESSORS}, не являются настоящими переменными; они реализуются специальным
видом макросов, называемых символьными макросами, которые позволяют простому имени
преобразовываться в произвольный код. Символьные макросы были введены в язык для
поддержки \lstinline{WITH-SLOTS} и \lstinline{WITH-ACCESSORS}, но вы также можете использовать их
для своих целей. Я их более подробно опишу в главе~\ref{ch:20}.}\hspace{\footnotenegspace}. Таким образом, вы можете
переписать \lstinline{assess-low-balance-penalty} вот так:
\begin{myverb}
(defmethod assess-low-balance-penalty ((account bank-account))
(with-slots (balance) account
(when (< balance *minimum-balance*)
(decf balance (* balance .01)))))
\end{myverb}
\noindent{}или используя списочную запись, вот так:
\begin{myverb}
(defmethod assess-low-balance-penalty ((account bank-account))
(with-slots ((bal balance)) account
(when (< bal *minimum-balance*)
(decf bal (* bal .01)))))
\end{myverb}
Если вы определили \lstinline{balance} с использованием опции \lstinline{:accessor}, а не
\lstinline{:reader}, то вы также можете использовать макрос \lstinline{WITH-ACCESSORS}. Форма
\lstinline{WITH-ACCESSORS}, такая же как \lstinline{WITH-SLOTS}, за тем исключением, что каждый
элемент списка слотов является списком из двух элементов, содержащих имя переменной и имя
функции доступа. Внутри тела \lstinline{WITH-ACCESSORS} ссылка на одну из переменных
аналогична вызову соответствующей функции доступа. Если функция доступа разрешает
выполнение \lstinline{SETF}, то то же самое возможно и для переменной.
\begin{myverb}
(defmethod assess-low-balance-penalty ((account bank-account))
(with-accessors ((balance balance)) account
(when (< balance *minimum-balance*)
(decf balance (* balance .01)))))
\end{myverb}
Первое вхождение \lstinline{balance} является именем переменной, а второе~-- именем функции
доступа; они не обязательно должны быть одинаковыми. Например, вы можете написать метод
для слияния двух счетов, используя два вызова \lstinline{WITH-ACCESSORS}, для каждого из
счетов.
\begin{myverb}
(defmethod merge-accounts ((account1 bank-account) (account2 bank-account))
(with-accessors ((balance1 balance)) account1
(with-accessors ((balance2 balance)) account2
(incf balance1 balance2)
(setf balance2 0))))
\end{myverb}
Выбор между использованием \lstinline{WITH-SLOTS} и \lstinline{WITH-ACCESSORS} примерно таков, как и
выбор между использованием \lstinline{SLOT-VALUE} и функций доступа: низкоуровневый код,
который обеспечивает основную функциональность класса, может использовать
\lstinline{SLOT-VALUE} или \lstinline{WITH-SLOTS} для работы со слотами напрямую, если функции
доступа не поддерживают нужного стиля работы или если хочется явно избежать использования
вспомогательных методов, которые могут быть определены для функций доступа. Но в общем вы
должны использовать функции доступа или \lstinline{WITH-ACCESSORS}, если только у вас не
имеются конкретные причины не делать этого.
\section{Слоты, выделяемые для классов}
Заключительной опцией, которую вам необходимо знать, является опция \lstinline{:allocation}.
Значением опции \lstinline{:allocation} может быть либо \lstinline{:instance}, либо \lstinline{:class}, и
по умолчанию оно равно \lstinline{:instance}, если оно не было явно указано. Когда слот имеет
значение опции, равное \lstinline{:class}, то слот имеет только одно значение, которое
сохраняется внутри класса и используется всеми экземплярами.
Однако доступ к слотам со значением \lstinline{:class} производится так же, как и для слотов со
значением \lstinline{:instance},~-- доступ производится с помощью \lstinline{SLOT-VALUE} или функции
доступа, что значит, что вы можете получить доступ только через экземпляр класса, хотя это
значение не хранится в этом экземпляре. Опции \lstinline{:initform} и \lstinline{:initarg} имеют
точно такой же эффект, за тем исключением, что начальное выражение вычисляется один раз,
при определении класса, а не при создании экземпляра. С другой стороны, передача
начальных аргументов \lstinline{MAKE-INSTANCE} установит значение, затрагивая все экземпляры
данного класса.
Поскольку вы не можете получить слот, выделенный для класса, не имея экземпляра класса, то
такие слоты не являются полным аналогом статическим членам в таких языках, как Java, C++ и
Python\footnote{Meta Object Protocol (MOP), который не является частью стандарта языка,
но поддерживается большинством реализаций Common Lisp, предоставляет функцию
\lstinline{class-prototype}, которая возвращает экземпляр класса, который может
использоваться для доступа к слотам, выделенным для класса. Если вы используете
реализацию, которая поддерживает MOP, и вы переносите программу с другого языка, который
часто использует статические переменные, то эта функция облегчит данный процесс. Но все
не настолько однозначно.}\hspace{\footnotenegspace}. В~значительной степени слоты, выделенные для класса, в
основном используются для уменьшения потребляемой памяти; если вы создаёте много
экземпляров класса и они все имеют ссылку на один и тот же объект (например, список
разделяемых ресурсов), то вы можете сократить использование памяти путём объявления такого
слота выделяемым для класса, а не для экземпляра.
\section{Слоты и наследование}
Как обсуждалось в предыдущей главе, классы наследуют поведение от своих суперклассов
благодаря механизмам обобщённых функции~-- метод, специализированный для класса~\lstinline{A},
также применим не только к экземплярам класса~\lstinline{A}, но также и к экземплярам классов,
унаследованных от~\lstinline{A}. Классы также наследуют от своих суперклассов слоты, но этот
механизм немного отличается.
В~Common Lisp конкретный объект может иметь только один слот с определённым именем.
Однако возможно, что в иерархии наследования класса несколько классов будут иметь слоты с
одним и тем же именем. Это может случиться либо потому, что подкласс включает
спецификатор слота с тем же именем, что и слот, указанный в суперклассе, либо потому, что
несколько суперклассов имеют слоты с одним и тем же именем.
Common Lisp решает эту проблему путём слияния всех спецификаторов с одним и тем же именем
из нового класса и всех его суперклассов для создания отдельных спецификаторов для каждого
уникального имени слота. При слиянии спецификатор, разные опции спецификаторов слотов
рассматриваются по-разному. Например, поскольку слот может иметь только одно значение по
умолчанию, то если несколько классов указывают опцию \lstinline{:initform}, то новый класс
будет использовать эту опцию из наиболее специализированного класса. Это позволяет
подклассам указывать собственные значение по умолчанию, а не те, которые были
унаследованы.
С другой стороны, опции \lstinline{:initargs} не должны быть взаимоисключающими~-- каждая опция
\lstinline{:initarg} создаёт именованный параметр, который может быть использован для
инициализации слота; множественные параметры не приводят к конфликту, так что новый
спецификатор слота будет содержать все опции \lstinline{:initargs}. Вызывающие
\lstinline{MAKE-INSTANCE} могут использовать любое из имён, указанных в \lstinline{:initargs} для
инициализации слота. Если вызывающий указывает несколько именованных аргументов, которые
инициализируют один и тот же слот, то используется то, которое стоит левее всех остальных
в списке аргументов \lstinline{MAKE-INSTANCE}.
Унаследованные опции \lstinline{:reader}, \lstinline{:writer} и \lstinline{:accessor} не включаются в
новый спецификатор слота, поскольку методы, созданные при объявлении суперкласса, будут
автоматически применяться к новому классу. Однако новый класс может создать свои
собственные функции доступа путём объявления собственных опций \lstinline{:reader},
\lstinline{:writer} или \lstinline{:accessor}.
И в заключение опция \lstinline{:allocation}, подобно \lstinline{:initform}, определяется наиболее
специализированным классом, определяющим данный слот. Таким образом, возможно сделать
так, что экземпляры одного класса будут использовать слот с опцией \lstinline{:class}, а
экземпляры его подклассов могут иметь свои собственные значения опции \lstinline{:instance} для
слота с тем же именем. А их подклассы, в свою очередь, могут переопределить этот слот с
опцией \lstinline{:class}, так что все экземпляры данного класса снова будут делить между собой
единственный экземпляр слота. В~последнем случае слот, разделяемый экземплярами
подподклассов, отличается от слота, разделяемого оригинальным суперклассом.
Например, у вас имеются следующие классы:
\begin{myverb}
(defclass foo ()
((a :initarg :a :initform "A" :accessor a)
(b :initarg :b :initform "B" :accessor b)))
(defclass bar (foo)
((a :initform (error "Must supply a value for a"))
(b :initarg :the-b :accessor the-b :allocation :class)))
\end{myverb}
При создании экземпляра класса \lstinline{bar} вы можете использовать унаследованный начальный
аргумент \lstinline{:a} для указания значения для слота~\lstinline{a} и в действительности должны
сделать это для того, чтобы избежать ошибок, поскольку опция \lstinline{:initform}, определённая
\lstinline{bar} замещает опцию, унаследованную от \lstinline{foo}. Для инициализации слота~\lstinline{b}
вы можете использовать либо унаследованный аргумент \lstinline{:b}, либо новый аргумент
\lstinline{:the-b}. Однако поскольку для слота~\lstinline{b} в определении \lstinline{bar} указана
опция \lstinline{:allocation}, то указанное значение будет храниться в слоте, используемом
всеми экземплярами \lstinline{bar}. Доступ к этому слоту может быть осуществлён либо
с помощью метода обобщённой функции \lstinline{b}, специализированного для \lstinline{foo}, либо с
помощью нового метода обобщённой функции \lstinline{the-b}, который специализирован для
\lstinline{bar}. Для доступа к слоту~\lstinline{a} классов \lstinline{foo} или \lstinline{bar} вы продолжите
использовать обобщённую функцию~\lstinline{a}.
Обычно слияние определений слотов происходит достаточно гладко. Однако важно помнить,
что при использовании множественного наследования два не относящихся друг к другу слота,
имеющих одно и то же имя, в новом классе будут слиты в один слот. Так что методы,
специализированные для разных классов, могут работать с одним и тем же слотом, когда они
будут применяться к классу, унаследованному от этих классов. На практике это не
доставляет особых проблем, поскольку, как вы увидите в главе~\ref{ch:21}, вы можете
использовать пакетную систему, для того чтобы избежать коллизий между именами в коде.
\section{Множественное наследование}
\label{sec:17-multi-inheritance}
Все классы, которые вы до сих пор видели, имели только один суперкласс. Common Lisp также
поддерживает множественное наследование~-- класс может иметь несколько прямых
суперклассов, наследуя соответствующие методы и спецификаторы слотов из всех этих классов.
Множественное наследование не вносит кардинальных изменений в механизмы наследования,
которые я уже обсуждал,~-- каждый класс, определённый пользователем, уже имеет несколько
суперклассов, поскольку они все наследуются от \lstinline{STANDARD-OBJECT}, который унаследован
от~\lstinline{T}, так что, по крайней мере, имеются два суперкласса. Затруднение, которое вносит
множественное наследование, заключается в том, что класс может иметь более одного
непосредственного суперкласса. Это усложняет понятие специфичности класса, которое
используется при построении эффективных методов для обобщённых функции и при слиянии
спецификаторов слотов.
Так что если бы классы могли иметь только один непосредственный суперкласс, то
упорядочение классов по специфичности было бы тривиальным~-- класс и все его суперклассы
могли быть выстроены в линию, начиная с самого класса, за которым следует один прямой
суперкласс, за которым следует его суперкласс, и так далее, до класса~\lstinline{T}. Но
когда класс имеет несколько непосредственных суперклассов, то эти классы обычно не связаны
друг с другом~-- конечно, если один класс был подклассом другого, вам не нужно наследовать
класс от обоих. В~этом случае правила, по которому подклассы более специфичны, чем
суперклассы, недостаточно для упорядочения всех суперклассов. Так что Common Lisp
использует второе правило, которое сортирует не относящиеся друг к другу суперклассы по
порядку, в котором они перечислены в определении непосредственных суперклассов в
\lstinline{DEFCLASS}: классы, указанные в списке первыми, считаются более специфичными,
чем классы, указанные в списке последними. Это правило считается достаточно произвольным,
но оно позволяет каждому классу иметь линейный \textit{список следования классов} (class
precedence list), который может использоваться для определения того, какой из суперклассов
будет считаться более специфичным, чем другой. Однако заметьте, что нет глобального
упорядочения классов~-- каждый класс имеет собственный список следования классов, и одни и
те же классы могут стоять на разных позициях в списках следования разных классов.
Для того чтобы увидеть, как это работает, давайте добавим новый класс к нашему банковскому
приложению: \lstinline{money-market-account}. Этот счёт объединяет в себе характеристики
чекового (\lstinline{checking-account}) и сберегательного (\lstinline{savings-account}) счетов:
клиент может выписывать чеки, но, кроме того, он получает проценты. Вы можете определить
его следующим образом:
\begin{myverb}
(defclass money-market-account (checking-account savings-account) ())
\end{myverb}
Список следования класса \lstinline{money-market-account} будет следующим:
\begin{myverb}
(money-market-account
checking-account
savings-account
bank-account
standard-object
t)
\end{myverb}
Заметьте, как список удовлетворяет обоим правилам: каждый класс появляется раньше своих
суперклассов, а \lstinline{checking-account} и \lstinline{savings-account} располагаются в порядке,
указанном в \lstinline{DEFCLASS}.
Этот класс не определяет своих собственных слотов, но унаследует слоты от обоих
суперклассов, включая слоты, которые те унаследовали от своих суперклассов. Аналогичным
образом все методы, которые применимы к любому из классов в списке следования, также
будут применимы к объекту \lstinline{money-market-account}. Поскольку все спецификаторы
одинаковых слотов объединяются, то не имеет значения, что \lstinline{money-market-account}
дважды наследует одни и те же слоты из \lstinline{bank-account}\pclfootnote{Другими словами,
Common Lisp не страдает от проблемы наследования (diamond inheritance problem), которая
имеется в C++. В~C++, когда один класс наследуется от двух классов, которые оба
наследуют переменную от общего суперкласса, он наследует эту переменную дважды, что
ведёт к беспорядку.}.
Множественное наследование наиболее просто понять, когда суперклассы предоставляют
совершенно независимые наборы слотов и методов. Например, \lstinline{money-market-account}
унаследует слоты и поведение по работе с чеками от \lstinline{checking-account}, а слоты и
поведение по вычислению процентов~-- от \lstinline{savings-account}. Вам не нужно беспокоиться
о списке следования класса для методов и слотов, унаследованных только от одного или
другого суперкласса.
Однако также можно унаследовать методы для одних и тех же обобщённых функций от
различных суперклассов. В~этом случае в игру включается список следования
классов. Например, предположим, что банковское приложение определяет обобщённую функцию
\lstinline{print-statement}, которая используется для генерации месячных отчётов. Вероятно,
что уже будут определены методы \lstinline{print-statement}, специализированные для.
\lstinline{checking-account} и \lstinline{savings-account}. Оба этих метода будут применимы для
экземпляров класса \lstinline{money-market-account}, но тот, который специализирован для
\lstinline{checking-account}, будет считаться более специфичным, чем специализированный для
\lstinline{savings-account}, поскольку \lstinline{checking-account} имеет больший приоритет перед
\lstinline{savings-account} в списке следования классов \lstinline{money-market-account}.
Предполагается, что унаследованные методы являются основными методами, и вы не определяли
других методов, специализированных для \lstinline{checking-account}, которые будут
использоваться, если вы выполните \lstinline{print-statement} для \lstinline{money-market-account}.
Однако это не обязательно даст вам то поведение, которое вы хотите, поскольку вы хотите,
чтобы отчёт для нового счёта содержал элементы из отчётов по чековому и сберегательному
счетам.
Вы можете изменить поведение \lstinline{print-statement} для \lstinline{money-market-accounts}
несколькими способами. Непосредственным способом является определение основного метода,
спе\-циа\-ли\-зи\-ро\-ван\-но\-го для \lstinline{money-market-account}. Это даст вам полный контроль за
поведением, но, вероятно, потребует написания кода для опций, которые я буду вскоре
обсуждать. Проблема заключается в том, что хотя вы можете использовать
\lstinline{CALL-NEXT-METHOD} для передачи управления <<вверх>>, следующему методу, а именно
специализированному для \lstinline{checking-account}, но не существует способа вызвать
конкретный менее специфичный метод, например специализированный для
\lstinline{savings-account}. Так что если вы хотите иметь возможность использования кода,
который создаёт часть отчёта, специфичную для \lstinline{savings-account}, то вам нужно разбить
этот код на отдельные функции, которые вы сможете вызвать напрямую из методов
\lstinline{print-statement} классов \lstinline{money-market-account} и \lstinline{savings-account}.
Другой возможностью является написание основных методов всех трёх классов так, чтобы они
вызывали \lstinline{CALL-NEXT-METHOD}. Тогда метод, специализированный для
\lstinline{money-market-account}, будет использовать \lstinline{CALL-NEXT-METHOD} для вызова метода,
специализированного для \lstinline{checking-account}. Затем этот метод вызовет
\lstinline{CALL-NEXT-METHOD}, что приведёт к запуску метода для \lstinline{savings-account},
поскольку он будет следующим наиболее специфичным методом в списке следования классов для
\lstinline{money-market-account}.
Конечно, если вы не хотите полагаться на соглашения о стиле кодирования (что каждый метод
будет вызывать \lstinline{CALL-NEXT-METHOD}), чтобы убедиться, что все применимые методы будут
вызваны в некоторый момент времени, вы должны подумать об использовании вспомогательных
методов. В~этом случае вместо определения основного метода \lstinline{print-statement} для
\lstinline{checking-account} и \lstinline{savings-account} вы можете определить их как методы
\lstinline{:after}, оставляя один основной метод для \lstinline{bank-account}. Так что
\lstinline{print-statement}, вызванный для \lstinline{money-market-account}, выдаст базовую
информацию о счёте, которая будет выведена основным методом, специализированным для
\lstinline{bank-account}, за которым следуют дополнительные детали, выведенные методами
\lstinline{:after}, специализированными для \lstinline{savings-account} и \lstinline{checking-account}. И
если вы хотите добавить детали, специфичные для \lstinline{money-market-accounts}, вы можете
определить метод \lstinline{:after}, специализированный для \lstinline{money-market-account},
который будет выполнен последним.
Преимуществом использования вспомогательных методов является то, что становится понятным,
какой из методов является ответственным за реализацию обобщённой функции и какие из них
вносят дополнительные детали в работу функции. Недостатком этого подхода является то, что
вы не получаете точного контроля за порядком, в котором будут выполняться вспомогательные
методы,~-- если вы хотите, чтобы часть отчёта, приготовленного для \lstinline{checking-account},
печаталась перед частью \lstinline{savings-account}, то вы должны изменить порядок, в котором
\lstinline{money-market-account} наследуются от этих классов. Но это достаточно трагическое
изменение, которое затрагивает другие методы и унаследованные слоты. В~общем, если вы
обнаружите, что рассматриваете изменение списка непосредственных суперклассов как способ
тонкой настройки поведения специфических методов, то вы, скорее всего, должны сделать шаг
назад и заново обдумать ваш подход.
С другой стороны, если вы не заботитесь о порядке наследования, но хотите, чтобы он был
последовательным для разных обобщённых функций, то использование вспомогательных методов
может быть одним из методов. Например, если в добавление к \lstinline{print-statement} вы
имеете функцию \lstinline{print-detailed-statement}, то вы можете реализовать обе функции,
используя методы\lstinline{:after} для разных подклассов \lstinline{bank-account}, и порядок частей
и для основного и для детального отчёта будет одинаков.
\section{Правильный объектно-ориентированный дизайн}
Это все о главных возможностях объектной системы Common Lisp. Если у вас имеется большой
опыт объектно-ориентированного программирования, вы, вероятно, увидите, как возможности
Common Lisp могут быть использованы для реализации правильного объектно-ориентированного
дизайна. Однако если у вас небольшой опыт объектно-ориентированного программирования, то
вам понадобится провести некоторое время, чтобы освоиться с объектно-ориентированным
мышлением. К сожалению, это достаточно большой раздел, находящийся за пределами данной
книги. Или как указано в справочной странице по объектной системе Perl <<Теперь, вам
нужно лишь выйти и купить книгу о методологии объектно-ориентированного дизайна и
провести с ней следующие шесть месяцев>>. Или вы можете продолжить чтение до практических
глав далее в этой книге, где вы увидите несколько примеров того, как эти возможности
используются на практике. Однако сейчас вы готовы к тому, чтобы взять перерыв и перейти
от теории объектно-ориентированного программирования к другой теме~-- как можно полезно
использовать мощную, но немного загадочную функцию Common Lisp~-- \lstinline{FORMAT}.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: