1

I'm studying the MVP pattern but having a hard time following the principles in order to update in real time a progress bar. As I understand the Presenter checks if there's any update in the Model and then outputs the result, so there's no instantiation of the Presenter in the Model, only the Presenter should instantiate the Model and the View.

My question is: how should I update the progress bar by following the MVP principle? I could of course call presenter.update_progress_bar(i, total) from Model, but then it would infringe the MVP principle.

Here's a minimal working example:

PS: for now, I'm using CLI.

/main.py

import modules

def main():
    modules.View(modules.Presenter).run()

if __name__ == "__main__":
    main()

/modules/__init__.py

from modules.Model.Model import Model
from modules.Model.progressbar import ProgressBar
from modules.View.View import View
from modules.Presenter.Presenter import Presenter

/modules/Model/Model.py

class Model:
def __init__(self):
    pass

def long_process(self):
    import time
    for i in range(10):
        time.sleep(0.1)
        print("Update the progress bar.")
    return True

/modules/Model/progressbar.py

# MIT license: https://gist.github.com/vladignatyev/06860ec2040cb497f0f3
import sys
class ProgressBar:
def progress(count, total, status=''):
    bar_len = 60
    filled_len = int(round(bar_len * count / float(total)))

    percents = round(100.0 * count / float(total), 1)
    bar = '=' * filled_len + '-' * (bar_len - filled_len)

    sys.stdout.write('[%s] %s%s ...%s\r' % (bar, percents, '%', status))
    sys.stdout.flush()

/modules/View/View.py

import sys
class View:
def __init__(self, presenter):
    self.presenter = presenter(self)

def run(self):
    self.presenter.long_process()

def update_progress_bar(self, msg):
    sys.stdout.write(msg)

def hide_progress_bar(self, msg):
    sys.stdout.write(msg)

def update_status(self, msg):
    print(msg)

/modules/Presenter/Presenter.py

class Presenter:
def __init__(self, view):
    import modules
    self.model = modules.Model()
    self.view = view

def long_process(self):
    if self.model.long_process():
        self.view.update_status('Long process finished correctly')
    else:
        self.view.update_status('error')

def update_progress_bar(self, i, total):
    from modules import ProgressBar
    ProgressBar.progress(i, total)
    self.view.update_progress_bar(ProgressBar.progress(i, total))

def end_progress_bar(self):
    self.view.end_progress_bar('\n')

I could do:

class Model:
def __init__(self, presenter):
    self.presenter = presenter  # Violation of MVP

def long_process(self):
    import time
    for i in range(10):
        time.sleep(0.1)
        self.presenter.update_progress_bar(i, 10)  # Violation of MVP
        print("Update the progress bar.")
    return True

But this is wrong since the Model now instantiates the Presenter. Any suggestions?

Raphael
  • 959
  • 7
  • 21

1 Answers1

2

Use a callback:

import time

class Model:
    def long_process(self, notify=lambda current, total: None):
        for i in range(10):
            time.sleep(0.1)
            notify(i, 10)  
        return True



class Presenter:
    def long_process(self):
        result = self.model.long_process(lambda c, t: self.update_progress_bar(c, t)):
        if result:
            self.view.update_status('Long process finished correctly')
        else:
            self.view.update_status('error')

This keeps your model independant from the client code, while still allowing it (the model I mean) to notify it's caller.

As a side note, there are quite a few things in your code that are totally unpythonic:

1/ you don't have to put each class in a distinct module (it's actually considered an antipattern in Python), and even less in nested submodules (Python Zen: "flat is better than nested").

2/ you don't have to use classes when a plain function is enough (hint: Python functions are objects... actually, everything in Python is an object) - your ProgressBar class has no state and only one method, so it could just be a plain function (Python Zen: "simple is better than complex").

3/ imports should be at the top of the module, not in functions (if you have to put them in a function to solve cyclic dependancies issues then the proper solution is to rethink your design to avoid cyclic dependancies).

4/ module names should be all_lower

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
  • Thank you Bruno for the suggestion, it's working as expected. Searching for "callback function python" I could find what I needed. Searching with the correct terms is a must. Regarding you suggestions about pythonic code: (1) apply for code with thousands of lines? And (3) is kind of hard to avoid when doing things that should be done in a serial way. I will keep that in mind anyway, thank you for pointing it out. – Raphael Sep 25 '19 at 11:20
  • 1
    1/ applies to whatever code you write - but it doesn't mean you should put all your code in a single file either, just that you should first strive to organize it _logically_ (high cohesion, low coupling) first, and only "split" a module into submodules when it actually becomes too large for your tastes. Also note that a "thousands lines" class is a sure symptom your class has way too much responsabilities. Remember, _plain functions are ok_ so you can factor out low-level, state-independant code in utility functions and only keep the higher-level and state-dependant code in your classes. – bruno desthuilliers Sep 25 '19 at 12:04
  • 3/ is definitly not "hard to avoid" (at least if you properly organize your code and pay attention to such issues from the start), and I fail to see how it relates to "things that should be done in a serial way". – bruno desthuilliers Sep 25 '19 at 12:07
  • I meant thousands of lines per file, not per class. But I get your point. Do you have by any chance a link for a post with code structure that you would recommend? I found a ton but they diverge a lot, so now I'm more confused -:) Please forget my comment on point 3, I mistook it for something else. Also, do you know if there's is some kind of glossary for programming? For example, I could find the answer to this post by searching for "callback functions" however I've never heard about it in tutorials. A book maybe? Thanks. – Raphael Sep 26 '19 at 07:58
  • 1
    I'm afraid I can't help much here... wrt/ modularity, the basic rules are [high cohesion and low coupling](https://stackoverflow.com/questions/14000762), but there's no silver bullet here, it's mostly about using your own judgement, and, well, experience. But don't overthink it either - try to keep things as simple as possible, start with simple modules, you can always refactor them in subpackages later _without breaking client code_ later. Just take time at the end of a session to re-read what you've done and check if some refactoring seems needed. – bruno desthuilliers Sep 26 '19 at 08:24
  • 1
    (hint: you can turn a module into a subpackage without breaking client code by importing the public names in the subpackage's `__init__.py`) – bruno desthuilliers Sep 26 '19 at 08:25
  • 1
    wrt/ vocabulary, well, I guess that reading a lot (manuals, tutorials, wikipedia articles, forums, questions and answers here etc) is key. At least that's how I learned and that's how I'm still learning ;-) – bruno desthuilliers Sep 26 '19 at 08:27
  • Thank you. Your comments are very helpful. – Raphael Sep 26 '19 at 08:28