Tuesday 18 May 2010

Note: test before release

Embarrassing...released version 0.40 with about 6 bugs that I found within a 10 minute play around with the program. For shame!

Anyway, that takes care of that, I hope. Version 0.40.1 released Had another bug report about damn unicode errors - thought I'd fixed those! oh dear

Sunday 16 May 2010

Whyteboard 0.4 released

New whyteboard released, bringing a whole new bunch of features.

New features:
- Highlighter tool
- Improvements to selection tool
- UI changes
- Better shape viewer
- PDF Cache viewer
- bug fixes and misc. changes/improvements - see changelog for further details

Enjoy. As always, report any bugs/issues/suggestions.

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.