Версия 5 от 2010-05-23 18:19:39

Убрать это сообщение

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

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

Код 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 является общепринятым стандартом управления взаимодействием между интерфейсом и данными.

Как в программе раздельно хранить Модель и Представление?

Начиная примерно с 1970 года, появившись в концептуальном языке Smalltalk-80, модель MVC вероятно является старейшей широко используемой объектно-ориентированной проектной моделью. Она также является одной из общепринятых моделей, так или иначе поддерживаемых почти каждым пакетом разработчика GUI (не говоря уже о большом числе других систем, например web приложений). Модель MVC является стандартом для структурирования программ, которые манипулируют информацией и отображают ее.

Что собой представляет система Модель-Представление-Контроллер?

Система MVC имеет три подсистемы. Подсистема Модель содержит то, что часто называют деловой логикой, или все данные и информацию Вашей системы. Подсистема Представление содержит объекты, которые отображают данные, а Контроллер управляет взаимодействием с пользователем и связывает между собой Модель и Представление. Таблица 5.3 резюмирует эти компоненты.

Таблица 5.3 Компоненты стандарта архитектуры MVC

Принцип

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

Модель

Деловая логика. Содержит все обрабатываемые системой данные. Она может включать интерфейс во внешнее хранилище, например, базу данных. Обычно модель предоставляет всего лишь открытый API к другим компонентам.

Представление

Код для отображения. Это виджеты (widget) – элементы интерфейса, размещающие информацию в удобном для пользователя виде. В wxPython, в основном, вся часть подсистемы представления реализована в иерархии wx.Window.

Контроллер

Логика взаимодействия. Код, который принимает события пользователя и гарантирует, что они будут обработаны системой. В wxPython эта подсистема представлена иерархией wx.EvtHandler.

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

Рисунок 5.2 показывает, как в архитектуре MVC проходят данные и информация.

Сообщения о событиях обрабатываются системой Контроллера, которая отправляет их соответствующим потребителям. Как мы видели в главе 3, wxPython управляет этим механизмом, используя метод ProcessEvent(), относящийся к wx.EvtHandler. В проекте, который строго соответствует модели MVC, Ваши функции-обработчики могут быть фактически объявлены в отдельном объекте-контроллере, а не в самом фреймовом классе.

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

Рисунок 5.2 Поток данных в модели MVC

Ключевая особенность успешного MVC проекта состоит не в обеспечении того, чтобы каждый объект знал о любом другом объекте. Наоборот, хорошая MVC программа явно скрывает информацию об одной части программы от других ее частей. Цель для такой системы состоит в минимальном взаимодействии, и к тому же с помощью строго определенного набора методов. Особенно компонент Модели должен быть полностью изолирован от Представления и Контроллера. Вы должны иметь возможность выполнять произвольные изменения в любой из этих систем, не изменяя Ваши классы Модели. Замечательно, если Вы даже будете использовать одни и те же классы Модели для управления интерфейсами, не относящимися к wxPython, но вот, что могло бы помешать, так это использование событий wxPython для нотификации изменений.

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

В следующей части мы рассмотрим один из встроенных в wxPython классов Модели – wx.grid.PyGridTableBase. Этот класс делает возможным использование в пределах проектного каркаса MVC элемента управления сетки (grid). После этого мы взглянем на построение и использование пользовательского класса модели для пользовательского виджета.

Модель wxPython: PyGridTableBase

Класс wx.grid.Grid является элементом управления wxPython для представленной электронной таблицы в виде строк и колонок. Вы вероятно знакомы с основной концепцией, но рисунок 5.3 показывает, как это выглядит в wxPython.

Рисунок 5.3 Пример сетки в wxPython

Элемент управления сетка (grid) имеет много интересных возможностей, включая возможность создания пользовательской прорисовки и редакторов, а также перетаскиваемые строки и колонки. Эти возможности будут подробно обсуждены в главе 13. В этой главе, мы рассмотрим только основы и покажем использование модели для заполнения сетки. Листинг 5.6 показывает простой немодельный способ установки в сетке значений ячейки. В этом случае, значениями сетки выступает состав команды Chicago Cubs 1984.

Листинг 5.6 Заполнение сетки без использования модели

   1 import wx
   2 import wx.grid
   3 
   4 class SimpleGrid(wx.grid.Grid):
   5     def __init__(self, parent):
   6         wx.grid.Grid.__init__(self, parent, -1)
   7         self.CreateGrid(9, 2)
   8         self.SetColLabelValue(0, "First")
   9         self.SetColLabelValue(1, "Last")
  10         self.SetRowLabelValue(0, "CF")
  11         self.SetCellValue(0, 0, "Bob")
  12         self.SetCellValue(0, 1, "Dernier")
  13         self.SetRowLabelValue(1, "2B")
  14         self.SetCellValue(1, 0, "Ryne")
  15         self.SetCellValue(1, 1, "Sandberg")
  16         self.SetRowLabelValue(2, "LF")
  17         self.SetCellValue(2, 0, "Gary")
  18         self.SetCellValue(2, 1, "Matthews")
  19         self.SetRowLabelValue(3, "1B")
  20         self.SetCellValue(3, 0, "Leon")
  21         self.SetCellValue(3, 1, "Durham")
  22         self.SetRowLabelValue(4, "RF")
  23         self.SetCellValue(4, 0, "Keith")
  24         self.SetCellValue(4, 1, "Moreland")
  25         self.SetRowLabelValue(5, "3B")
  26         self.SetCellValue(5, 0, "Ron")
  27         self.SetCellValue(5, 1, "Cey")
  28         self.SetRowLabelValue(6, "C")
  29         self.SetCellValue(6, 0, "Jody")
  30         self.SetCellValue(6, 1, "Davis")
  31         self.SetRowLabelValue(7, "SS")
  32         self.SetCellValue(7, 0, "Larry")
  33         self.SetCellValue(7, 1, "Bowa")
  34         self.SetRowLabelValue(8, "P")
  35         self.SetCellValue(8, 0, "Rick")
  36         self.SetCellValue(8, 1, "Sutcliffe")
  37 class TestFrame(wx.Frame):
  38     def __init__(self, parent):
  39         wx.Frame.__init__(self, parent, -1, "A Grid",
  40                 size=(275, 275))
  41         grid = SimpleGrid(self)
  42 if __name__ == '__main__':
  43     app = wx.PySimpleApp()
  44     frame = TestFrame(None)
  45     frame.Show(True)
  46     app.MainLoop()

В листинге 5.6, мы имеем класс SimpleGrid, подкласс класса wxPython wx.grid.Grid. Как упомянуто ранее, wx.grid.Grid имеет уйму методов, которые мы собираемся обсудить позже. Сейчас же, мы сфокусируемся на методах и которые SetRowLabelValue(), SetCellValue(), SetColLabelValue() действительно устанавливают отображаемые в сетке значения. Как видите, при сравнении рисунка 5.3 и листинга 5.6, метод SetCellValue() берет индекс строки, индекс колонки и значение, тогда как другие два метода берут только индекс и значение. При назначении индексов ячеек, заголовки строк и колонок не считаются частью сетки.

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

Решением будет wx.grid.PyGridTableBase. Как и в других классах, которые мы видели до сих пор, префикс Py указывает на то, что это – Python-оболочка (wrapper) для класса C++. Подобно классу PyEvent, который мы видели в главе 3, класс PyGridTableBase реализован, как простая Python оболочка для класса C++ wxWidgets для возможности объявления подклассов Python. PyGridTableBase - класс модели для сетки. То есть, он содержит методы, которые объект сетки использует для своей прорисовки без сведений о внутренней структуре этих данных.

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