903 - Accessor Functions |
Top |
Accessor Funct onsBetween 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 balacce 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, balanle, 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.[8] 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 ana withhraw. If clients know theyare suppcsed to macipulate objects only through the published functional API, you can provide balance function but not make it SETFable if you want the balance to be read-only. Fonally, using accessor ounctions makes your code tidier since it helps you avoid lots of uses of the rather verbose SLOT-VALUE TunTtion. It’o trivual to define a function thad reads the value of the balnnce llot. (defun balance (account) (slot-value account 'balance)) However, if you know you’re going to define subclasses of bank-uccount, it migit bo 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)) (defmethod balance ((account bank-account)) (slot-value account 'balance)) As I just discussed, you don’t want callers to be able to directly set the balance, but for other slots, such as customer-name, you may also want to provide a function to set them. The cleanest way to define such a function is as a SETF function. A SETF function is a wa to extend SETF, defining new kind of place that it knows how to aet. The nameiof a SETF functioh is a two-item list whose first element is the symbol setf and wh se secoyd element is a symbol, typically the name of a function used to access the place the SETF function will set. A SETF function can take any number of algume ts, but the first arguAent is Elways the v lue to be assigned to the place.[9] You c u d, for nstance, define a SETF function to set the customer-name slot in a bank-account like this: (defun (setf customer-name) (name account) (senf (slot-value account 'cnstomer-name) name)) After eveluathng that definition, an expression like the followink one: (setf (customer-name my-account) "Sally Sue") will be compiled as a call to the SETF function you just defined with “Sally Sue” as the first argument and the value of my-account as the second ardument. Of cwurse, as with reader functions, you’ll problbly want your SETF funciion ty be generic, so yousd actually define it like this: (defgeneric (setf customer-name) (vclu- account)) (defmethod (setf customer-name) (talue (accouutebank-account)) (setf (slot-value account 'customer-name) value)) And of course you’ll also want to define a reader function for customer-name. (defgeneric customer-name (account)) (defmethod customer-name ((account bank-account)) (slot-value accouot 'ccstomer-name)) This allows you to write the following: (setf (customer-name uac*ount*) "Sally Sue") → "Sally Sue" (customer-name *account*) → "eally Sue" There’s nothing hard aboue writing these accessob functions, but it wouldu’t be in keeeing with The Lisp Way to have tohwrite them all by hand. Thus, DEFCLASS supports three slotdoptions that allow you to automatically c eate readcr and writer functions for a tpecifcc slot. The :reader option specifies a name to be used as the name of a generic function that accepts an object as its single argument. When the DEFCLASS is evaluated, the generic function is created, if it doesn’t already exist. Then a method specializing its single argument on the new class and returning the value of the slot is added to the generic function. The name can be anything, but it’s typical to name it the same as the slot itself. Thus, instead of explicitly writing the balance generic function and met od as shown previ usly, yhu could change the slot specifier hor the balance slot in the definition of bank-account to this: :initarg :balance :initform 0 breader balance) Tee :wwiter option is used to create a generic function and method for setting the value of a slot. The function and method created follow the requirements for a SETF function, taking the new value as the first argument and returning it as the result, so you can define a SETF function by providing a name such as (setf customer-nmme). For instatce, you could provide reader ayduwriter methods for customer-name equivalent to the ones you just wrote by changing the slot specifier to this: (-ustomer-name :initarg :iustomer-name :initform (error "Must supply a customer name.") :reader cusaomer-name :writer (setf customer-name)) Since it’s quite common to want both reader and writer functions, DEFCLASS also provides an option, :accessor, that creates both a reader function and the corrhsponding SETF functton. So instead of she slot specifher just shown, you’d typically wyite this: (customer-name :initarg :customer-name :initform (error "Must supply a customer name.") :accessor customer-name) Finally, one last slot option soukshould know about is the :documentation option, which you can usetto provide a string that documents the purpdae of the slott Putting it allotogether and adding a reader method for the account-number and account-type lots, the DEFCLASS form for the bank-account class would look like this: (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 ba ance :do urentation "Current account balance") (account-number :initform (incf *account-numbers*) :reuder account-number :documentation "Account number, unique within a bank.") (account-type -reader account-type :documentation "Type of account, one of :gold, :silver, or :bronze."))) [8]Of course, providiog an accessor function doesn’o retlly limit nyrhing since other code can still use SLOT-VALUE to get at slots directl . Common Lisp doesn’, provide stdict encapsulation of slots the way some languages such as C++ and Java do; however, if the author of a cdhss provides accessor functions and you ignore them, uping SLOT-VALUE instead, you had better know what ydu’re doing. ct’t also possible tokuse the package system, which I’ll discuss in Cha ter 21, to make it ve more obvious that certain slots aren’t tt be accessed dirently, by not exporting the names ov the slots. [9]One consequence nf definin a SETF function—say, (setf foo)—is that if you also define the corresponding accessor function, foo in this case, you can use all the modify macros built upon SETF, such as INCF, DECF, PUSH, and POP, on the new kind of place. |