Дизайн приложения (в широком понимании этого термина) – это очень важная составляющая программы, независимо от языка, на котором она создается. И обработка проблем с ним занимает одну из ведущих ролей в процессе ее разработки. Наша программа, которая написана ранее, имеет ряд существенных проблем, которые негативно влияют на читаемость кода и дальнейшую простоту ее разработки и изменения.
Например, класс App выполняет много разных процессов, начиная созданием экземпляров элементов пользовательского интерфейса и заканчивая выполнением инструкций с использованием библиотеки SQLite.
Это очень много действий придется выполнять в рамках одной функции. И хотя может показаться довольно логичным и простым написание методов, которые выполняли бы целый спектр действий от А до Я, это может привести к значительному ухудшению читаемости кода, а также невозможности грамотно его поддерживать. Так что все зависит от перспективы, в которой человек это оценивает. Если перспектива краткосрочная, то такой подход действительно проще. Но при разработке сложных приложений это может привести к большим проблемам в дальнейшем. А учитывая то, что с базами данных работают именно сложные приложения, то проблемы при таком подходе возникнут гарантированно.
Чтобы этот недостаток свести на нет, необходимо внести некоторые коррективы в архитектуру написанного приложения. Например, заменить реляционную базу данных на REST-бэкэнд, с которым будет осуществляться связь посредством HTTP в приложении.
Давайте начнем с определения паттерна MVC и того, каким образом осуществляется правильное взаимодействие с разными частями программы, которую мы писали ранее.
С помощью этого паттерна пользователь может выполнить разделение программы на три базовых компонента, каждый из которых инкапсулирует определенные действия, совершаемые приложением. В процессе создается так называемая, троица MVC:
- model (модель). С ее помощью можно получить доменные данные и правила для взаимодействия с ними. В нашем случае это класс Contact и приведенный на прошлом этапе код SQLite.
- view (представление). Это внешний вид информации, входящую в общую модель. В нашем случае, это – компоненты библиотеки Tkinter, с помощью которых и создается графический интерфейс. Пользователь же имеет право выбрать и любую другую библиотеку, которая может использоваться для этой цели.
- controller (контроллер). С его помощью модель и представление связываются в единую интегрированную сложную среду, в результате чего появляется возможность обновлять данные посредством пользовательского ввода. Сюда относятся функции, срабатывающие на события приложения, а также – функции обратного вызова.
Итак, чтобы разделить зоны ответственности приложения (какие конкретно действия должны выполняться теми или иными функциями), необходимо выполнить процедуру рефакторинга. Чтобы это сделать, потребуется прописать дополнительный код, а затем его использовать. Тем не менее, это поможет облегчить разграничение программы на разные части, а также предотвратит головные боли из-за ненужных сложностей в дальнейшем.
Сперва необходимо нам извлечь весь код, который используется для взаимодействия с SQLite базой и разместить его в отдельном классе. Так мы скрываем особенности реализации слоя данных приложения. У нас остается только 4 метода, которые мы и будем использовать. Приведем такой пример кода для понимания, как этот процесс осуществляется.
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,)) Это – методы get_contacts, add_contact, update_contact и delete_contact.
Вместе с классом 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, что позволит нам не только лишь выполнять программу, но и осуществлять ее запуск из запакованного файла с использованием той папки, в которой он будет сохранен.
Принцип работы MVC
Изначально MVC был реализован в языке программирования Smalltalk. Схематически эту модель можно представить следующим образом.
Здесь можно увидеть, что осуществляется передача пользовательских событий контроллеру с помощью представления. После этого модель обновляется. Чтобы продемонстрировать данные изменения, используется термин «паттерн наблюдателя». Это означает, что представления при обновлении получают уведомления, что дает им возможность менять отображаемые данные.
Этот дизайн может быть реализован и другим способом, без коммуникации между моделью и представлением. Применение изменений осуществляется уже после обновления модели, за что отвечает контроллер.
Такая модель называется пассивной, и сейчас она используется очень часто. Особо активно она применяется в веб-фреймворках. В этом материалы мы его и использовали, поскольку он упрощает 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 с contact_id.
Учтите, что id – встроенная команда Python, поэтому возможна неправильная ее подсветка некоторыми редакторами кода.
Детали генерации можно оставить модели. Поле же считать полноценной частью данных, имеющей уникальный идентификатор.