Скачайте код уроков с 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-приложениях, особенно веб-фреймворках. Он использовался и в этом материале, потому что он упрощает 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, поэтому некоторые редакторы могут неправильно ее подсветить).
Затем можно предположить, что это поле — полноценная часть данных и представляет собой уникальный идентификатор. А уже детали генерации можно оставить модели.