'MVC the simplest example

I'm struggling to understand the MVC pattern. I've been working with MVC frameworks like ASP.NET MVC and Django, but project structure there is pretty much forced, so it really didn't help to understand how to build my own apps based on this pattern. To clear things up i decided to write the simplest example of my understanding of MVC (console program in Python) and figure out if there is anything wrong.

|- program:
|—— controller.py
|—— model.py
|—— view.py
|—— db.txt #simulates database

So this is my basic structure. What this program will do is display all people that are inside db.txt. I use db.txt(json) to simulate actual database.

controller.py

from model import Person
import view

def showAll():
    #gets list of all Person objects
    people_in_db = Person.getAll()
    #calls view
    return view.showAllView(people_in_db)

def start():
    view.startView()
    input = raw_input()
    if input == 'y':
        return showAll()
    else:
        return view.endView()

if __name__ == "__main__":
    #running controller function
    start()

view.py

from model import Person


def showAllView(list):
    print 'In our db we have %i users. Here they are:' % len(list)
    for item in list:
        print item.name()
def startView():
    print 'MVC - the simplest example'
    print 'Do you want to see everyone in my db?[y/n]'

def endView():
    print 'Goodbye!'    

model.py

import json

class Person(object):

    def __init__(self, first_name = None, last_name = None):
        self.first_name = first_name
        self.last_name = last_name
    #returns Person name, ex: John Doe
    def name(self):
        return ("%s %s" % (self.first_name,self.last_name))

    @classmethod
    #returns all people inside db.txt as list of Person objects
    def getAll(self):
        database = open('db.txt', 'r')
        result = []
        json_list = json.loads(database.read())
        for item in json_list:
            item = json.loads(item)
            person = Person(item['first_name'], item['last_name'])
            result.append(person)
        return result

So this is the scenario when the user wants to see all people in the db: enter image description here

Is this approach correct?



Solution 1:[1]

The MVC architecture is very broad and can change depending on the programming language and type of application you are doing, so in this case, yes your approach can be accepted as correct.

What I have learned from static typed languages is that you define the model and views as complete separate entities, and the controller takes an instance of both model and views as parameters.

What you need to ask yourself to define if your app is MVC is the following:

  • If I change something in the view do I break anything in the model?
  • If I change something in the model do I break anything in the view?
  • Is the controller communicating everything in both view and model so that they don't have to communicate with each other?

If nothing breaks and the controller does all of the communication then yes, your application is MVC.

You might want to look into design patterns such as Singleton, Factory and others that all use the MVC architecture and define ways to implement it.

Solution 2:[2]

Because you are working with data that needs persistence, you could try also using Repository design pattern which fits perfect in a MVC Architecture.

You can do it a little bit different with a Layered Architecture that also follows the Chain of Responsibility.

Layered Architecture

As showing in this image, the whole ideea is to have layers each one on top of the other.

Let's try to do a minimal Layered Architecture

For this: You can have:

> person.py -> This will be our model data

> repo.py -> Class for persistence layer that handles persons

> service.py -> Here you can handle business logic

> console.py -> Can be the view of your application, or presentation layer

Depending on the application, or the way you handle Presentation Layer You can also have a Controller on top of your Service.

Let's say you make a GUI in PySide, and in the design Gui.py file, you don't want to perform any task. For that, you can have a Controller.py that handles user input, signals and everything else. Based on the user interaction, Controller.py will use Service.py to perform tasks.

Layered Architecture Example 2

Let's make this simple example:

person.py -> Our Model Class

class Person:
    def __init__(self, name, age, id):
        self.id = id
        self.name = name
        self.age = age

repo.py -> Our Repository Class for Person

Here you can store to file, to memory, etc. You can also use decorator pattern to decorate a memory repository with a file repository that can handle another persistence type.

You can maybe add 2 functions store_to_file and load_from_file, first called after every update, and last called when object is created to populate with data our repository. But For the moment we will keep it simple.

class PersonRepository:
    def __init__(self):
        self.persons = []

    def add(self, person):
        self.persons.append(person)

    def get(self, id):
        for person in self.persons:
            if person.id == id:
                return person
        return None

    def get_all(self):
        return self.persons

    def update(self, old_person, new_person):
        self.delete(old_person.id)
        self.add(new_person)
        raise Exception("Person not found")

    def delete(self, id):
        for p in self.persons:
            if p.id == id:
                self.persons.remove(p)
                return
        raise Exception("Person not found")
service.py -> Here must lay your business logic of your application. 

Every computation, filter, CRUD and everything you want to do to your model.

# A Person Service class for Person, which is a service layer for PersonRepository
class PersonService:
    def __init__(self, repository):
        self.repository = repository

    def get(self, id):
        return self.repository.get(id)

    def get_all(self):
        return self.repository.get_all()

    def add(self, person):
        self.repository.add(person)

    def update(self, old_person, new_person):
        self.repository.update(old_person, new_person)

    def delete(self, id):
        self.repository.delete(id)

    # OTHER BUSINESS LOGIC CAN BE ADDED HERE
    def get_by_name(self, name):
        for person in self.repository.get_all():
            if person.name == name:
                return person
        return None
    
    # FILTER PERSON BY AGE
    def filter_by_age(self, age):
        persons = []
        for person in self.repository.get_all():
            if person.age == age:
                persons.append(person)
        return persons
Controller.py -> Handles Tasks from GUI

In this example it is pretty much useless since we don't have a GUI.py, but I will keep it in case someone has to deal with it.

But if you have a GUI drawn directly, let's say in PySide, you can handle signals, tasks from GUI and everything else in Controller.py. That way you will let your GUI.py be JUS A GUI, nothing more.

For Now I will put the functions from Service, so our Controller.py, in this example, will be the Service.

class PersonController:
    def __init__(self, service):
        self.service = service

    def get(self, id):
        return self.service.get(id)

    def get_all(self):
        return self.service.get_all()

    def add(self, person):
        self.service.add(person)

    def update(self, old_person, new_person):
        self.service.update(old_person, new_person)

    def delete(self, id):
        self.service.delete(id)

    def get_by_name(self, name):
        return self.service.get_by_name(name)
    
    def filter_by_age(self, age):
        return self.service.filter_by_age(age)
    
    # HANDLE TASKS / SIGNALS FROM GUI
console.py -> This will be our application view.

For this example, we will use a Console UI, not a Graphical UI.

Here lays our user interaction. This Should be the only class that interacts with user (eg. getting input from user, printing stuff, etc).

To be very clear, you don't have to use prints in your application, if you have prints in other classes, something is wrong.

You can communicate between layers when something is wrong using Exceptions, and catch them in your view.

# A Console class for PersonController
class Console:
    def __init__(self, controller):
        self.controller = controller

    def get_all(self):
        persons = self.controller.get_all()
        for person in persons:
            print(person.id, person.name, person.age)

    def get(self, id):
        person = self.controller.get(id)
        if person is None:
            print("Person not found")
        else:
            print(person.id, person.name, person.age)

    def add(self, name, age):
        person = Person(name, age, 0)
        self.controller.add(person)
        print("Person added")

    def update(self, id, name, age):
        person = Person(name, age, id)
        self.controller.update(self.controller.get(id), person)
        print("Person updated")

    def delete(self, id):
        self.controller.delete(id)
        print("Person deleted")

    def get_by_name(self, name):
        person = self.controller.get_by_name(name)
        if person is None:
            print("Person not found")
        else:
            print(person.id, person.name, person.age)
    
    def filter_by_age(self, age):
        persons = self.controller.filter_by_age(age)
        for person in persons:
            print(person.id, person.name, person.age)
    
    # Menu for user input
    def run(self):
        while True:
            print("1. Get all persons")
            print("2. Get person by id")
            print("3. Add person")
            print("4. Update person")
            print("5. Delete person")
            print("6. Get person by name")
            print("7. Filter person by age")
            print("8. Exit")
            print("Enter your choice: ", end="")
            choice = int(input())
            if choice == 1:
                self.get_all()
            # and other functions for your menu, you got the point
main.py -> Program Startup

Note How everything gets layered on top of each other.

def main():
    repository = PersonRepository()
    service = PersonService(repository)
    controller = PersonController(service)
    console = Console(controller)

    console.run()

If you also want to handle Data Validation, which is something I recommend you to, you can also use a Validator. Let's see how it works.

validator.py -> Data validation for Person
class Validator:
    def __init__(self):
        pass

    def validate(self, person):
        if person.name == "":
            raise Exception("Name cannot be empty")
        if person.age < 0:
            raise Exception("Age cannot be negative")
        if person.id < 0:
            raise Exception("Id cannot be negative")

Now just add the validator to the service, in main and just call the validate function.

def main():
    repository = PersonRepository()
    validator = Validator()
    service = PersonService(repository, validator)
    controller = PersonController(service)
    console = Console(controller)

    console.run()

Now in your service.py, just go to add, update function and call validator.validate() method, if input data is invalid, it will raise an Exception, that can be catch in our view, to give user feedback.

service.py
    def add(self, person):
        validator.validate(person) # DATA VALIDATION
        self.repository.add(person)

That's it, soo simple :). And one more step and we're good to go, Handle the exception in our view.

console.py
    def add(self, name, age):
        person = Person(name, age, 0)
        try:
            self.controller.add(person)
            print("Person added")
        except Exception as e:
            print(e)

And that's it. We have an application that can do CRUD, filtering, data validation, chain of responsability, layered architecture, mvc, etc.

You have many advantages using such an architecture, for example, if you want to change the persistence layer, to use a database for example, you can add some logic to your repository, and everything works without any changes. Or for example, if you want to add new functionalities, just add new methods to your service layer and that's it. Or if you want to change the GUI, you can do it and just use your service and it works without doing anything below your Presentation Layer.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2