Различия между версиями 3 и 4
Версия 3 от 2010-05-21 12:52:29
Размер: 8790
Редактор: alafin
Комментарий:
Версия 4 от 2010-05-23 18:06:30
Размер: 31852
Редактор: alafin
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 35: Строка 35:
== Пример рефакторинга ==

Чтобы показать, как эти принципы работают, мы с Вами рассмотрим пример рефакторинга. Рисунок 5.1 показывает окно, которое может быть использовано как интерфейс к базе данных, подобной Microsoft Access. Его вид чуть-чуть сложнее, чем те, которые мы видели до сих пор, но оно остается совсем простым относительно стандарта реальных приложений. Листинг 5.1 показывает слабо структурированный способ вывести окно рисунка 5.1. Когда люди говорят, что UI-код стал похож на «месиво», они знают, что говорят. Может быть всё несколько преувеличено, но это отражает проблему, с которой Вы можете столкнуться в коде размещения элементов интерфейса. И, несомненно, это отражает проблему, в которую попадаю я сам при записи кода размещения.

'''Рисунок 5.1 Образец окна как пример рефакторинга'''

'''Листинг 5.1 Нерефакторный способ воспроизвести окно из рисунка 5.1'''
{{{#!highlight python
#!/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()
}}}

Давайте, определим, как работает этот код относительно принципов таблицы 5.1. Хорошо то, что здесь нет глубокой вложенности. Плохо, что другие три идеи, указанные в таблице 5.1 совсем не выполняются. Таблица 5.2 суммирует все способы, в которых рефакторинг может улучшить данный код.

'''Таблица 5.2 Возможности рефакторинга в листинге 5.1'''

||'''Принцип'''||'''Проблема в коде'''||
||Отсутствие дублирования||Многократно дублируются несколько структур, включая: «добавить кнопку и назначить действие», «добавить пункт меню и назначить действие» и «создать пару заголовок/текст».||
||Одна операция за один раз||Этот код делает несколько вещей. Дополнительно к основной настройке фрейма, он создает строку меню (menu bar), добавляет кнопки и добавляет текстовые поля. Хуже, что эти три функции перемешали код, как будто только что были добавлены последние изменения внизу метода.||
||Сокращение количества литералов||Каждая кнопка, пункт меню и текстовый блок содержит литеральную строку и литерал указан в конструкторе.||

== Начало рефакторинга ==

Листинг 5.2 содержит код, использованный только для создания панели кнопок из предшествующего листинга. В качестве первого шага рефакторинга мы выделим этот код в свой собственный метод.

'''Листинг 5.2 Панель кнопок как отдельный метод'''
{{{#!highlight python
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)
}}}

С таким обособленным кодом легко увидеть, сколько общего имеют все определения кнопок. Мы можем вынести (факторизовать) данную общую часть в отдельный метод и просто повторно его вызывать, как показано в листинге 5.3:

'''Листинг 5.3 Улучшенный и общий методы панели кнопок'''
{{{#!highlight python
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
}}}

Есть несколько преимуществ применения второго примера вместо первого. Прежде всего, назначение кода становится ясным сразу после его прочтения - короткие методы с выразительными именами имеют большое значение в раскрытии смысла кода. Второй пример также лишен всех локальных переменных, которые нужны для связи с идентификаторами (ID) (надо сказать, Вы могли бы также избавиться от локальных переменных при помощи фиксации (hardwiring) идентификаторов, но это может вызвать проблемы их дублирования). Это полезно, поскольку делает код менее сложным, и также, поскольку это почти устраняет общую ошибку вырезания и вставки нескольких строк кода, забывая при этом изменять все имена переменных. (В реальном приложении Вам, вероятно, понадобится хранить кнопки как отдельные переменные, чтобы позже иметь к ним доступ, но в данном примере в этом нет необходимости.) Кроме того, метод buildOneButton() может быть легко перемещен в модуль для утилит и повторно использован в других фреймах или других проектах. Комплект утилит общего применения – это та вещь, которую всегда полезно иметь.

== Дальнейший рефакторинг ==

Сделав существенное улучшение, мы могли бы на этом остановиться. Но в коде все еще остается много «магических» литералов (жестко запрограммированных констант, которые используются в различных местах). Литералы указания позиции могут привести к ошибкам, прежде всего, когда в панель добавляется другая кнопка, особенно если новая кнопка размещается в середине панели. Итак, продвигаемся на один шаг вперед, и отделяем литеральные данные от обрабатывающего кода. Листинг 5.4 показывает другой механизм управления данными для создания кнопок.

'''Листинг 5.4 Создание кнопок при помощи изолированных от кода данных'''
{{{#!highlight python
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
}}}

В листинге 5.4 данные для отдельных кнопок хранятся в виде встроенного кортежа внутри метода buttonData(). Такой выбор структуры данных и использование константного метода не случайно. Данные можно было бы сохранить в виде переменной класса или модуля, что покажется лучше, чем возвращать их как результат метода, или они могли бы быть сохранены во внешнем файле. Главное преимущество в использование метода состоит в сравнительно простом переходе, т.е. если вдруг Вы захотите сохранить кнопочные данные в другом месте, то просто измените метод, так чтобы вместо возвращения константы, он возвращал внешние данные.

Метод createButtonBar() итеративно обрабатывает возвращенный buttonData() список и создает каждую кнопку из этих данных. Теперь метод автоматически при прохождении списка вычисляет позицию кнопок по оси x. Это полезное, поскольку гарантируется, что порядок кнопок в коде будет идентичен их порядку на экране, к тому же делает код более ясным и менее подверженным появлению ошибок. Если теперь Вам нужно добавить кнопку в середину панели, Вы можете просто добавить данные в середину списка, и код гарантирует, что кнопка будет установлена правильно.

Разделение данных имеет и другие преимущества. В более сложном примере, данные могли бы храниться вовне - в ресурсе или файле XML. Это позволяет изменять интерфейс даже без пересмотра кода и также облегчает интернационализацию, упрощая изменение текста. На данный момент мы все еще жестко ограничены шириной кнопки, но это можно также легко добавить к методу данных. (В действительности, мы должны, вероятно, использовать входящий в wxPython объект Sizer, который рассматривается в главе 11). createButtonBar стал теперь полезным методом и может легко использоваться в другом фрейме или проекте, не обязательно связанном с обработкой базы данных.

После выполнения тех же шагов объединения, факторинга общего процесса и разделения данных для меню и кода текстового поля, получим результат, показанный в листинге 5.5.

'''Листинг 5.5 Рефакторизованный пример'''
{{{#!highlight python
#!/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() # Упрощенный метод инициализации
                                                                 
        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)
    # 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()
}}}

Усилия для преобразования листинга 5.1 в листинг 5.5 были минимальными, но вознаграждение колоссальные – базовый код стал значительно более ясным и более устойчивым к появлению ошибок. Представление кода логически соответствует формату данных. Исключены несколько основных путей, при которых слабо структурный код может привести к ошибкам, таким как после серий копирования и вставки при создании нового объекта. Основной объем функциональности может теперь легко быть перемещен в суперкласс или во вспомогательный модуль, что сохраняет код для будущего использования. Как дополнительный бонус, разделение данных позволяет свободно использоватьпроект в качестве шаблона с разными данными, включая интернационализацию данных.

Ключ к успешному рефакторингу лежит в небольших приращениях при написании Вашего кода. Пока Вы не предпримете усилий по регулярному улучшению плохо организованного кода, он, подобно грязной посуде, может быстро нагромоздиться в беспорядочную массу. Как только Вы прочувствуете, что действующий код является только промежуточным шагом по отношению к конечной цели хорошо факторизованного рабочего кода, Вы сможете превратить рефакторизацию в часть Вашего обычного процесса разработки.

Тем не менее, даже после проведенной в листинге 5.5 рефакторизации, все еще не достает чего-то важного: фактических данных пользователя. В основном, Ваше приложение будет зависеть от управляющих данных в откликах на запросы пользователя. Структура Вашей программы может расшириться, что придаст ей гибкости и стабильности. Шаблон MVC является общепринятым стандартом управления взаимодействием между интерфейсом и данными.


Создание Вашего проекта

Эта глава включает:

  • Рефакторинг и как он улучшает программный код
  • Разделение Модели (Model) и Представления (View)
  • Применение класса Model
  • Отладка модуля GUI-программы
  • Тестирование событий пользователя

Код GUI-приложения известен своей тяжелой читабельностью, трудностью в сопровождении, и всегда выглядит похожим на длинное, волокнистое и запутанное спагетти. Один примечательный модуль GUI-программы на Python (написанный не на wxPython) содержит следующие слова в своём комментарии: "Почему же, несмотря на все усилия по поддержанию порядка GUI-кода, он всегда выходит похожим на месиво?" Этого не должно происходить. Нет никаких конкретных причин, из-за которых UI-код (интерфейсный код) может быть тяжелым для написания или обслуживания, чем любая другая часть Вашей программы. В этой главе мы обсудим три способа укрощения Вашего UI-кода.

Так как проектный код особенно восприимчив к низкому развитию своей структуры, мы обсудим рефакторинг этого кода, что облегчит его читабельность, обслуживание и сопровождение. Другая область, где UI-программист может запутаться – это взаимодействие между кодом представления и базовыми объектами бизнес-логики. Шаблон проекта Model/View/Controller (MVC) (Модель/Представление/ Контроллер) - это структура для раздельного хранения представления и данных, что позволяет изменять каждый из них без взаимного их влияния друг на друга. Наконец, мы обсудим приёмы испытания модуля с Вашим кодом на wxPython. Хотя все примеры в этой главе будут использовать wxPython, многие из принципов применимы и к любому комплекту инструментов пользовательского интерфейса (кстати, язык Python и комплект инструментов wxPython делают некоторые из этих приёмов особенно изящными).

Дизайн и архитектура Вашего кода определяют проект Вашей системы. A хорошо продуманный проект сделает Ваше приложение более простым для построения и более легким в обслуживании. Рекомендации этой главы помогут Вам спроектировать основательный проект Вашей программы.

Как рефакторинг помогает улучшить мой код?

Есть много причин, почему у хороших программистов может получиться неудачный интерфейс или код компоновки элементов. Даже простой интерфейс может потребовать много строк кода для вывода на экране всех его элементов. Программисты часто пытаются обойтись использованием единственного метода, и этот метод быстро становится длинным и трудно управляемым. Кроме того, интерфейсный код постоянно совершенствуется и изменяется, что может нанесите ущерб, если Вы не будете внимательно отслеживать все изменения. Поскольку написание кода размещения элементов интерфейса зачастую утомительно, программисту удобнее пользоваться комплектом инструментов для дизайна, генерирующим этот код. Без использования инструментов генерации кода, машинно-генерированный код покажется неуклюжим и трудным для восприятия.

В принципе, не тяжело держать UI-код под контролем. Главное это - рефакторинг или непрерывное улучшение проекта и структуры существующего кода. Цель рефакторинга - поддержание кода в состоянии, при котором он в будущем легко читается и обслуживается. Таблица 5.1 содержит описание некоторых принципов, которые нужно иметь в виду при рефакторинге. Помните, что кто-то будет читать и разбирать Ваш код в будущем. Попытайтесь облегчить жизнь другим, в конце концов, это Вам под силу.

Таблица 5.1 Список наиболее важных принципов рефакторинга

Принцип

Описание

Отсутствие дублирования

Вы должны избегать многочисленных сегментов кода с похожим функциональным назначением. Это может вызвать проблемы, когда при эксплуатации функциональное назначению нужно будет изменить.

Одна операция за один раз

Метод должен выполнять одну и только одну операцию. Другие операции должны быть перемещены в отдельные методы. Методы должны быть предельно короткими.

Ограничение глубины вложенности

Попытайтесь использовать не более двух или трёх уровней вложенности кода. Глубоко вложенный код является хорошим кандидатом для отдельного метода.

Сокращение количества литералов

Строчные и числовые литералы (константы) должны быть сведены к минимуму. Хорошим приёмом является отделение литеральных данные от основной части Вашего кода и хранение их в списке или словаре.

Некоторые из этих принципов особенно важны в коде Python. Так как синтаксис Python основан на отступах, небольшие, компактные методы очень легко читаются. Более длинные методы, однако, могут быть трудны для разбора, особенно если они не в состоянии поместиться на одном экране. Так же, глубокая вложенность в Python может затруднить отслеживание начала и окончания кодовых блоков. Тем не менее, Python является особенно хорошим языком для избежания дублирования, главным образом из-за удобства с которым функции и методы могут быть переданы как аргументы.

Пример рефакторинга

Чтобы показать, как эти принципы работают, мы с Вами рассмотрим пример рефакторинга. Рисунок 5.1 показывает окно, которое может быть использовано как интерфейс к базе данных, подобной Microsoft Access. Его вид чуть-чуть сложнее, чем те, которые мы видели до сих пор, но оно остается совсем простым относительно стандарта реальных приложений. Листинг 5.1 показывает слабо структурированный способ вывести окно рисунка 5.1. Когда люди говорят, что UI-код стал похож на «месиво», они знают, что говорят. Может быть всё несколько преувеличено, но это отражает проблему, с которой Вы можете столкнуться в коде размещения элементов интерфейса. И, несомненно, это отражает проблему, в которую попадаю я сам при записи кода размещения.

Рисунок 5.1 Образец окна как пример рефакторинга

Листинг 5.1 Нерефакторный способ воспроизвести окно из рисунка 5.1

   1 #!/usr/bin/env python
   2 import wx
   3 class RefactorExample(wx.Frame):
   4     def __init__(self, parent, id):
   5         wx.Frame.__init__(self, parent, id, 'Refactor Example',
   6                 size=(340, 200))
   7         panel = wx.Panel(self, -1)
   8         panel.SetBackgroundColour("White")
   9         prevButton = wx.Button(panel, -1, "<< PREV", pos=(80, 0))
  10         self.Bind(wx.EVT_BUTTON, self.OnPrev, prevButton)
  11         nextButton = wx.Button(panel, -1, "NEXT >>", pos=(160, 0))
  12         self.Bind(wx.EVT_BUTTON, self.OnNext, nextButton)
  13         self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  14         menuBar = wx.MenuBar()
  15         menu1 = wx.Menu()
  16         openMenuItem = menu1.Append(-1, "&Open", "Copy in status bar")
  17         self.Bind(wx.EVT_MENU, self.OnOpen, openMenuItem)
  18         quitMenuItem = menu1.Append(-1, "&Quit", "Quit")
  19         self.Bind(wx.EVT_MENU, self.OnCloseWindow, quitMenuItem)
  20         menuBar.Append(menu1, "&File")
  21         menu2 = wx.Menu()
  22         copyItem = menu2.Append(-1, "&Copy", "Copy")
  23         self.Bind(wx.EVT_MENU, self.OnCopy, copyItem)
  24         cutItem = menu2.Append(-1, "C&ut", "Cut")
  25         self.Bind(wx.EVT_MENU, self.OnCut, cutItem)
  26         pasteItem = menu2.Append(-1, "Paste", "Paste")
  27         self.Bind(wx.EVT_MENU, self.OnPaste, pasteItem)
  28         menuBar.Append(menu2, "&Edit")
  29         self.SetMenuBar(menuBar)
  30         static = wx.StaticText(panel, wx.NewId(), "First Name",
  31                 pos=(10, 50))
  32         static.SetBackgroundColour("White")
  33         text = wx.TextCtrl(panel, wx.NewId(), "", size=(100, -1),
  34                 pos=(80, 50))
  35         static2 = wx.StaticText(panel, wx.NewId(), "Last Name",
  36                 pos=(10, 80))
  37         static2.SetBackgroundColour("White")
  38         text2 = wx.TextCtrl(panel, wx.NewId(), "", size=(100, -1),
  39                 pos=(80, 80))
  40         firstButton = wx.Button(panel, -1, "FIRST")
  41         self.Bind(wx.EVT_BUTTON, self.OnFirst, firstButton)
  42         menu2.AppendSeparator()
  43         optItem = menu2.Append(-1, "&Options...", "Display Options")
  44         self.Bind(wx.EVT_MENU, self.OnOptions, optItem)
  45         lastButton = wx.Button(panel, -1, "LAST", pos=(240, 0))
  46         self.Bind(wx.EVT_BUTTON, self.OnLast, lastButton)
  47 
  48     # Just grouping the empty event handlers together
  49     def OnPrev(self, event): pass
  50     def OnNext(self, event): pass
  51     def OnLast(self, event): pass
  52     def OnFirst(self, event): pass
  53     def OnOpen(self, event): pass
  54     def OnCopy(self, event): pass
  55     def OnCut(self, event): pass
  56     def OnPaste(self, event): pass
  57     def OnOptions(self, event): pass
  58     def OnCloseWindow(self, event):
  59         self.Destroy()
  60 if __name__ == '__main__':
  61     app = wx.PySimpleApp()
  62     frame = RefactorExample(parent=None, id=-1)
  63     frame.Show()
  64     app.MainLoop()

Давайте, определим, как работает этот код относительно принципов таблицы 5.1. Хорошо то, что здесь нет глубокой вложенности. Плохо, что другие три идеи, указанные в таблице 5.1 совсем не выполняются. Таблица 5.2 суммирует все способы, в которых рефакторинг может улучшить данный код.

Таблица 5.2 Возможности рефакторинга в листинге 5.1

Принцип

Проблема в коде

Отсутствие дублирования

Многократно дублируются несколько структур, включая: «добавить кнопку и назначить действие», «добавить пункт меню и назначить действие» и «создать пару заголовок/текст».

Одна операция за один раз

Этот код делает несколько вещей. Дополнительно к основной настройке фрейма, он создает строку меню (menu bar), добавляет кнопки и добавляет текстовые поля. Хуже, что эти три функции перемешали код, как будто только что были добавлены последние изменения внизу метода.

Сокращение количества литералов

Каждая кнопка, пункт меню и текстовый блок содержит литеральную строку и литерал указан в конструкторе.

Начало рефакторинга

Листинг 5.2 содержит код, использованный только для создания панели кнопок из предшествующего листинга. В качестве первого шага рефакторинга мы выделим этот код в свой собственный метод.

Листинг 5.2 Панель кнопок как отдельный метод

   1 def createButtonBar(self):
   2     firstButton = wx.Button(panel, -1, "FIRST")
   3     self.Bind(wx.EVT_BUTTON, self.OnFirst, firstButton)
   4     prevButton = wx.Button(panel, -1, "<< PREV", pos=(80, 0))
   5     self.Bind(wx.EVT_BUTTON, , self.OnPrev, prevButton)
   6     nextButton = wx.Button(panel, -1, "NEXT >>", pos=(160, 0))
   7     self.Bind(wx.EVT_BUTTON, self.OnNext, nextButton)
   8     lastButton = wx.Button(panel, -1, "LAST", pos=(240, 0))
   9     self.Bind(wx.EVT_BUTTON, self.OnLast, lastButton)

С таким обособленным кодом легко увидеть, сколько общего имеют все определения кнопок. Мы можем вынести (факторизовать) данную общую часть в отдельный метод и просто повторно его вызывать, как показано в листинге 5.3:

Листинг 5.3 Улучшенный и общий методы панели кнопок

   1 def createButtonBar(self, panel):
   2     self.buildOneButton(panel, "First", self.OnFirst)
   3     self.buildOneButton(panel, "<< PREV", self.OnPrev, (80, 0))
   4     self.buildOneButton(panel, "NEXT >>", self.OnNext, (160, 0))
   5     self.buildOneButton(panel, "Last", self.OnLast, (240, 0))
   6 def buildOneButton(self, parent, label, handler, pos=(0,0)):
   7     button = wx.Button(parent, -1, label, pos)
   8     self.Bind(wx.EVT_BUTTON, handler, button)
   9     return button

Есть несколько преимуществ применения второго примера вместо первого. Прежде всего, назначение кода становится ясным сразу после его прочтения - короткие методы с выразительными именами имеют большое значение в раскрытии смысла кода. Второй пример также лишен всех локальных переменных, которые нужны для связи с идентификаторами (ID) (надо сказать, Вы могли бы также избавиться от локальных переменных при помощи фиксации (hardwiring) идентификаторов, но это может вызвать проблемы их дублирования). Это полезно, поскольку делает код менее сложным, и также, поскольку это почти устраняет общую ошибку вырезания и вставки нескольких строк кода, забывая при этом изменять все имена переменных. (В реальном приложении Вам, вероятно, понадобится хранить кнопки как отдельные переменные, чтобы позже иметь к ним доступ, но в данном примере в этом нет необходимости.) Кроме того, метод buildOneButton() может быть легко перемещен в модуль для утилит и повторно использован в других фреймах или других проектах. Комплект утилит общего применения – это та вещь, которую всегда полезно иметь.

Дальнейший рефакторинг

Сделав существенное улучшение, мы могли бы на этом остановиться. Но в коде все еще остается много «магических» литералов (жестко запрограммированных констант, которые используются в различных местах). Литералы указания позиции могут привести к ошибкам, прежде всего, когда в панель добавляется другая кнопка, особенно если новая кнопка размещается в середине панели. Итак, продвигаемся на один шаг вперед, и отделяем литеральные данные от обрабатывающего кода. Листинг 5.4 показывает другой механизм управления данными для создания кнопок.

Листинг 5.4 Создание кнопок при помощи изолированных от кода данных

   1 def buttonData(self):
   2     return (("First", self.OnFirst),
   3              ("<< PREV", self.OnPrev),
   4              ("NEXT >>", self.OnNext),
   5              ("Last", self.OnLast))
   6 def createButtonBar(self, panel, yPos=0):
   7     xPos = 0
   8     for eachLabel, eachHandler in self.buttonData():
   9         pos = (xPos, yPos)
  10         button = self.buildOneButton(panel, eachLabel,
  11                   eachHandler, pos)
  12         xPos += button.GetSize().width
  13 def buildOneButton(self, parent, label, handler, pos=(0,0)):
  14     button = wx.Button(parent, -1, label, pos)
  15     self.Bind(wx.EVT_BUTTON, handler, button)
  16     return button

В листинге 5.4 данные для отдельных кнопок хранятся в виде встроенного кортежа внутри метода buttonData(). Такой выбор структуры данных и использование константного метода не случайно. Данные можно было бы сохранить в виде переменной класса или модуля, что покажется лучше, чем возвращать их как результат метода, или они могли бы быть сохранены во внешнем файле. Главное преимущество в использование метода состоит в сравнительно простом переходе, т.е. если вдруг Вы захотите сохранить кнопочные данные в другом месте, то просто измените метод, так чтобы вместо возвращения константы, он возвращал внешние данные.

Метод createButtonBar() итеративно обрабатывает возвращенный buttonData() список и создает каждую кнопку из этих данных. Теперь метод автоматически при прохождении списка вычисляет позицию кнопок по оси x. Это полезное, поскольку гарантируется, что порядок кнопок в коде будет идентичен их порядку на экране, к тому же делает код более ясным и менее подверженным появлению ошибок. Если теперь Вам нужно добавить кнопку в середину панели, Вы можете просто добавить данные в середину списка, и код гарантирует, что кнопка будет установлена правильно.

Разделение данных имеет и другие преимущества. В более сложном примере, данные могли бы храниться вовне - в ресурсе или файле XML. Это позволяет изменять интерфейс даже без пересмотра кода и также облегчает интернационализацию, упрощая изменение текста. На данный момент мы все еще жестко ограничены шириной кнопки, но это можно также легко добавить к методу данных. (В действительности, мы должны, вероятно, использовать входящий в wxPython объект Sizer, который рассматривается в главе 11). createButtonBar стал теперь полезным методом и может легко использоваться в другом фрейме или проекте, не обязательно связанном с обработкой базы данных.

После выполнения тех же шагов объединения, факторинга общего процесса и разделения данных для меню и кода текстового поля, получим результат, показанный в листинге 5.5.

Листинг 5.5 Рефакторизованный пример

   1 #!/usr/bin/env python
   2 import wx
   3 class RefactorExample(wx.Frame):
   4     def __init__(self, parent, id):
   5         wx.Frame.__init__(self, parent, id, 'Refactor Example',
   6                      size=(340, 200))
   7         panel = wx.Panel(self, -1)
   8         panel.SetBackgroundColour("White")
   9         self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  10         self.createMenuBar() # Упрощенный метод инициализации
  11                                                                  
  12         self.createButtonBar(panel)
  13                                                                  
  14         self.createTextFields(panel)
  15                                                      
  16     def menuData(self): # Данные для меню
  17         return (("&File",
  18                      ("&Open", "Open in status bar", self.OnOpen),
  19                      ("&Quit", "Quit", self.OnCloseWindow)),
  20               ("&Edit",
  21                      ("&Copy", "Copy", self.OnCopy),
  22                      ("C&ut", "Cut", self.OnCut),
  23                      ("&Paste", "Paste", self.OnPaste),
  24                      ("", "", ""),
  25                      ("&Options...", "DisplayOptions", self.OnOptions)))
  26     def createMenuBar(self): # Создание меню
  27         menuBar = wx.MenuBar()                                                                
  28                                                                                               
  29         for eachMenuData in self.menuData():
  30               menuLabel = eachMenuData[0]
  31               menuItems = eachMenuData[1:]
  32               menuBar.Append(self.createMenu(menuItems), menuLabel)
  33         self.SetMenuBar(menuBar)
  34     def createMenu(self, menuData):
  35         menu = wx.Menu()
  36         for eachLabel, eachStatus, eachHandler in menuData:
  37               if not eachLabel:
  38                      menu.AppendSeparator()
  39                      continue
  40               menuItem = menu.Append(-1, eachLabel, eachStatus)
  41               self.Bind(wx.EVT_MENU, eachHandler, menuItem)
  42         return menu
  43     def buttonData(self): # Данные панели кнопок
  44         return (("First", self.OnFirst),                                                                                        
  45                  ("<< PREV", self.OnPrev),
  46                  ("NEXT >>", self.OnNext),
  47                  ("Last", self.OnLast))
  48     def createButtonBar(self, panel, yPos = 0): # Создание кнопок
  49         xPos = 0
  50         for eachLabel, eachHandler in self.buttonData():
  51               pos = (xPos, yPos)
  52               button = self.buildOneButton(panel, eachLabel,
  53                      eachHandler, pos)
  54               xPos += button.GetSize().width
  55     def buildOneButton(self, parent, label, handler, pos=(0,0)):
  56         button = wx.Button(parent, -1, label, pos)
  57         self.Bind(wx.EVT_BUTTON, handler, button)
  58         return button
  59     def textFieldData(self): # Текстовые данные
  60                                                                        
  61         return (("First Name", (10, 50)),
  62                      ("Last Name", (10, 80)))
  63                                                                                  
  64     def createTextFields(self, panel): # Создание текста
  65         for eachLabel, eachPos in self.textFieldData():
  66               self.createCaptionedText(panel, eachLabel, eachPos)
  67     def createCaptionedText(self, panel, label, pos):
  68         static = wx.StaticText(panel, wx.NewId(), label, pos)
  69         static.SetBackgroundColour("White")
  70         textPos = (pos[0] + 75, pos[1])
  71         wx.TextCtrl(panel, wx.NewId(), "", size=(100, -1),
  72              pos=textPos)
  73     # Just grouping the empty event handlers together
  74     def OnPrev(self, event): pass
  75     def OnNext(self, event): pass
  76     def OnLast(self, event): pass
  77     def OnFirst(self, event): pass
  78     def OnOpen(self, event): pass
  79     def OnCopy(self, event): pass
  80     def OnCut(self, event): pass
  81     def OnPaste(self, event): pass
  82     def OnOptions(self, event): pass
  83     def OnCloseWindow(self, event):
  84         self.Destroy()
  85         
  86 if __name__ == '__main__':
  87     app = wx.PySimpleApp()
  88     frame = RefactorExample(parent=None, id=-1)
  89     frame.Show()
  90     app.MainLoop()

Усилия для преобразования листинга 5.1 в листинг 5.5 были минимальными, но вознаграждение колоссальные – базовый код стал значительно более ясным и более устойчивым к появлению ошибок. Представление кода логически соответствует формату данных. Исключены несколько основных путей, при которых слабо структурный код может привести к ошибкам, таким как после серий копирования и вставки при создании нового объекта. Основной объем функциональности может теперь легко быть перемещен в суперкласс или во вспомогательный модуль, что сохраняет код для будущего использования. Как дополнительный бонус, разделение данных позволяет свободно использоватьпроект в качестве шаблона с разными данными, включая интернационализацию данных.

Ключ к успешному рефакторингу лежит в небольших приращениях при написании Вашего кода. Пока Вы не предпримете усилий по регулярному улучшению плохо организованного кода, он, подобно грязной посуде, может быстро нагромоздиться в беспорядочную массу. Как только Вы прочувствуете, что действующий код является только промежуточным шагом по отношению к конечной цели хорошо факторизованного рабочего кода, Вы сможете превратить рефакторизацию в часть Вашего обычного процесса разработки.

Тем не менее, даже после проведенной в листинге 5.5 рефакторизации, все еще не достает чего-то важного: фактических данных пользователя. В основном, Ваше приложение будет зависеть от управляющих данных в откликах на запросы пользователя. Структура Вашей программы может расшириться, что придаст ей гибкости и стабильности. Шаблон MVC является общепринятым стандартом управления взаимодействием между интерфейсом и данными.

Перевод: Савицкий Юрий

Книги/WxPythonInAction/Создание Вашего проекта (последним исправлял пользователь alafin 2010-05-30 07:49:33)