Software architecture — MVC
by Don Dang
Programming languages e.g. Python, Go, Javascript, etc can be used to write scripts to hack together something quickly, and of course, they are great for that purpose. And it is perfectly fine to write scripts that are used only once, for example, to quickly clean up some junk files or folders. However, as soon as the code is not used only once, or it will be used by others, for example, writing library code to solve a particular problem. We have to think about the Software architecture of the code.
We can think of the code organization as a stack of three layers:
- In the lowest layer, is choosing some particular syntax elements or solutions for specific problems and, for example, using a for-loop or a while-loop? Which algorithm to use for sorting a list? Should data be stored in an array or dictionary? And so on.
- In the middle layer are the Software Design Principle and Design Pattern, such as using functions or classes, using appropriate design patterns to avoid code duplication and be more maintainable.
- And on the top level is Software Architecture, which defines the overall philosophy or approach of how the code works and how it solves the main problem.
For example, in the case of the Python web framework Django, under the hood, there are many design patterns and solutions used to solve particular problems. But there is also an overarching approach that’s Django’s way of doing things. For example, Django expects you to write Templates for representing your content; it provides you tools to work with data more easily. In the case of Django, its software architecture is called Model-View-Template architecture (MVT), which could be considered as a variant of the Model-View-Controller (MVC) architecture.
Model-View-Controller (MVC) architecture splits the software into three main parts.
- The Model deals with the data/database, and it is independent of the UI.
- The View is the presentation of the model in a particular format such as a chart, diagram, table, etc.
- The Controller is the logic that binds Model and View. It accepts user input and converts it into commands for the Model or View.
MVC is one of the techniques to achieve the Separation of Concern (SOC) design principle, which simply tells you not to write your program as one solid block, instead, break up the code into chunks that are finalized tiny pieces of the system each able to complete a simple distinct job. Keeping these pieces separate make it easier for developers to perform tasks independently without affecting others and the code would be more maintainable.
MVC has been widely adopted as a design for web applications. However, it was originally developed for desktop computing. So in this post, we will take a look at the implementation of MVC architecture with a simple desktop application example — Calculator application made with Python and Tkinter.
Let’s first look at the code from the first version of the calculator. We simply import the Tkinter library and then initiate a root window which will be the main window for our calculator. A variable formula
will be used as a global variable to hold our calculation string and display it to the calculator UI. There is also the calculation Label which does the displaying job of the calculator.
We also have three main functions that track down if the user clicks to the calculator’s buttons and respond accordingly.
And after that, we define all the buttons that our calculator will have together with the displayed text on them and the corresponding function that will be called when they are clicked. Finally we start the application by calling root.mainloop()
method.
from tkinter import *root = Tk()formula=""
equation = StringVar()calculation = Label(root, textvariable=equation)equation.set("0")calculation.grid(columnspan=4)#Creating buttons & functionsdef button_press(num):
# We create a gloabl variable that will be updated whenever the button is pressed
global formula
formula = formula + str(num)
equation.set(formula)def equal_button():
# We will need to use eval() to evaluate equation string and do the math
global formula
total = str(eval(formula))
equation.set(total)
formula=""def clear_button():
global formula
formula = ""
equation.set(formula)button_1 = Button(root, text="1", command=lambda: button_press(1))
button_1.grid(row=1, column=0)button_2 = Button(root, text="2", command=lambda: button_press(2))
button_2.grid(row=1, column=1)... # And so onbutton_plus = Button(root, text="+", command=lambda: button_press("+"))
button_plus.grid(row=1, column=3)... # And so onbutton_clear = Button(root, text="C", command=clear_button)
button_clear.grid(row=4, column=1)root.mainloop()
And when we run this script, we will have our little calculator look like this:
This is all good if you want to quickly build this calculator up and feel excellent about your programming skills. However, suppose you start thinking about scaling this calculator into something more, for example. In that case, a scientific calculator with a lot more buttons and functions, then putting them all in one script would be a complete mess, and debugging would be a nightmare.
And so, one of the solutions for our little calculator is to refactor the code, following the MVC architecture. We will split the script into 3 main parts; each part is represented as a Python class for this simple example. However, you can also put each part into its module. Splitting them into their modules would be preferred, especially when scaling this simple calculator into a scientific calculator.
We will name the classes exactly as Model
, View
, and Controller
, following the MVC architecture for demonstration purposes.
The Model
First of all, the Model
, by definition, deals with the data/database and it is independent of the UI. For our little calculator, we don't use any kind of database, however, if we do, the Model
is where we store all the database related code. Take a look at our application, the only that can be considered as data is the formula
string, because it is used to store input and also to display to the UI. So, while defining the Model
class, we will also rename it to input_str
, perhaps it would be more meaningful.
class Model:
def __init__(self):
self.input_str: str = "" @property
def data(self) -> str:
return self.input_str @data.setter
def data(self, value) -> None:
self.input_str = value
To make it a bit more OOP style, we will use setter and getter methods to get/set the input_str
from the Model
, and called it data
.
The View
Next step, we will construct our View
, and again, by definition, it is the presentation of the Model
in a particular format such as a chart, diagram, table, etc. Because we are using Tkinter
to construct our UI, so everything that is related to Tkinter
should be included in the View
class.
from tkinter import *
from typing import Callable...class View:
def __init__(self):
self.root = Tk()
self.equation = StringVar()def set_equation(self, value: str) -> None:
self.equation.set(value)def setup_view(self, callback: Callable) -> None:
calculation = Label(self.root, textvariable=self.equation)
self.set_equation("0")
calculation.grid(columnspan=4)
self.setup_buttons(callback)def setup_buttons(self, callback: Callable) -> None:
button_1 = Button(self.root, text="1", command=lambda: callback("1"))
button_1.grid(row=1, column=0)button_2 = Button(self.root, text="2", command=lambda: callback("2"))
button_2.grid(row=1, column=1)... # and so ondef start_main_loop(self) -> None:
self.root.mainloop()
- The
View
class once initialized will also initialize a root window for our calculator, together with aStringVar
object that will display the input data on the UI. - There is
setup_buttons
method that takes care of initializing the buttons that we're using in our calculator. Note that this method will require acontroller
to be passed in when called, because the controller will hold all the logics of what would happen if the button is clicked. - The
setup_view
is the method that takes care of initializing everything, including the buttons and the grid. Once this method is called, our calculator is ready to be shown. - The last method is
start_main_loop
which will start showing the calculator once it's called.
The Controller
Controller is the logics that binds Model and View. It accepts user input and converts it into commands for the Model
or View
. Base on that definition, we can see that our button click handler should belong to the Controller
more than to the View
. Those functions don't have any thing to do with showing the data to the UI, but instead, they are preparing the data
for the UI to show. In general, they will access to the data
in the Model
, depends on what user clicked, they will get or set the data accordingly, then feed that data to the View
to display. To keep the Controller
separate from the View
, we will create a single method that will be passed to the View
as the click handler callback. Thus, we have our Controller class defined like this:
class Controller:
def __init__(self, view: View, model: Model):
self.model = model
self.view = viewdef start(self) -> None:
"""Set up and start the view"""
self.view.setup_view(self.button_click_handler)
self.view.start_main_loop()def button_click_handler(self, value: str) -> None:
"""Redirect to the suitable handler function base on the value of the clicked button"""
if value == "=":
self._equal_button()
elif value == "C":
self._clear_button()
else:
self._button_pressed(value)def _button_pressed(self, num: str) -> None:
"""Add the value of the clicked button to the equation"""
self.model.data += str(num)
self.view.set_equation(self.model.data)def _equal_button(self) -> None:
"""Evaluate the equation and show the result"""
total = str(eval(self.model.data))
self.view.set_equation(total)
self.model.data = ""def _clear_button(self) -> None:
"""Clear out the sreen of the calculator"""
self.model.data = ""
self.view.set_equation(self.model.data)
The Controller binds the Model
and the View
together, so when initialized, Model
and View
instances need to be passed in. We define the start method in the Controller
, which will call the setup_view
method from the View
and then start the view’s main loop. We are going to use the Controller
as following:
if __name__ == "__main__":
controller = Controller(View(), Model())
controller.start()
And so, we have completed refactoring our calculator using MVC architecture. For now, by looking at the classes, you will quickly know where to put new codes if you want to scale this calculator up. For example, you need more buttons, put them in the View
, if the new buttons are connected the new new functions, put the new functions in the Controller
. If you want to have a mechanism to store the calculation in a database, put the code in the Model.
You can find the full code for the new version here.
And ideally, you would have your project structured like this for better code management:
src
┣ calculator
┃ ┣ __init__.py
┃ ┣ controllers.py
┃ ┣ models.py
┃ ┗ views.py
┗ main.py
This concludes this post about MVC architecture, hopefully it would be helpful for your career. Happy coding.