Having made a significant improvement, we could stop here. But there are still a lot of magic literals—hardcoded constants used in multiple locations—in the code. For one thing, the literal points used for positioning could make the code prone to errors when another button is being added to the bar, especially if the new button is placed in the middle of the bar. So let's go one step farther and separate the literal data from the processing. Listing 5.4 shows a more data-driven mechanism for creating buttons.
Listing 5.4 Creating buttons with data separated from code def buttonData(self):
return (("First", self.OnFirst), ("<< PREV", self.OnPrev), ("NEXT >>", self.OnNext), ("Last", self.OnLast))
def createButtonBar(self, panel, yPos=0): xPos = 0
for eachLabel, eachHandler in self.buttonData(): pos = (xPos, yPos)
button = self.buildOneButton(panel, eachLabel, eachHandler, pos) xPos += button.GetSize().width def buildOneButton(self, parent, label, handler, pos=(0,0)): button = wx.Button(parent, -1, label, pos) self.Bind(wx.EVT_BUTTON, handler, button) return button
In listing 5.4, the data for the individual buttons is stored in a nested tuple in the buttonData() method. The choice of data structure and use of a constant method is not inevitable. The data could be stored as a class-level or module-level variable, rather than the result of a method, or it could be stored in an external file. One advantage to using a method is being able to make a relatively simple transition if you wish to store the button data in another location—just change the method so that instead of returning a constant, it returns the external data.
The createButtonBar() method iterates over the list returned by button-Data() and creates each button from that data. The method now calculates the x-axis position of the buttons automatically as it traverses the list. This is helpful because it ensures that the order of the buttons in the code will be identical to the order on the screen, making the code clearer and less error-prone. If you need to add a button in the middle of the bar now, you can just add the data to the middle of the list and the code guarantees that it will be placed correctly.
The separation of the data has other benefits. In a more elaborate example, the data could be stored externally in a resource or XML file. This would allow interface changes to be made without even looking at the code, and also makes internationalization easier, by making it easier to change text. We're currently still hard-wiring the button width, but that could easily be added to the data method as well. (In reality, we'd probably use a wxPython Sizer object, which is covered in chapter 11). Also, with the specifics of the data removed, createButtonBar is now well on its way to being a utility method itself, and could easily be reused in another frame or project.
After performing the same steps of consolidating, factoring the common process, and separating data for the menu and text field code, the result is shown in listing 5.5.
Listing 5.5 A refactored example
#!/usr/bin/env python import wx class RefactorExample(wx.Frame):
wx.Frame._init_(self, parent, id, 'Refactor Example'
n ^ n Simplified init method self.createButtonBar(panel) r self.createTextFields(panel)
def menuData(self): return (("&File"
("&Open", "Open in status bar", self.OnOpen), ("&Quit", "Quit", self.OnCloseWindow)), ("&Edit",
("&Copy", "Copy", self.OnCopy), ("C&ut", "Cut", self.OnCut), ("&Paste", "Paste", self.OnPaste), ("", "", ""),
("^Options...", "DisplayOptions", self.OnOptions)))
def createMenuBar(self): <1-
menuBar = wx.MenuBar() for eachMenuData in self.menuData(): menuLabel = eachMenuData menuItems = eachMenuData[1:]
menuBar.Append(self.createMenu(menuItems), menuLabel) self.SetMenuBar(menuBar)
Menu creation here def createMenu(self, menuData): <——
for eachLabel, eachStatus, eachHandler in menuData: if not eachLabel:
menu.AppendSeparator() continue menuItem = menu.Append(-1, eachLabel, eachStatus) self.Bind(wx.EVT_MENU, eachHandler, menuItem) return menu def buttonData(self): <-J Button bar data return (("First", self.OnFirst), ("<< PREV", self.OnPrev), ("NEXT >>", self.OnNext), ("Last", self.OnLast))
def createButtonBar(self, panel, yPos = 0): xPos = 0
for eachLabel, eachHandler in self.buttonData(): pos = (xPos, yPos)
button = self.buildOneButton(panel, eachLabel, eachHandler, pos)
xPos += button.GetSize().width
Create buttons def buildOneButton(self, parent, label, handler, pos=(0,0)): —— button = wx.Button(parent, -1, label, pos) self.Bind(wx.EVT_BUTTON, handler, button) return button def textFieldData(self): <—! Text data return (("First Name", (10, 50)), ("Last Name", (10, 80)))
def createTextFields(self, panel):
for eachLabel, eachPos in self.textFieldData():
self.createCaptionedText(panel, eachLabel, eachPos)
def createCaptionedText(self, panel, label, pos):
static = wx.StaticText(panel, wx.NewId(), label, pos) static.SetBackgroundColour("White") textPos = (pos + 75, pos) wx.TextCtrl(panel, wx.NewId(), "", size=(100, -1), pos=textPos)
# Just grouping the empty event handlers together def OnPrev(self, event): pass def OnNext(self, event): pass def OnLast(self, event): pass def OnFirst(self, event): pass def OnOpen(self, event): pass def OnCopy(self, event): pass def OnCut(self, event): pass def OnPaste(self, event): pass def OnOptions(self, event): pass def OnCloseWindow(self, event): self.Destroy()
app = wx.PySimpleApp()
frame = RefactorExample(parent=None, id=-1)
The amount of effort involved in moving from listing 5.1 to listing 5.5 was minimal, but the reward is tremendous—a code base that is much clearer and less error-prone. The layout of the code logically matches the layout of the data. Several common ways that poorly structured code can lead to errors—such as requiring a lot of copying and pasting to create new objects—have been removed. Much of the functionality can now be easily moved to a superclass or utility module, making the code savings continue to pay off in the future. As an added bonus, the data separation makes it easy to use the layout as a template with different data, including international data.
The key to successfully refactoring is to keep doing it in small increments as you write your code. Like dirty dishes, poor code can pile up to an overwhelming mess quickly unless you make an effort to clean it up regularly. If you can acquire the mindset that working code is only an intermediate step toward the final goal of well-factored working code, then you can make refactoring part of your regular developing process.
However, even with the refactoring that has been done, the code in listing 5.5 is still missing something important: the actual user data. Most of what your application will do depends on manipulating data in response to user requests. The structure of your program can go a long way toward making your program flexible and stable. The MVC pattern is the accepted standard for managing the interaction between interface and data.
Was this article helpful?