본문 바로가기
Practical Python Design Patterns

Model-View-Controller Pattern

by 자동매매 2023. 3. 29.

CHAPTER 19 Model-View-Controller Pattern

Always, worlds within worlds.

Clive Barker, Weaveworld

Not far along in your journey as a programmer, you begin to see some other patterns emerge more like patterns of patterns. Like this one. Most if not all programs consist of some sort of external action that initiates the program. The initiating action may or may not have data, but if it does, the program consumes that data, sometimes persisting it, sometimes not. Finally, your program has an effect on the world. That s it, basically; all programs you will ever encounter will have this pattern.

Let me give you a simple example and then one that you might not see from the start.

The first example is that of a visitor signing up to receive email updates from a website. It does not matter how the user enters the information, as from the system s perspective everything begins with a POST request hitting the program. The request contains a name and email address. Now the program is initiated as in the preceding example, and we have some extra data. The program goes through the process of checking if the email is already in the database. If not, it is persisted together with the name of the visitor. If it is, then no further action is taken. Finally, the system returns

a status code alerting the calling program that everything went as expected. Note that there are two places where the program changes the world outside of the execution of the program itself, namely the database and the response to the calling system.

We could also look at a game, where you have the game in some state and the player interacts with the game via the keyboard and mouse. These peripheral actions are translated into commands by the game engine, the commands are processed, and the game state updates, which is relayed back to the player in terms of visual, audio, and possible tactile feedback. The process remains the same: input, process, output.

299

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_19
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

Seeing programs in this way helps, since you become better at asking questions about what happens at each step in the process. Sadly, this view is too general and leaves too much out, and thus it is not completely helpful when it comes to the actual nuts and bolts development work. Still, it is a useful tool to have.

This book is called Practical Python Design Patterns, and not Abstract Programming Models, so let s get practical.

Without completely discarding the idea of the three parts of a program, different types of programs take different perspectives on the idea. Games execute a game loop, while web applications take user input and use that input together with some form of stored data to produce an output to be displayed in the user s browser window.

For the sake of this chapter, we are going to use a command-line dummy program to demonstrate the ideas I want you to internalize. Once you have a clear grasp of the basic ideas, we will implement them, using a couple of Python libraries to build a basic web application that can serve web pages from your local machine.

Our first program will simply take a name as an argument. If the name is already stored, it will welcome the visitor back; otherwise, it will tell the visitor that it is nice to meet them.

import sys

def main(name):

try:

with open(’names.dat’, ’r’) as data_file:

names = [x for x in data_file.readlines()]

except FileNotFoundError as e:

with open(’names.dat’, ’w’) as data_file:

data_file.write(name)

names = []

if name in names:

print("Welcome back {}!".format(name)) else:

print("Hi {}, it is good to meet you".format(name)) with open(’names.dat’, ’a’) as data_file:

data_file.write(name)

300
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

if __name__ == "__main__":

main(sys.argv[1])

The program uses the sys package in the standard library to expose the arguments passed to the program. The first argument is the name of the script being executed. In our example program, the first additional argument that is expected is the name of a person. We have some checks included, ensuring that some string was in fact passed

to the program. Next, we check if the program encountered the name previously, and it displays the relevant text.

Now, for all the reasons discussed throughout the book, we know that this is not good. The script is doing too much in the main function, so we proceed to decompose it into different functions.

import sys import os

def get_append_write(filename): if os.path.exists(filename): return ’a’

return ’w’

def name_in_file(filename, name): if not os.path.exists(filename):

return False

return name in read_names(filename)

def read_names(filename):

with open(filename, ’r’) as data_file:

names = data_file.read().split(’\n’)

return names

def write_name(filename, name):

with open(filename, get_append_write(filename)) as data_file:

data_file.write("{}\n".format(name))

def get_message(name):

if name_in_file(’names.dat’, name):

return "Welcome back {}!".format(name)

301
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

write_name(’names.dat’, name)

return "Hi {}, it is good to meet you".format(name)

def main(name):

print(get_message(name))

if __name__ == "__main__":

main(sys.argv[1])

I m sure you suspect that there is more to come in terms of cleaning up this code,

so why go through this process again? The reason for following these steps is that I want you to get a feel for when you need to deconstruct a function or method. If all programs stayed the way they were planned in the beginning of the project, this sense of when and how to break things into smaller parts would not be needed. Following the same logic, the process is incremental. You might start out like we did, breaking the program into functions. After a while, the program grows significantly, and now the single file becomes a burden, a good sign that you need to break the one file program into separate files. But how will you do that? We will see as we grow our initial program.

They say always be you, unless you can be a lion, in which case always be a lion.

So, in that spirit, we will now extend the program to display a special message for certain arguments, specifically lion.

import sys import os

def get_append_write(filename): if os.path.exists(filename): return ’a’

return ’w’

def name_in_file(filename, name): if not os.path.exists(filename):

return False

return name in read_names(filename)

def read_names(filename):

with open(filename, ’r’) as data_file:

names = data_file.read().split(’\n’)

302
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

return names

def write_name(filename, name):

with open(filename, get_append_write(filename)) as data_file:

data_file.write("{}\n".format(name))

def get_message(name):

if name == "lion":

return "RRRrrrrroar!"

if name_in_file(’names.dat’, name):

return "Welcome back {}!".format(name)

write_name(’names.dat’, name)

return "Hi {}, it is good to meet you".format(name)

def main(name):

print(get_message(name))

if __name__ == "__main__":

main(sys.argv[1])

Now, we finally begin to see another pattern appear. What we have here are three distinct types of functions, which are the result of separating the concerns as much as we can. We end up with code that deals with getting the greeting to return, code writing the greeting to the console, and some code to handle the flow of the program, sending the relevant requests to the data-retrieval code, getting the greeting, and sending that to the code that ultimately displays it on the console.

Model-View-Controller Skeleton

In general terms, we want to capture the pattern discussed in the previous paragraph in a reusable pattern. We also want to bake the open-closed principle into the code, and, to that end, we want to encapsulate key parts of the program in objects.

Before anything can happen, our program must receive some form of a request. This request must be interpreted and then kick off the relevant actions. All of this happens inside an object that exists solely for controlling the flow of the program. This object handles actions like requesting data, receiving data, and dispatching responses to the

303
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

command line. The object in question is all about control, and, unsurprisingly, objects of this class are called controllers. They are the glue that holds the system together, and usually the place where the most action takes place.

Here are the control functions from our previous program.

controller_ functions.py

def name_in_file(filename, name): if not os.path.exists(filename):

return False

return name in read_names(filename)

def get_message(name):

if name_in_file(’names.dat’, name):

return "Welcome back {}!".format(name)

write_name(’names.dat’, name)

return "Hi {}, it is good to meet you".format(name)

if __name__ == "__main__":

main(sys.argv[1])

These are not yet encapsulated in an object, but at least they are in one place. Now, let s proceed with the next part of the system, grouping functions as we go before we return to them and clean up the parts.

Once a request is received and the controller decides what must happen to the request, some data from the system is needed. Since we are dealing with the grouping together of code that shares functionality, we will now grab all the functions that have anything to do with the retrieval of data and place them into a function file. Often, the structural representation of data is referred to as a data model, and so the part of the program that deals with the data is called the model.

Here are the model functions from our previous program.

model_ functions.py

def get_append_write(filename): if os.path.exists(filename): return ’a’

304
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

return ’w’

def read_names(filename):

with open(filename, ’r’) as data_file:

names = data_file.read().split(’\n’)

return names

def write_name(filename, name):

with open(filename, get_append_write(filename)) as data_file:

data_file.write("{}\n".format(name))

Once the code is encapsulated into an object, you will find that as a program grows different types of data are required, and each of these gets placed into its own model object. Usually, model objects each end up in their own file.

What remains is the code that is focused on delivering some sort of information back to the user. In our case, this is done via the standard output port to the console. This, as you have guessed by now, is called the view code.

Here are the remaining view functions from our previous program.

view_ functions.py

def main(name):

print(get_message(name))

As I am certain you noticed, the grouping of functions is not as clear-cut as we would like it to be. Ideally, you should be able to change anything in either one of the groups of functions (without changing their interfaces), and this should not have any effect on the other modules.

Before we make a clean split and encapsulate the three classes of functions into separate classes, let s look at the elements that go into the pattern.

Controllers

The controller is the heart of the pattern, the part that marshalls all the other classes. Users interact with the controller, and through it the whole system. The controller takes the user input, handles all the business logic, gets data from the model, and sends the data to the view to be transformed into the representation to be returned to the user.

305
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

class GenericController(object):

def __init__(self):

self.model = GenericModel() self.view = GenericView()

def handle(self, request):

data = self.model.get_data(request) self.view.generate_response(data)

Models

Models deal with data: getting data, setting data, updating data, and deleting data. That s it. You will often see models doing more than that, which is a mistake. Your models should be a program-side interface to your data that abstracts away the need to directly interact with the data store, allowing you to switch from a file-based store to some key- value store or a full-on relational database system.

Often, a model will contain the fields used as attributes on the object, allowing you to interact with the database as with any other object, with some sort of save method to persist the data to the data store.

Our GenericModel class contains only a simple method to return some form of data.

class GenericModel(object):

def __init__(self):

pass

def get_data(self, request):

return {’request’: request}

Views

As with models, you do not want any business logic in your views. Views should only deal with the output or rendering of data passed to it into some sort of format to be returned to the user, be that print statements to the console or a 3D render to the player of a game. The format of the output does not make a major difference in what the view does. You will also be tempted to add logic to the view; this is a slippery slope and not a path you should head down.

306
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

The following is a simple GenericView class that you can use as a base for building your own views, be they for returning JSON to an HTTP call or plotting graphs for data analysis.

class GenericView(object): def __init__(self):

pass

def generate_response(self, data):

print(data)

Bringing It All Together

Finally, here is a full program using the model, view, and controller to give you an idea of how these elements interact.

import sys

class GenericController(object):

def __init__(self):

self.model = GenericModel() self.view = GenericView()

def handle(self, request):

data = self.model.get_data(request) self.view.generate_response(data)

class GenericModel(object):

def __init__(self):

pass

def get_data(self, request):

return {’request’: request}

class GenericView(object): def __init__(self):

pass

307
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

def generate_response(self, data):

print(data)

def main(name):

request_handler = GenericController() request_handler.handle(name)

if __name__ == "__main__":

main(sys.argv[1])

Now that we have a clear picture of what each of the object classes should look like, let s proceed to implement the code in our example for this chapter in terms of these object classes.

As we have done thus far, we will lead with the Controller object.

controller.py

import sys

from model import NameModel from view import GreetingView

class GreetingController(object):

def __init__(self):

self.model = NameModel() self.view = GreetingView()

def handle(self, request):

if request in self.model.get_name_list():

self.view.generate_greeting(name=request, known=True) else:

self.model.save_name(request) self.view.generate_greeting(name=request, known=False)

def main(main):

request_handler = GreetingController() Request_handler.handle(name)

if __name__ == "__main__":

main(sys.argv[1])

308
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

Next, we create the Model object that retrieves a greeting from file or returns the standard greeting.

model.py

import os

class NameModel(object):

def __init__(self):

self.filename = ’names.dat’

def _get_append_write(self):

if os.path.exists(self.filename):

return ’a’

return ’w’

def get_name_list(self):

if not os.path.exists(self.filename):

return False

with open(self.filename, ’r’) as data_file:

names = data_file.read().split(’\n’)

return names

def save_name(self, name):

with open(self.filename, self._get_append_write()) as data_file:

data_file.write("{}\n".format(name))

Lastly, we create a View object to display the greeting to the user. view.py

class GreetingView(object): def __init__(self):

pass

def generate_greeting(self, name, known): if name == "lion":

print("RRRrrrrroar!")

return

309
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

if known:

print("Welcome back {}!".format(name)) else:

print("Hi {}, it is good to meet you".format(name)) Great! If you want to make a request, start the program like this:

$ python controller.py YOUR_NAME

The first time you run this script, the response, as expected, looks like this: Hi YOUR_NAME, it is good to meet you

Before we go on, I want you to see why this is such a great idea. Let s say we grow our program, and at some time we decide to include different types of greetings that depend on the time of day. To do this, we can add a time model to the system, which would then retrieve the system time and decide if it is morning, afternoon, or evening based on

the time. This information is sent along with the name data to the view to generate the relevant greeting.

controller.py

class GreetingController(object):

def __init__(self):

self.name_model = NameModel() self.time_model = TimeModel() self.view = GreetingView()

def handle(self, request):

if request in self.name_model.get_name_list():

self.view.generate_greeting(

name=request, time_of_day=self.time_model.get_time_of_day(), known=True

)

else:

self.name_model.save_name(request) self.view.generate_greeting(

name=request,

310
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

time_of_day=self.time_model.get_time_of_day(), known=False

)

view.py

class GreetingView(object): def __init__(self):

pass

def generate_greeting(self, name, time_of_day, known):

if name == "lion":

print("RRRrrrrroar!")

return

if known:

print("Good {} welcome back {}!".format(time_of_day, name)) else:

print("Good {} {}, it is good to meet you".format(time_of_day, name))

models.py

class NameModel(object):

def __init__(self):

self.filename = ’names.dat’

def _get_append_write(self):

if os.path.exists(self.filename):

return ’a’

return ’w’

def get_name_list(self):

if not os.path.exists(self.filename):

return False

with open(self.filename, ’r’) as data_file:

names = data_file.read().split(’\n’)

return names

311
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

def save_name(self, name):

with open(self.filename, self._get_append_write()) as data_file:

data_file.write("{}\n".format(name))

class TimeModel(object): def __init__(self):

pass

def get_time_of_day(self):

time = datetime.datetime.now() if time.hour < 12:

return "morning"

if 12 <= time.hour < 18:

return "afternoon"

if time.hour >= 18:

return "evening"

Alternatively, we could use a database to store the greetings instead of saving them in different files. The beauty of it all is that it does not matter to the rest of the system what we do in terms of storage. Just to illustrate this fact, we will alter our model to store the greeting in a JSON file rather than in plain text.

Implementing this functionality will require the json package in the standard library. The hardest part of adapting this approach to programming is that you have

to decide what goes where. How much do you place in the model, or what level of processing goes into the view? Here, I like the concept of fat controllers and skinny models and views. We want models to just deal with the creation, retrieval, updating,

and deletion of data. Views should only concern themselves with displaying data. Everything else should sit in the controller. Sometimes, you might implement helper classes for the controller, and that is good, but do not let yourself get tempted into doing more than you should in a model or view.

A classic example of a model doing too much is having one model that requires another model, like having a UserProfile class that requires a User class so you know which user the profile belongs to. The temptation is there to send all the information to the UserProfile model s constructor and have it create the User instance on the fly; it is all data after all.

That would be a mistake, as you would have the model controlling some of the flow of the program, which should rest on the controller and its helper classes. A much better

312
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

solution would be to have the controller create the User instance and then force it to pass the User object to the constructor of the UserProfile model class.

The same problems could arise on the view side of the picture. The key thing to remember is that every object should have one responsibility and one responsibility only. If it is displaying information to the user, it should not do calculations on the information; if it is handling data, it should not concern itself with creating missing pieces on the fly; and if it is controlling the flow, it should not pay any attention to how data is stored or retrieved or how the data is formatted or displayed.

Parting Shots

The more cleanly you separate the concerns in your objects, and the better you isolate their purpose, the easier it will be to maintain your code, update it, or find and fix bugs.

One of the most important concepts you should take with you on your journey toward programming mastery is the paradox of the Ship of Theseus. The paradox is

stated as such: Imagine you have a ship, and on the ship they have enough lumber to completely rebuild the ship. The ship sets out, and along the way, each piece of lumber built into the ship is replaced by some of the stock carried on the ship. The replaced

piece is discarded. By the time the ship reaches its destination, not a single plank from the original ship remains. The question is, at what point do you have a new ship? When does the ship cease being the ship you set out with?

What does this have to do with programming?

You should build your programs in such a way that you can constantly swap out bits of the program without changing any other part of the code. Every period depending

on the size and scope of your project, this might mean a year or five years or six

months you want to have cycled out all the old code and replaced it with better, more elegant, and more efficient code.

Keep asking yourself how much of a pain it will be to replace the part of the code you are currently working on with something completely different. If the answer is that you hope such a replacement is not needed before you have moved on, rip up the code and start over.

313
CHAPTER 19 MODEL-VIEW-CONTROLLER PATTERN

Exercises

Swap out the view from the last implementation of the greeting

program to display its output in a graphical pop-up; you could look at pygame or pyqt for this, or any other graphics package available to you.

Alter the model to read and write from and to a JSON file rather than

a flat text file

Install SQLAlchemy and SQLite on your local system and swap out the model object so it uses the database instead of the file system.

314
CHAPTER

Publish Subscribe Pattern

Yelling is a form of publishing.

Margaret Atwood

If you think back to the observer pattern we looked at in Chapter 14, you will remember that we had an Observable class and some Observer classes. Whenever the Observable object changed its state and was polled for a change, it alerted all the observers registered with it that they needed to activate a callback.

There is no need to jump back to that chapter, as the code we are talking about is in this snippet:

class ConcreteObserver(object):

def update(self, observed):

print("Observing: {}".format(observed))

class Observable(object):

def __init__(self):

self.callbacks = set() self.changed = False

def register(self, callback):

self.callbacks.add(callback)

315

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_20

'Practical Python Design Patterns' 카테고리의 다른 글

Design Pattern Quick Reference  (0) 2023.03.29
PUBLISH–SUBSCRIBE PATTERN  (0) 2023.03.29
Visitor Pattern  (0) 2023.03.29
Template Method Pattern  (0) 2023.03.29
Strategy Pattern  (0) 2023.03.29

댓글