Рефакторинг с помощью паттерна MVC / tkinter 14

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Теперь, когда вся функциональность приложения готова, можно обратить внимание на проблемы в его дизайне. Например, класс App имеет несколько обязанностей: от создания экземпляров виджетов Tkinter до выполнения инструкций SQL.

Хотя кажется довольно простым и очевидным писать методы, которые бы выполняли операции от начала и до конца, этот подход приводит к тому, что код становится все сложнее поддерживать. Определить этот недостаток можно, предположив кое-какие архитектурные изменения, как например, замену реляционной базы данных на REST-бэкенд, связываться с которым приложение будет по HTTP.

Начнем с определения паттерна MVC и того, как он соотносится с разными частями приложения, которое было создано в прошлых материалах.

Паттерн отвечает за разделение приложение на три компонента, каждый из которых инкапсулирует отдельную зону ответственности, формируя троицу MVC:

  • model (модель) представляет доменные данные и содержит бизнес-правила для взаимодействия с ними. В этом примере это класс Contact и конкретный код SQLite.
  • view (представление) — графическое представление данных модели. В нашем приложении это виджеты Tkinter, которые и представляют собой графический интерфейс.
  • controller (контроллер) связывает представление и модель, получая пользовательский ввод и обновляя данные. Это — функции обратного вызова и обработчики событий, а также некоторые атрибуты.

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

В первую очередь нужно извлечь весь код, который отвечает за взаимодействие с базой данных, и поместить его в отдельный класс. Это позволит скрыть детали реализации слоя данных, оставив всего лишь 4 метода: get_contacts, add_contact, update_contact и delete_contact:


class ContactsRepository(object):
def __init__(self, conn):
self.conn = conn

def to_values(self, c):
return c.last_name, c.first_name, c.email, c.phone

def get_contacts(self):
sql = """SELECT rowid, last_name, first_name, email, phone
FROM contacts"""
for row in self.conn.execute(sql):
contact = Contact(*row[1:])
contact.rowid = row[0]
yield contact

def add_contact(self, contact):
sql = "INSERT INTO contacts VALUES (?, ?, ?, ?)"
with self.conn:
cursor = self.conn.cursor()
cursor.execute(sql, self.to_values(contact))
contact.rowid = cursor.lastrowid
return contact

def update_contact(self, contact):
sql = """UPDATE contacts
SET last_name = ?, first_name = ?, email = ?, phone = ?
WHERE rowid = ?"""
with self.conn:
self.conn.execute(sql, self.to_values(contact) + (contact.rowid,))
return contact

def delete_contact(self, contact):
sql = "DELETE FROM contacts WHERE rowid = ?"
with self.conn:
self.conn.execute(sql, (contact.rowid,))

Это, вместе с классом Contact, станет моделью приложения.
Представление будет включать лишь код, необходимый для отображения графического интерфейса, и методы контроллера для обновления. Переименуем класс в ContactsView, что лучше выразит его назначение:


class ContactsView(tk.Tk):
def __init__(self):
super().__init__()
self.title("Список контактов")
self.list = ContactList(self, height=15)
self.form = UpdateContactForm(self)
self.btn_new = tk.Button(self, text="Добавить контакт")

self.list.pack(side=tk.LEFT, padx=10, pady=10)
self.form.pack(padx=10, pady=10)
self.btn_new.pack(side=tk.BOTTOM, pady=5)

def set_ctrl(self, ctrl):
self.btn_new.config(command=ctrl.create_contact)
self.list.bind_doble_click(ctrl.select_contact)
self.form.bind_save(ctrl.update_contact)
self.form.bind_delete(ctrl.delete_contact)

def add_contact(self, contact):
self.list.insert(contact)

def update_contact(self, contact, index):
self.list.update(contact, index)

def remove_contact(self, index):
self.form.clear()
self.list.delete(index)

def get_details(self):
return self.form.get_details()

def load_details(self, contact):
self.form.load_details(contact)

Также стоит обратить внимание, что пользовательский ввод обрабатывается контроллером. Для этого добавлен метод set_ctrl, который связывается с функциями обратного вызова Tkinter.
Класс ContactsController теперь будет включать весь оставшийся код — взаимодействие интерфейса и слоя с данными с атрибутами selection и contacts:


class ContactsController(object):
def __init__(self, repo, view):
self.repo = repo
self.view = view
self.selection = None
self.contacts = list(repo.get_contacts())

def create_contact(self):
new_contact = NewContact(self.view).show()
if new_contact:
contact = self.repo.add_contact(new_contact)
self.contacts.append(contact)
self.view.add_contact(contact)

def select_contact(self, index):
self.selection = index
contact = self.contacts[index]
self.view.load_details(contact)

def update_contact(self):
if not self.selection:
return
rowid = self.contacts[self.selection].rowid
update_contact = self.view.get_details()
update_contact.rowid = rowid

contact = self.repo.update_contact(update_contact)
self.contacts[self.selection] = contact
self.view.update_contact(contact, self.selection)

def delete_contact(self):
if not self.selection:
return
contact = self.contacts[self.selection]
self.repo.delete_contact(contact)
self.view.remove_contact(self.selection)

def start(self):
for c in self.contacts:
self.view.add_contact(c)
self.view.mainloop()

Создадим скрипт __main__.py, который позволит не только загружать приложение, но также запускать его из запакованного файла с помощью названия папки, где он сохранен:

# Предположим, что __main__.py находится в lesson_14
$ python lesson_14
# Или, если мы сжимаем содержимое каталога
$ python lesson_14.zip

Как работает реализация MVC

Оригинальная реализация MVC была представлена в языке программирования Smalltalk. Ее можно представить следующей схемой:

Оригинальная реализация MVC

На ней можно увидеть, что представление передает пользовательские события контроллеру, который, в свою очередь, обновляет модель. Чтобы показать эти изменения, вводится понятие паттерна наблюдателя. Это значит, что представления подписываются на то, чтобы получать уведомления при обновлении. Таким образом они запрашивают состояние модели и меняют отображаемые данные.

Существует вариация этого дизайна, где нет коммуникации между представлением и моделью. Вместо этого изменения применяются контроллером после обновления модели:

Рефакторинг с помощью паттерна MVC / tkinter 14

Такой подход называется пассивной моделью и является самым распространенным в современных MVC-приложениях, особенно веб-фреймворках. Он использовался и в этом материале, потому что он упрощает ContactsRepository и не требует серьезных изменений в классе ContactsController.

Можно было обратить внимание, что операции обновления и удаления работают благодаря полю rowid, например, в случае с методом update_contact из класса ContactsController:


def update_contact(self):
if not self.selection:
return
rowid = self.contacts[self.selection].rowid
update_contact = self.view.get_details()
update_contact.rowid = rowid

Поскольку это — особенность реализации для базы данных SQLite, ее нужно скрыть от остальных компонентов.

Решение — добавить другое поле классу Contact с именем id или contact_id (важно не забыть, что id — это еще и встроенная функция Python, поэтому некоторые редакторы могут неправильно ее подсветить).

Затем можно предположить, что это поле — полноценная часть данных и представляет собой уникальный идентификатор. А уже детали генерации можно оставить модели.

Максим
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: Почта
admin@pythonru.comAlex Zabrodin2018-10-26OnlinePython, Programming, HTML, CSS, JavaScript