Practical Object Oriented Design in Common Lisp part 3
Hiding data structures in the next strategy Metz discusses in Chapter 2. Just as hiding instance variables with accessors, is a good idea, we’ll see how hiding data structures also makes your code more flexible. Again we’ll adapt the sample code to Common Lisp and see how these principles work in CLOS.
Here is one of Metz’s examples, a class whose objects must be initialized with an array of two member arrays.
class ObscuringReferences
attr_reader :data
def initialize(data)
@data = data
end
def diameters
# 0 is rim, 1 is tire
data.collect {|cell|
cell[0] + (cell[1] * 2)}
end
end
Here the diameters methods must know about the array’s structure as it loops over it.
Here’s a quick pass at implementing it in Common Lisp.
(defclass obscuring-references ()
(data :reader data :initarg data))
(defmethod diameters ((o-ref obscuring-references))
(map 'list (lambda (wheel)
(destructuring-bind (rim tire) wheel
(+ rim (* tire 2))))
(data o-ref)))
This may not seem so bad at first, since
DESTRUCTURING-BIND
allows us to create structural
references on the fly, and it’s also useful in the “code as
documentation” sense, but the references are only useful in that one
place. As Metz discusses, this code is vulnerable to future changes in
the initializing structure. Ultimately we want to create a class and
set up objects that be more flexibly adapted.
Metz suggests thinking about how to take structure and give it labels, and interface that can be used, while hiding the structure, like this:
class RevealingReferences
attr_reader :wheels
def initialize(data)
@wheels = wheelify(data)
end
def diameters
wheels.collect {|wheel|
wheel.rim + (wheel.tire * 2)}
end
Wheel = Struct.new(:rim, :tire)
def wheelify(data)
data.collect {|cell|
Wheel.new(cell[0], cell[1])}
end
end
In Common Lisp we might do it like this:
(defclass revealing-references ()
((wheels :reader wheels :initarg :wheels)))
(defstruct (wheel (:conc-name Nil)) rim tire)
(defmethod initialize-instance :after ((o-ref revealing-references) &key wheels)
(setf (slot-value o-ref 'wheels)
(map 'list (lambda (wheel)
(destructuring-bind (rim tire) wheel
(make-wheel :rim rim :tire tire)))
wheels)))
(defmethod diameters ((o-ref revealing-references))
(map 'list (lambda (wheel)
(with-accessors ((rim rim) (tire tire)) wheel
(+ rim (* tire 2))))
(wheels o-ref)))
We’re still using DESTRUCTURING-BIND
but now it’s in the
specialized INITIALIZE-INSTANCE :AFTER
method. This will
transparently create a list of WHEEL
structures with
RIM
and TIRE
accessors. The diameters method
just has to roll over the wheel objects and use the accessors.
In Ruby the Struct class is more like a Common Lisp class, a basic structure which provides accessors. You can add other methods to it easily enough, and, of course you can lisp too. Here’s what we should do in Common Lisp to isolate the diameter calculating functionality
(defmethod diameter ((wheel wheel))
(with-accessors ((rim rim) (tire tire)) wheel
(+ rim (* tire 2))))
(defmethod diameters ((o-ref revealing-references))
(map 'list 'diameter (wheels o-ref)))
Common Lisp structs are optimized but if you want turn the wheel into a class, now you can. The interface is stable. Lets look at the wheel class.
(defclass wheel ()
((rim :reader rim :initarg :rim)
(tire :reader tire :initarg :tire)))
The DIAMETER
method stays the same as above, lets bring
back the GEAR
class and RATIO
method.
(defclass gear ()
((cog :reader cog :initarg :cog)
(chainring :reader chainring :initarg :chainring)))
(defmethod ratio ((gear gear))
(with-accessors ((cog cog) (chainring chainring)) gear
(/ chainring (float cog))))
In Common Lisp we don’t have to figure out whether a gear should belong to a wheel or a wheel have a gear. We can write a method for both.
(defmethod gear-inches ((wheel wheel) (gear gear))
(* (ratio gear) (diameter wheel)))
This chapter has been about isolating responsibilities in your code.
The GEAR
and WHEEL
classs and methods are
pretty much down to a single responsibility. There’s still more to
learn, and in POODR chapter 3 we learn about “Managing Dependencies”.
We’ve already avoided one dependency, by using multiple-dispatch with
our GEAR-INCHES
method, but there’s some other
dependencies worth considering next.