Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
918 lines (783 sloc) 81.2 KB
\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: