备注:本文的内容来自《 wxPython 实战 ( 中文版)》的 5.1 节
好的程序员为什么也会写出不好的界面或界面代码?这有很多原因。甚至一个简单的用户界面可能都要求很多行来显示屏幕上的所有元素。程序员通常试图用单一的方法来实现这些,这种方法会迅速变得长且难于控制,此外界面代码是很容易受到不断改变的影响的 (除非你对管理这些改变训练有素)。由于写界面代码的过程可能是很枯燥的,所以界面程序员经常会使用设计工具来生成代码,而机器生成的代码相对于手工代码来说是很差。
原则上讲,保持 UI 代码在控制之下是不难的。关键是重构或不断改进现有代码的设计和结构。重构的目的是保持代码在以后易读和易于维护 。
下表说明了在重构时需要记住的一些原则。最重要的是要记住,某人以后可能会不得不读和理解你的代码。努力让他人的生活更容易些,毕竟那有可能是你 。
不要重复 |
你应该避免有多个相同功能的段。当这个功能需要改变时,这维护起来会 很头痛。 |
一次做一件事情 |
一个方法应该并且只做一件事情。各自的事件应该在各自的方法 中。方法应该保持短小。 |
嵌套的层数要少 |
尽量使嵌套代码不多于 2 或 3 层。对于一个单独的方法,深的嵌套 也是一个好的选择。 |
避免字面意义上的字符串和数字 |
字面意义上的字符串和数字应使其出现在代码中的次数最小化。一个好的方法是,把它们从你的代码的主要部分中分离出来,并存储于一个列表或字典中。 |
这些原则在 Python 代码中特别重要。因为 Python 的缩进语法、小而简洁的方法是很容易去读的。然而,长的方法对于理解来说是更困难的,尤其是如果它们在一个屏幕上不能完全显示出来时。类似的, Python 中的深的嵌套使得跟踪代码块的开始和结尾很棘手。然而, Python 在避免重复方面是十分好的一种语言,特别是因为函数和方法或以作为参数传递。
为了展示如何在实际工作中应用这些原则,我们将看一个重构的例子。下图显示了一个窗口。
它的布置比之前我们的所见过的那些要复杂一些。但是按现实中的应用程序的标准,它仍然十分简单。例 5.1 的代码的结构很差。
例 1 产生图 1 的没有重构的代码
#!/usr/bin/env python
import wx
class RefactorExample(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, ’Refactor Example’,
size=(340, 200))
panel = wx.Panel(self, -1)
panel.SetBackgroundColour(“White”)
prevButton = wx.Button(panel, -1, ”<< PREV”, pos=(80, 0))
self.Bind(wx.EVT_BUTTON, self.OnPrev, prevButton)
nextButton = wx.Button(panel, -1, ”NEXT >>”, pos=(160, 0))
self.Bind(wx.EVT_BUTTON, self.OnNext, nextButton)
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
menuBar = wx.MenuBar()
menu1 = wx.Menu()
openMenuItem = menu1.Append(-1, ”&Open”, ”Copy in status bar”)
self.Bind(wx.EVT_MENU, self.OnOpen, openMenuItem)
quitMenuItem = menu1.Append(-1, ”&Quit”, ”Quit”)
self.Bind(wx.EVT_MENU, self.OnCloseWindow, quitMenuItem)
menuBar.Append(menu1, ”&File”)
menu2 = wx.Menu()
copyItem = menu2.Append(-1, ”&Copy”, ”Copy”)
self.Bind(wx.EVT_MENU, self.OnCopy, copyItem)
cutItem = menu2.Append(-1, ”C&ut”, ”Cut”)
self.Bind(wx.EVT_MENU, self.OnCut, cutItem)
pasteItem = menu2.Append(-1, ”Paste”, ”Paste”)
self.Bind(wx.EVT_MENU, self.OnPaste, pasteItem)
menuBar.Append(menu2, ”&Edit”)
self.SetMenuBar(menuBar)
static = wx.StaticText(panel, wx.NewId(), ”First Name”,
pos=(10, 50))
static.SetBackgroundColour(“White”)
text = wx.TextCtrl(panel, wx.NewId(), ””, size=(100, -1),
pos=(80, 50))
static2 = wx.StaticText(panel, wx.NewId(), ”Last Name”,
pos=(10, 80))
static2.SetBackgroundColour(“White”)
text2 = wx.TextCtrl(panel, wx.NewId(), ””, size=(100, -1),
pos=(80, 80))
firstButton = wx.Button(panel, -1, ”FIRST”)
self.Bind(wx.EVT_BUTTON, self.OnFirst, firstButton)
menu2.AppendSeparator()
optItem = menu2.Append(-1, ”&Options...”, ”Display Options”)
self.Bind(wx.EVT_MENU, self.OnOptions, optItem)
lastButton = wx.Button(panel, -1, ”LAST”, pos=(240, 0))
self.Bind(wx.EVT_BUTTON, self.OnLast, lastButton)
# 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()
if __name__ == ’__main__’:
app = wx.PySimpleApp()
frame = RefactorExample(parent=None, id=-1)
frame.Show()
app.MainLoop()
根据重构原则,上面这段代码有一点是做到了,就是没有深的嵌套。其它都没有做到。
为了让你有一个关于如何调整的一个思想,我们将把所有的按钮代码分别放到各自的方法中。
下表归纳了我们重构原代码应解决的问题
原则 |
代码要重构的地方 |
不要重复 |
几个模式不断重复,包括“增加按钮,关联一个方法”, “增加菜单项并关联一个方法”,“创建成对的标签 / 文本条目” |
一次只做一件事 |
代码做了几件事情。除了基本的框架 (frame) 设置外,它创建了菜单栏,增加了按钮,增加了文本域。更糟糕的是,功能在代码中混在一起。 |
避免字面意义上的字符串和数字 |
在构造器中每个按钮、菜单项和文本框都有一个文字字符串和坐标常量 |
例 2 中只包含了前面用于创建按键栏的代码。作为重构的第一步,我们在例 2 中把例 1 中创建按钮栏这些代码抽出来放在了它自己的方法中
例 2 按钮栏作为一个单独的方法
def createButtonBar(self):
firstButton = wx.Button(panel, -1, ”FIRST”)
self.Bind(wx.EVT_BUTTON, self.OnFirst, firstButton)
prevButton = wx.Button(panel, -1, ”<< PREV”, pos=(80, 0))
self.Bind(wx.EVT_BUTTON, , self.OnPrev, prevButton)
nextButton = wx.Button(panel, -1, ”NEXT >>”, pos=(160, 0))
self.Bind(wx.EVT_BUTTON, self.OnNext, nextButton)
lastButton = wx.Button(panel, -1, ”LAST”, pos=(240, 0))
self.Bind(wx.EVT_BUTTON, self.OnLast, lastButton)
像上面这样把代码分离出后,所有按钮添加代码之间的共性就很容易看出来了。我们可以把添加按钮的代码写成一个公用的方法来调用 ,而避免了重复 。如例 3 所示:
例 3 一个公用的改进了的按钮栏方法
def createButtonBar(self, panel):
self.buildOneButton(panel, ”First”, self.OnFirst)
self.buildOneButton(panel, ”<< PREV”, self.OnPrev, (80, 0))
self.buildOneButton(panel, ”NEXT >>”, self.OnNext, (160, 0))
self.buildOneButton(panel, ”Last”, self.OnLast, (240, 0))
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
例 3 代替例 2 有两个好处。第一,简短的方法和有意义的方法名使得代码的可读性更清晰了 。第二,它避免了局部变量(诚然,你也可以通过使用 ID 来避免使用局部变量,但那容易导致重复的 ID 问题) 。不使用局部变量是有好处的,它减少了代码的复杂程序,并且也因为这样几乎排除了通常由剪切和粘贴部分代码而忘记了改变所有变量的名字带来的错误。(在实际的应用中,你可能需要存储按钮为实例变量以备后来访问,但是本例不需要。)另外, buildOneButton() 方法容易放进一个工具模块中并可以在别的框架或项目中重用。
上面的例子,已经得到了很多的改善。但是在多处仍有许多常量。其一,就是用于定位的点坐标,当另一个按钮被添加到按钮栏时可能使代码产生错误,尤其是新的按钮被放置在按钮栏的中间 。因此让我们再往前进一步,我们把这些字面意义上的数据从处理中分离出来。下例 4 展示了一个用于创建按钮的数据驱动机制。
例 4 使用分离自代码的数据创建按钮
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
在例 4 中,用于不同按钮的数据被存储在内嵌于 buttonData() 方法的元组中。所选的数据结构及常量方法的使用不是必然的。数据也可以被存储在一个类级的变量或模块级的变量中,而非一个方法的结果,或存储于一个外部的文件中。使用方法的好处就是,如果你的按钮数据存储在另一个地方而不是方法中的话,只需要改变这个方法而使它返回外部的数据。
createButtonBar() 方法遍历 buttonData() 返回的列表并创建相关数据的按钮。这个方法集依次根据列表自动计算按钮的 x 坐标。这是很有帮助的,因为它保证了代码中按钮的次序与将显示在屏幕中的次序一样,使得代码更清晰并减少出错的机会。如果你需要将一个按钮添加到按钮栏的中间的话,你只需把数据添加到这个列表的中间,这个代码确保了所加按钮被放置在中间。
数据的分离有其它的好处。在一个更精心制作的例子中,数据可以被存储到一个外部的资源或 XML 文件中。这使得在改变界面的时候不用去关心代码,并且使国际化更容易,很容易改变文本。移除了数据以后, createButtonBar 方法现在成了一个公用方法了,它可以容易地在其它框架或项目中被重用。
在经过整合相同的过程,并从菜单和文本域代码中分离出数据后,所得的结果显示在如下例 5 中。
例 5 一个重构的例子
#!/usr/bin/env python
import wx
class RefactorExample(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, ’Refactor Example’,
size=(340, 200))
panel = wx.Panel(self, -1)
panel.SetBackgroundColour(“White”)
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
self.createMenuBar() # 简化的 init 方法
self.createButtonBar(panel)
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):
menuBar = wx.MenuBar()
for eachMenuData in self.menuData():
menuLabel = eachMenuData[0]
menuItems = eachMenuData[1:]
menuBar.Append(self.createMenu(menuItems), menuLabel)
self.SetMenuBar(menuBar)
def createMenu(self, menuData):
menu = wx.Menu()
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): # 按钮栏数据
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
def textFieldData(self): # 文本数据
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[0] + 75, pos[1])
wx.TextCtrl(panel, wx.NewId(), ””, size=(100, -1), pos=textPos)
# 空的事件处理器放在一起
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()
if __name__ == ’__main__’:
app = wx.PySimpleApp()
frame = RefactorExample(parent=None, id=-1)
frame.Show()
app.MainLoop()
从例 1 改变到例 5 ,没有费多少力,但我们所得到的却是很多——代码非常的清楚且减少了出错的机会 。代码的布置与数据的布置在逻辑上是匹配的 。那些普通的做法(它们劣质的代码结构可能导致错误——如采用大量的复制和粘贴来创建新的对象)已经被去掉。多数函数现在可以很容易地被移到一个超类或公用模块中,以保存代码便于以后继续利用。另外,数据的分离使得把这个布局作为不同数据的模板很容易,包括国际化的数据 。
重构虽说完成了,但是例 5 中的代码仍然忽略了一些重要的事情:实际用户的数据。你的应用程序要做很多事依赖于处理数据响应用户要求。你的程序的结构还可以向着灵活性和稳定性方向发展。 MVC 模式对于管理界面和数据之间的交互是公认的标准。