Monday 3 May 2010

The Law of Demeter

I've been learning a lot about refactoring over the past few days, and have made an effort to put these practices into effect. One interesting concept is the Law of Demeter - classes should know as little about their interacting classes as possible. This means restricting access on instance variable, and method arguments.

Suppose we have an instance variable of type B inside A. Class B contains a reference to class C. The Law of Demeter states we should not access properties of C through B; instead we should tell B on how to interact with C.

Here's a before/after example of some code from Whyteboard I've modified, involving selecting a shape using the Select tool:



class Select:
def check_for_hit(self, shape, x, y):
"""
Sees if a shape is underneath the mouse coords, and allows the shape to
be re-dragged to place
"""
found = False
handle = shape.handle_hit_test(x, y) # test handle before area

if handle:
self.handle = handle
found = True
elif shape.hit_test(x, y):
found = True

if found:
self.canvas.overlay = wx.Overlay()
self.shape = shape
self.dragging = True
self.offset = self.shape.offset(x, y)

if self.canvas.selected:
self.canvas.deselect()
self.canvas.selected = shape
shape.selected = True

pub.sendMessage('shape.selected', shape=shape)
return found


class Gui:
# bind a handler for shape.selected
pub.subscribe(self.shape_selected, 'shape.selected')

def shape_selected(self, shape):
"""
Shape getting selected (by Select tool)
"""
x = self.canvas.shapes.index(shape)
self.canvas.shapes.pop(x)
self.canvas.redraw_all() # hide 'original'
self.canvas.shapes.insert(x, shape)
shape.draw(self.canvas.get_dc(), False) # draw 'new'

ctrl, menu = True, True
if not shape.background == wx.TRANSPARENT:
ctrl, menu = False, False

self.control.transparent.SetValue(ctrl)
self.menu.Check(ID_TRANSPARENT, menu)


In this code, we see the Select tool (a "model" is manipulating the canvas directly, when it really shouldn't be doing so. We can delegate this to the canvas itself, through the GUI.



class Select:
def check_for_hit(self, shape, x, y):
"""
Sees if a shape is underneath the mouse coords, and allows the shape to
be re-dragged to place
"""
found = False
handle = shape.handle_hit_test(x, y) # test handle before area

if handle:
self.handle = handle
found = True
elif shape.hit_test(x, y):
found = True

if found:
self.shape = shape
self.dragging = True
self.offset = self.shape.offset(x, y)
pub.sendMessage('shape.selected', shape=shape)
return found


class Gui:
# bind a handler for shape.selected
pub.subscribe(self.shape_selected, 'shape.selected')

def shape_selected(self, shape):
"""
Shape getting selected (by Select tool)
"""
self.canvas.select_shape(shape)

ctrl, menu = True, True
if not shape.background == wx.TRANSPARENT:
ctrl, menu = False, False

self.control.transparent.SetValue(ctrl)
self.menu.Check(ID_TRANSPARENT, menu)


class Canvas:
def select_shape(self, shape):
"""Selects the selected shape"""
self.overlay = wx.Overlay()
if self.selected:
self.deselect_shape()

self.selected = shape
shape.selected = True
x = self.shapes.index(shape)
self.shapes.pop(x)
self.redraw_all() # hide 'original'
self.shapes.insert(x, shape)
shape.draw(self.get_dc(), False) # draw 'new'



We can now see that the appropriate classes are performing their correct responsibilities. The model sends out a message to indicate something's changed, without caring how the event is handled, and the GUI is updating its controls and menus. The GUI delegates a method call to the canvas which updates itself.

There are no longer many "access levels" (this.that.other.do_something) as all operations are on variables within a small scope. This is better code that's easier to test.

No comments:

Post a Comment