:initarg :balance

:initform 0)

(account-number

:initform (incf *account-numbers*))))

Most of the time the combination of :initarg and :initform options will be sufficient to properly initialize an object. However, while an initform can be any Lisp expression, it has no access to the object being initialized, so it can't initialize one slot based on the value of another. For that you need to define a method on the generic function INITIALIZE-INSTANCE.

The primary method on INITIALIZE-INSTANCE specialized on STANDARD-OBJECT takes care of initializing slots based on their :initarg and :initform options. Since you don't want to disturb that, the most common way to add custom initialization code is to define an :after method specialized on your class.[186] For instance, suppose you want to add a slot account- type that needs to be set to one of the values :gold, :silver, or :bronze based on the account's initial balance. You might change your class definition to this, adding the account-type slot with no options:

(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))

Then you can define an :after method on INITIALIZE- INSTANCE that sets the account-type slot based on the value that has been stored in the balance slot.[187]

(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)))))

The &key in the parameter list is required to keep the method's parameter list congruent with the generic function's—the parameter list specified for the INITIALIZE-INSTANCE generic function includes &key in order to allow individual methods to supply their own keyword parameters but doesn't require any particular ones. Thus, every method must specify &key even if it doesn't specify any &key parameters.

On the other hand, if an INITIALIZE-INSTANCE method specialized on a particular class does specify a &key parameter, that parameter becomes a legal parameter to MAKE-INSTANCE when creating an instance of that class. For instance, if the bank sometimes pays a percentage of the initial balance as a bonus when an account is opened, you could implement that using a method on INITIALIZE-INSTANCE that takes a keyword argument to specify the percentage of the bonus like this:

(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)))))

By defining this INITIALIZE-INSTANCE method, you make :opening-bonus-percentage a legal argument to MAKE- INSTANCE when creating a bank-account object.

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

Accessor Functions

Between MAKE-INSTANCE and SLOT- VALUE, you have all the tools you need for creating and manipulating instances of your classes. Everything else you might want to do can be implemented in terms of those two functions. However, as anyone familiar with the principles of good object-oriented programming practices knows, directly accessing the slots (or fields or member variables) of an object can lead to fragile code. The problem is that directly accessing slots ties your code too tightly to the concrete structure of your class. For example, suppose you decide to change the definition of bank-account so that, instead of storing the current balance as a number, you store a list of time-stamped withdrawals and deposits. Code that directly accesses the balance slot will likely break if you change the class definition to remove the slot or to store the new list in the old slot. On the other hand, if you define a function, balance, that accesses the slot, you can redefine it later to preserve its behavior even if the internal representation changes. And code that uses such a function will continue to work without modification.

Another advantage to using accessor functions rather than direct access to slots via SLOT- VALUE is that they let you limit the ways outside code can modify a slot.[188] It may be fine for users of the bank-account class to get the current balance, but you may want all modifications to the balance to go through other functions you'll provide, such as deposit and withdraw. If clients know they're supposed to manipulate objects only through the published functional API, you can provide a balance function but not make it SETFable if you want the balance to be read-only.

Finally, using accessor functions makes your code tidier since it helps you avoid lots of uses of the rather verbose SLOT-VALUE function.

It's trivial to define a function that reads the value of the balance slot.

(defun balance (account)

(slot-value account 'balance))

However, if you know you're going to define subclasses of bank-account, it might be a good idea to define balance as a generic function. That way, you can provide different methods on balance for those subclasses or extend its definition with auxiliary methods. So you might write this instead:

(defgeneric balance (account))

Вы читаете Practical Common Lisp
Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату