Автор оригинала: Mike Driscoll.
В мире много плохого кода. Моя цель в этой статье заключается в том, чтобы помочь программистам WXPYPHON учиться, как облегчить их приложения для обслуживания и изменения. Следует отметить, что то, что в этой статье не обязательно так называемый «лучший» способ рефакторов программы; Вместо этого следующее является представлением того, что я узнал из своего собственного опыта, с небольшим количеством помощи от книги Робина Данна, WxPython в действии и сообщество WXPYPHON.
Для иллюстрации я создал графический скелет популярной программы калькулятора, которую компьютерные науки любят весной, на не подозревающие новые студенты. Код создает только пользовательский интерфейс. Это на самом деле не будет делать какие-либо расчеты. Тем не менее, я нашел какой-то код на Этот сайт Это должно быть легко адаптировать для этой программы. Я оставлю это как упражнение для читателя. Давайте посмотрим на грубый, неограниченный кусок кода:
import wx class PyCalc(wx.App): def __init__(self, redirect=False, filename=None): wx.App.__init__(self, redirect, filename) def OnInit(self): # create frame here self.frame = wx.Frame(None, wx.ID_ANY, title="Calculator") panel = wx.Panel(self.frame, wx.ID_ANY) self.displayTxt = wx.TextCtrl(panel, wx.ID_ANY, "0", size=(155,-1), style=wx.TE_RIGHT|wx.TE_READONLY) size=(35, 35) zeroBtn = wx.Button(panel, wx.ID_ANY, "0", size=size) oneBtn = wx.Button(panel, wx.ID_ANY, "1", size=size) twoBtn = wx.Button(panel, wx.ID_ANY, "2", size=size) threeBtn = wx.Button(panel, wx.ID_ANY, "3", size=size) fourBtn = wx.Button(panel, wx.ID_ANY, "4", size=size) fiveBtn = wx.Button(panel, wx.ID_ANY, "5", size=size) sixBtn = wx.Button(panel, wx.ID_ANY, "6", size=size) sevenBtn = wx.Button(panel, wx.ID_ANY, "7", size=size) eightBtn = wx.Button(panel, wx.ID_ANY, "8", size=size) nineBtn = wx.Button(panel, wx.ID_ANY, "9", size=size) zeroBtn.Bind(wx.EVT_BUTTON, self.method1) oneBtn.Bind(wx.EVT_BUTTON, self.method2) twoBtn.Bind(wx.EVT_BUTTON, self.method3) threeBtn.Bind(wx.EVT_BUTTON, self.method4) fourBtn.Bind(wx.EVT_BUTTON, self.method5) fiveBtn.Bind(wx.EVT_BUTTON, self.method6) sixBtn.Bind(wx.EVT_BUTTON, self.method7) sevenBtn.Bind(wx.EVT_BUTTON, self.method8) eightBtn.Bind(wx.EVT_BUTTON, self.method9) nineBtn.Bind(wx.EVT_BUTTON, self.method10) divBtn = wx.Button(panel, wx.ID_ANY, "/", size=size) multiBtn = wx.Button(panel, wx.ID_ANY, "*", size=size) subBtn = wx.Button(panel, wx.ID_ANY, "-", size=size) addBtn = wx.Button(panel, wx.ID_ANY, "+", size=(35,100)) equalsBtn = wx.Button(panel, wx.ID_ANY, "Enter", size=(35,100)) divBtn.Bind(wx.EVT_BUTTON, self.method11) multiBtn.Bind(wx.EVT_BUTTON, self.method12) addBtn.Bind(wx.EVT_BUTTON, self.method13) subBtn.Bind(wx.EVT_BUTTON, self.method14) equalsBtn.Bind(wx.EVT_BUTTON, self.method15) mainSizer = wx.BoxSizer(wx.VERTICAL) masterBtnSizer = wx.BoxSizer(wx.HORIZONTAL) vBtnSizer = wx.BoxSizer(wx.VERTICAL) numSizer = wx.GridBagSizer(hgap=5, vgap=5) numSizer.Add(divBtn, pos=(0,0), flag=wx.CENTER) numSizer.Add(multiBtn, pos=(0,1), flag=wx.CENTER) numSizer.Add(subBtn, pos=(0,2), flag=wx.CENTER) numSizer.Add(sevenBtn, pos=(1,0), flag=wx.CENTER) numSizer.Add(eightBtn, pos=(1,1), flag=wx.CENTER) numSizer.Add(nineBtn, pos=(1,2), flag=wx.CENTER) numSizer.Add(fourBtn, pos=(2,0), flag=wx.CENTER) numSizer.Add(fiveBtn, pos=(2,1), flag=wx.CENTER) numSizer.Add(sixBtn, pos=(2,2), flag=wx.CENTER) numSizer.Add(oneBtn, pos=(3,0), flag=wx.CENTER) numSizer.Add(twoBtn, pos=(3,1), flag=wx.CENTER) numSizer.Add(threeBtn, pos=(3,2), flag=wx.CENTER) numSizer.Add(zeroBtn, pos=(4,1), flag=wx.CENTER) vBtnSizer.Add(addBtn, 0) vBtnSizer.Add(equalsBtn, 0) masterBtnSizer.Add(numSizer, 0, wx.ALL, 5) masterBtnSizer.Add(vBtnSizer, 0, wx.ALL, 5) mainSizer.Add(self.displayTxt, 0, wx.ALL, 5) mainSizer.Add(masterBtnSizer) panel.SetSizer(mainSizer) mainSizer.Fit(self.frame) self.frame.Show() return True def method1(self, event): pass def method2(self, event): pass def method3(self, event): pass def method4(self, event): pass def method5(self, event): pass def method6(self, event): pass def method7(self, event): pass def method8(self, event): pass def method9(self, event): pass def method10(self, event): pass def method13(self, event): pass def method14(self, event): pass def method12(self, event): pass def method11(self, event): pass def method15(self, event): pass def main(): app = PyCalc() app.MainLoop() if __name__ == "__main__": main()
Я основанный на этом коде на какой-то очень неприятный код VBA, который мне пришлось сохранить за последние несколько лет. Это тот код, который вы можете столкнуться с программами, которые автоматически генерируют код для программиста, такой как Visual Studio или Macro Builder в Microsoft Office. Обратите внимание, что функции просто пронумерованы вместо того, чтобы быть описательным, и что многие из них выглядят одинаково. Когда вы видите два или более строк кода, которые выглядят одинаково или, похоже, имеют ту же цель, они обычно имеют право на рефакторинг. Термин для этого явления – «копия и вставка» или Код спагетти (Не быть путать с другими кодами, связанными с макароном, связанным с кодом эвфемизмами). Да, копия и вставленный код злой! Когда вам нужно сделать изменения, вам нужно найти каждый экземпляр скопированного кода и слишком изменить его.
На этой записке давайте начнем рефакторинг этого беспорядка! Я думаю, что разделяя кадр, приложение и панельные объекты облегчают дело, так что это то, что мы сделаем первым. Глядя на родителей виджета, мы видим, что текстовый контроль и все кнопки используют панель для их родителя, поэтому давайте поставим все это в один класс. Я также поставлю фактическое создание и макет виджета в одну функцию, которая может быть вызвана из класса панели __init__ (см. Ниже).
class MainPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) self.parent = parent self.formula = [] self.currentVal = "0" self.previousVal = "0" self.operator = None self.operatorFlag = False self.createAndlayoutWidgets() def createAndlayoutWidgets(self): mainSizer = wx.BoxSizer(wx.VERTICAL) masterBtnSizer = wx.BoxSizer(wx.HORIZONTAL) vBtnSizer = wx.BoxSizer(wx.VERTICAL) numSizer = wx.GridBagSizer(hgap=5, vgap=5) self.displayTxt = wx.TextCtrl(self, wx.ID_ANY, "0", size=(155,-1), style=wx.TE_RIGHT|wx.TE_READONLY) # number buttons size=(45, 45) zeroBtn = wx.Button(self, wx.ID_ANY, "0", size=size) oneBtn = wx.Button(self, wx.ID_ANY, "1", size=size) twoBtn = wx.Button(self, wx.ID_ANY, "2", size=size) threeBtn = wx.Button(self, wx.ID_ANY, "3", size=size) fourBtn = wx.Button(self, wx.ID_ANY, "4", size=size) fiveBtn = wx.Button(self, wx.ID_ANY, "5", size=size) sixBtn = wx.Button(self, wx.ID_ANY, "6", size=size) sevenBtn = wx.Button(self, wx.ID_ANY, "7", size=size) eightBtn = wx.Button(self, wx.ID_ANY, "8", size=size) nineBtn = wx.Button(self, wx.ID_ANY, "9", size=size) numBtnLst = [zeroBtn, oneBtn, twoBtn, threeBtn, fourBtn, fiveBtn, sixBtn, sevenBtn, eightBtn, nineBtn] for button in numBtnLst: button.Bind(wx.EVT_BUTTON, self.onButton) # operator buttons divBtn = wx.Button(self, wx.ID_ANY, "/", size=size) multiBtn = wx.Button(self, wx.ID_ANY, "*", size=size) subBtn = wx.Button(self, wx.ID_ANY, "-", size=size) addBtn = wx.Button(self, wx.ID_ANY, "+", size=(45,100)) equalsBtn = wx.Button(self, wx.ID_ANY, "Enter", size=(45,100)) equalsBtn.Bind(wx.EVT_BUTTON, self.onCalculate) opBtnLst = [divBtn, multiBtn, subBtn, addBtn] for button in opBtnLst: button.Bind(wx.EVT_BUTTON, self.onOperation) numSizer.Add(divBtn, pos=(0,0), flag=wx.CENTER) numSizer.Add(multiBtn, pos=(0,1), flag=wx.CENTER) numSizer.Add(subBtn, pos=(0,2), flag=wx.CENTER) numSizer.Add(sevenBtn, pos=(1,0), flag=wx.CENTER) numSizer.Add(eightBtn, pos=(1,1), flag=wx.CENTER) numSizer.Add(nineBtn, pos=(1,2), flag=wx.CENTER) numSizer.Add(fourBtn, pos=(2,0), flag=wx.CENTER) numSizer.Add(fiveBtn, pos=(2,1), flag=wx.CENTER) numSizer.Add(sixBtn, pos=(2,2), flag=wx.CENTER) numSizer.Add(oneBtn, pos=(3,0), flag=wx.CENTER) numSizer.Add(twoBtn, pos=(3,1), flag=wx.CENTER) numSizer.Add(threeBtn, pos=(3,2), flag=wx.CENTER) numSizer.Add(zeroBtn, pos=(4,1), flag=wx.CENTER) vBtnSizer.Add(addBtn, 0) vBtnSizer.Add(equalsBtn, 0) masterBtnSizer.Add(numSizer, 0, wx.ALL, 5) masterBtnSizer.Add(vBtnSizer, 0, wx.ALL, 5) mainSizer.Add(self.displayTxt, 0, wx.ALL, 5) mainSizer.Add(masterBtnSizer) self.SetSizer(mainSizer) mainSizer.Fit(self.parent)
Вы заметите, что добавление функции и некоторого белого пространства сделали эту часть кода выглядеть намного приятнее. Это также позволяет нам взять этот код и поместить его в отдельный файл, если мы хотели, чтобы мы могли импортировать. Это может помочь продвинуть весь MVC способ делать вещи. Если вы обращали внимание, вы увидите, что я связываю цифровые кнопки в один обработчик и кнопки оператора к другому обработчику. Вот методы, которые я использую:
def onOperation(self, event): """ Add an operator to the equation """ print "onOperation handler fired" def onButton(self, event): """ Keeps the display up to date """ # Get the button object buttonObj = event.GetEventObject() # Get the label of the button object buttonLbl = buttonObj.GetLabel() def onCalculate(self): """ Calculate the total """ print 'in onCalculate'
Я застрял какой-нибудь код в onbutton Обработчик событий, чтобы вы могли видеть, как получить ручку на кнопочный объект, который его назвал. В противном случае этот метод на самом деле ничего не делает. Другие методы – это просто заглушки. Теперь давайте посмотрим на объектный код рамы и приложения:
class PyCalcFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="Calculator") panel = MainPanel(self) class PyCalc(wx.App): def __init__(self, redirect=False, filename=None): wx.App.__init__(self, redirect, filename) def OnInit(self): # create frame here frame = PyCalcFrame() frame.Show() return True def main(): app = PyCalc() app.MainLoop() if __name__ == "__main__": main()
Как видите, они очень короткие и до точки. Если вы спешите, то этот маленький рефакторинг может быть достаточно для вас. Однако я думаю, что мы можем сделать лучше. Прокрутите резервную версию и возьмите второй взгляд на этот повторный класс панели. Посмотрите, как есть в десятке строки создания кнопок, которые в основном такие же? Там также есть много линий «Sizer.add». Это будет нашей следующей целью!
На списке рассылки WXPYPHON это прошлой весной (2009), на этой теме было большая дискуссия. Я видел много интересных решений, но тот, который был заблужден, больше всего, было создание своего рода метода здания виджета. Это то, что я покажу вам, как это сделать. Вот моя ограниченная версия:
def onWidgetSetup(self, widget, event, handler, sizer, pos=None, flags=[]): """ Accepts a widget, the widget's default event and its handler, the sizer for the widget, the position of the widget inside the sizer (if applicable) and the sizer flags (if applicable) """ widget.Bind(event, handler) if not pos: sizer.Add(widget, 0, wx.ALL, 5) elif pos and flags: sizer.Add(widget, pos=pos, flag=wx.CENTER) else: sizer.Add(widget, pos=pos) return widget
Это довольно простой кусочек кода, который принимает виджет, связывает его к событию и добавляет полученную комбинацию для Sizer. Этот скрипт может быть расширен для выполнения дополнительных привязки, гнездовые сизуры, установите шрифты и многое другое. Использовать свое воображение. Теперь мы можем взглянуть на то, как это изменило код класса панели:
class MainPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) self.parent = parent self.formula = [] self.currentVal = "0" self.previousVal = "0" self.operator = None self.operatorFlag = False self.createAndlayoutWidgets() def createAndlayoutWidgets(self): mainSizer = wx.BoxSizer(wx.VERTICAL) masterBtnSizer = wx.BoxSizer(wx.HORIZONTAL) vBtnSizer = wx.BoxSizer(wx.VERTICAL) numSizer = wx.GridBagSizer(hgap=5, vgap=5) self.displayTxt = wx.TextCtrl(self, wx.ID_ANY, "0", size=(155,-1), style=wx.TE_RIGHT|wx.TE_READONLY) # number buttons size=(45, 45) zeroBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "0", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(4,1)) oneBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "1", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(3,0)) twoBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "2", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(3,1)) threeBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "3", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(3,2)) fourBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "4", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(2,0)) fiveBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "5", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(2,1)) sixBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "6", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(2,2)) sevenBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "7", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(1,0)) eightBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "8", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(1,1)) nineBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "9", size=size), wx.EVT_BUTTON, self.onButton, numSizer, pos=(1,2)) # operator buttons divBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "/", size=size), wx.EVT_BUTTON, self.onOperation, numSizer, pos=(0,0), flags=wx.CENTER) multiBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "*", size=size), wx.EVT_BUTTON, self.onOperation, numSizer, pos=(0,1), flags=wx.CENTER) subBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "-", size=size), wx.EVT_BUTTON, self.onOperation, numSizer, pos=(0,2), flags=wx.CENTER) addBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "+", size=(45,100)), wx.EVT_BUTTON, self.onOperation, vBtnSizer) equalsBtn = self.onWidgetSetup(wx.Button(self, wx.ID_ANY, "Enter", size=(45,100)), wx.EVT_BUTTON, self.onCalculate, vBtnSizer) masterBtnSizer.Add(numSizer, 0, wx.ALL, 5) masterBtnSizer.Add(vBtnSizer, 0, wx.ALL, 5) mainSizer.Add(self.displayTxt, 0, wx.ALL, 5) mainSizer.Add(masterBtnSizer) self.SetSizer(mainSizer) mainSizer.Fit(self.parent) def onWidgetSetup(self, widget, event, handler, sizer, pos=None, flags=[]): """ Accepts a widget, the widget's default event and its handler, the sizer for the widget, the position of the widget inside the sizer (if applicable) and the sizer flags (if applicable) """ widget.Bind(event, handler) if not pos: sizer.Add(widget, 0, wx.ALL, 5) elif pos and flags: sizer.Add(widget, pos=pos, flag=wx.CENTER) else: sizer.Add(widget, pos=pos) return widget
Лучшая часть об этом коде состоит в том, что она помещает все материалы создания кнопок в метод, поэтому нам не нужно писать «Wx.Button ()» снова и снова. Эта итерация также удаляла большинство звонков «Sizer.add». Конечно, на его месте вместо этого у нас есть много звонков методов «Self.onwidgetSetetup ()». Похоже, это все еще может быть повторно, но как!? Для моего следующего трюка я просматривал рефакторинг в книге Робина Данна и пришел к выводу, что его идеи стоят попробовать. (Он Создатель Wxpython, в конце концов.)
В его книге у него есть метод Button Builder, который выглядит похоже на мой виджет Builder, хотя его намного проще. У него также есть метод, который просто возвращает данные кнопки. Я взял эти идеи и адаптировал их для этой программы, как вы можете увидеть в моем конечном коде главы:
class MainPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) self.parent = parent self.formula = [] self.currentVal = "0" self.previousVal = "0" self.operator = None self.operatorFlag = False self.createDisplay() def createDisplay(self): """ Create the calculator display """ mainSizer = wx.BoxSizer(wx.VERTICAL) masterBtnSizer = wx.BoxSizer(wx.HORIZONTAL) vBtnSizer = wx.BoxSizer(wx.VERTICAL) numSizer = wx.GridBagSizer(hgap=5, vgap=5) self.displayTxt = wx.TextCtrl(self, wx.ID_ANY, "0", size=(155,-1), style=wx.TE_RIGHT|wx.TE_READONLY) for eachLabel, eachSize, eachHandler, eachPos in self.buttonData(): button = self.buildButton(eachLabel, eachSize, eachHandler) if eachPos: numSizer.Add(button, pos=eachPos, flag=wx.CENTER) else: vBtnSizer.Add(button) masterBtnSizer.Add(numSizer, 0, wx.ALL, 5) masterBtnSizer.Add(vBtnSizer, 0, wx.ALL, 5) mainSizer.Add(self.displayTxt, 0, wx.ALL, 5) mainSizer.Add(masterBtnSizer) self.SetSizer(mainSizer) mainSizer.Fit(self.parent) def buttonData(self): size=(45, 45) return (("0", size, self.onButton, (4,1)), ("1", size, self.onButton, (3,0)), ("2", size, self.onButton, (3,1)), ("3", size, self.onButton, (3,2)), ("4", size, self.onButton, (2,0)), ("5", size, self.onButton, (2,1)), ("6", size, self.onButton, (2,2)), ("7", size, self.onButton, (1,0)), ("8", size, self.onButton, (1,1)), ("9", size, self.onButton, (1,2)), ("/", size, self.onOperation, (0,0)), ("*", size, self.onOperation, (0,1)), ("-", size, self.onOperation, (0,2)), ("+", (45,100), self.onOperation, None), ("Enter", (45,100), self.onCalculate, None)) def buildButton(self, label, size, handler): """ Builds a button and binds it to an event handler. Returns the button object """ button = wx.Button(self, wx.ID_ANY, label, size=size) self.Bind(wx.EVT_BUTTON, handler, button) return button
На данный момент мы должны вернуться назад и посмотреть, что это набрало нам. Оригинальный код составлял 132 строк, первый рефакторист ударил подсчет строки до 128, второй увеличил счет до 144, и этот последний отвел нас обратно до 120. Циничный может сказать, что все, что мы сохранены, – это 12 линий код. Я не согласен. Что мы в конечном итоге (будь то больше кода, чем оригинал), – это гораздо упрощенная поддержка базы кода. Это может быть изменено и хранится намного проще, чем оригинал.
Я надеюсь, что этот пост помогли вам посмотреть, как рефакторинг вашего кода на классы и методы могут сделать ваши программы более читабельными, проще для обслуживания – и поделиться с другими участниками, без позора!
Загрузки
- calc.zip
- Calc.tar.tar.
Дальнейшее чтение