Practical Object Oriented Design in Common Lisp part 2
In POODR chapter 2 in the section called “Writing Code That Embraces Change” Metz discusses two strategies: hiding instance variables and hiding data structures. I’m going discuss the first of these.
Metz strongly recommends wrapping instance variables in accessor methods instead of directly referring to them.
In this code the ratio method calls on the instance variables directly.
class Gear
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
@chainring / @cog.to_f
end
end
This code defines reader methods for it’s instance variables and the ratio method uses those:
class Gear
attr_reader :chainring, :cog
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
chainring / cog.to_f
end
end
In Common Lisp the method SLOT-VALUE
can be used to
access the values of Common Lisp slots. We can define a very primitive
gear class and ratio method like so:
(defclass gear ()
((chainring :initarg :chainring)
(cog :initarg :cog)))
(defmethod ratio ((gear gear))
(/ (slot-value gear 'chainring)
(float (slot-value gear 'cog))))
The macro WITH-SLOTS
provides a convenient shorthand:
(defmethod ratio ((gear gear))
(with-slots (chainring cog) gear
(/ chainring (float cog))))
Metz advises that all methods use accessors to access the values of
instance variables. The DEFCLASS
macro will create
accessor functions for slots if specified:
(defclass gear ()
((chainring :reader chainring :initarg :chainring)
(cog :reader cog :initarg :cog)))
And now we can use them in the ratio method
(defmethod ratio ((gear gear))
(/ (chainring gear) (cog gear)))
The macro WITH-ACCESSORS
allows you to use the accessor
methods, but requires you define aliases for them:
(defmethod ratio ((gear gear))
(with-accessors ((cr chainring) (cg cog)) gear
(/ ch (float cg))))
For the sake of clarity, most lisp code I’ve seen uses aliases with the same name as the accessor:
(defmethod ratio ((gear gear))
(with-accessors ((chainring chainring) (cog cog)) gear
(/ chainring (float cog))))
Stylistic considerations aside, unquestionably, using accessor methods like this is more flexible in both Common Lisp and Ruby. If you had to refactor you would be much better off changing the accessor once rather than adjusting everywhere you called the value. This classic code virtue is called “Don’t Repeat Yourself” or DRY.
What’s funny, is that in these simple examples we see that Common Lisp requires you to repeat yourself often.
Ruby classes allow you to define standard accessors with it’s attr_
syntax, where Common Lisp’s DEFCLASS
has you specify the
accessor keyword for every slot that has one.
Because Ruby methods belong to the class, you don’t have to specify
method arguments with a name and a class, like you do with Common
Lisp’s (defmethod method-name ((object-parameter
class-specializer)) ... )
method definitions. You can name the
object parameter whatever you want to call it in the body of the
method, but for clarity’s sake, most method definitions I’ve seen give
the object parameter is given the same symbol as the
class-specializer. Likewise with WITH-ACCESSORS
with it’s
accessors and aliases.
I’m not going to defend this extra verbosity of
WITH-ACCESSORS
, which I hope you can see is trivial,
rather than seriously problematic. But I do want to show you why
DEFMETHOD
takes the class-specializer and why it’s useful
when hiding instance variables/slots in accessors.
As in Ruby, so in Common Lisp, you can redefine the accessor, to provide extra functionality and isolate features:
(defmethod chainring ((gear gear))
(+ (slot-value gear 'chainring)
*unanticipated-adjustment-factor*))
But in Common Lisp you can, and really should, define what’s called an “around method” like this:
(defmethod chainring :around ((gear gear))
(+ (call-next-method)
*unanticipated-adjustment-factor*))
Around methods wrap around the core method definition and are where
you can make changes to what the method call returns, further
isolating the functionality. There are also :before
and
:after
methods you can define which you can use to setup
side effects; they don’t return anything to the method call, they just
get triggered, either before or after it, and inside the
:around
method. You can make as many of these ancillary
methods as you want, even define how they combine.
In this way, you can preserve the accessor method (or any other method you define), but add all kinds of features to the same method call, while isolating the features to distinct parts of your code. Furthermore, because Common Lisp methods can be specialized by class, the ancillary methods will apply to objects of child classes the methods specialize on. You can overwrite them too, of course, but using the class specialization features provides you options you couldn’t easily do in single-inheritance single dispatch object system, and makes the system even more flexible.
Next, hiding data structures.