[Tutor] How to refactor a simple, straightforward script into a "proper" program?

Alan Gauld alan.gauld at yahoo.co.uk
Mon Jan 6 06:29:57 EST 2020


On 06/01/2020 03:25, boB Stepp wrote:

> So I have been thinking a lot about this today and find myself
> stumbling around in a confused daze despite making lists, crude
> UML-like diagrams, etc.  So questions!

OK, This switches the emphasis of the thread but lets go...

> I think the basic unit of code organization would be a ReadingMaterial
> class, choosing this name to reflect Alan's thoughts that ultimately
> there might be other things to read besides paper-based books.

Good thinking. But...

I'd create a very thin abstract class with just the interface
 - remember think about behaviour first not attributes.
Attributes are only there to support behaviour.

Then forget about eReaders etc for now and create a subclass
that deals with the concrete problem of a paper book.


> keep with the current confines of the original problem as stated, an
> object of this class would have the following user-entered attributes:
>  title, total_amount_to_read, total_amount_read, and goal_date.  I
> switched from "pages" to "amount" as my best wording to reflect a
> potential shift from pages read to percent of item read or lines read
> or whatever.  This means that I will have to throw in the concept of
> "units" if I do this generalization now.  

The units are specific to te subclass. Amount are the generalized concept.

My only question is whether the dates are relevant to the book? Or do
you need a Reader object? It is the reader who has reading goals. The
book will tell you how much to read and current position. But it is not
interested in dates.

So now we have two objects interacting. And in theory you could have
multiple readers reading the same book, and one reader reading multiple
books.

> I know the Agile people
> might say stick with today's problem (pages) and worry about other
> units when one is forced to.  So I may stick with pages after I hash
> out more important difficulties.  So back to "pages" for now.

Correct approach. Recognize that you may have another type of
ReadingMaterial to deal with in the future, but park it for
now - its a differnt class.

> First point of confusion:  the creation of the ReadingMaterial object.
> Should it start with (a) my_book = ReadingMaterial() or with (b)
> my_book = Reading_Material(title [, total_pages_to_read,
> total_pages_read, goal_date])?  The brackets indicate optionally
> starting with all four attributes from the get-go.  In other words in
> (a) the object would see to initiating the acquisition of user input
> itself, while the second has some other entity taking care of user
> input and then passing it into the instantiation process. 

Separate user interaction from logic. What happens when you want to use
this ReadingMaterial object in a GUI? So you have a ReadingMaterialView
that can deal with interacting with the user to create the book. In an
industrial version of this we probably have a database of readMaterial
and the readers select an existing book. The creation of the book is a
database(library?) admins job.

So I'd definitely opt for a separate data collection function/object
and then instantiate the object with the data already gathered.

> the proper way to proceed?  On the (a) side, ReadingMaterial knows
> what it needs and it is the natural location for future additional
> attributes that may need to be acquired someday. 

It knows what it needs but it does that by being created with it. Thats
what __init__() - ie. initialization - is for.

> direction I am currently leaning.  So the initialization of a
> ReadingMaterial object might look like:
> 
> class ReadingMaterial:
>     def __init__(self):
>         self.title = _get_input(???)
>         self.total_pages_to_read = _get_input(???)
>         self.total_pages_read = _get_input(???)
>         self.goal_date = _get_input(???)

Again, how does that work in a GUI? Or on the web?

> Based on my previous code explorations, I need to do two forms of
> input validation:  (1) syntactical, so no ValueErrors result 

Thats the job of the user interaction mechanism...

> logical:  Things like the number of pages read are less than the
> length of what is being read, etc.

That's the job of the book. It should check the logical
validity of the data and if necessary raise a ValueError.

> InputManager (yet to be fleshed out) should do (1) and ReadingMaterial
> should do (2), especially as it cannot be done until *all* of the
> input is acquired.  So what about (???) ?  Only ReadingMaterial knows
> about the details of each attribute, especially validating the "type"
> of input.  

The "input manager" knows the types required - that's part of
the creation API. So to create the object it must provide the
required types.

> Also, it seems natural that in case of type validation
> exception being thrown, the resulting message to the user should be
> customized to the input being asked for.  

The message can be added to the error when it is raised, so it is
type/value specific.

> such error messages should be stored in the ReadingMaterial class.

They are "stored" wherever you raise the exception.
(Again, being truly industrial, you store them in a separate list
and use i18n code to extract them in whatever language/locale is in use.
But that's way beyond the scope of this exercise!! And outside of the
class in any case.)

> Additionally, the input prompt for each attribute to be acquired needs
> to be customized appropriately as well.  And if I am engaging in
> "good"  OOD thinking then that suggest that (???) should minimally be
> the input type being requested (str -- not really an issue since
> input() returns a string, int, date object, etc), the customized input
> prompt and the customized error message.  I presume good practice
> would for the InputManager to pass the input prompt to a
> DisplayManager to be printed to the user's screen and that the
> InputManager would convert the user input to the requested (by
> ReadingMaterial) type, syntactically validate it, if that fails send
> the error message to the DisplayManager and await a new effort at
> valid input.  Is this how I should be thinking about this problem?

What you are working towards is the classic Model-View-Controller
architecture. Google MVC!

In essence the model is the logic and core data.
The view is what goes n the screen
The controller gets input and passes it to the view and/or model.

There are a ton of variations on this theme but its all about
keeping these elements decoupled. And you are right to be struggling,
it is hard, and that's why there is a standard model. Personally
I tend to favour the simplified version where you only have a Model
and View.
The View does all the display/interaction and the Model does all the
application stuff.

Understand that views are application specific and therefore not
usually reusable, whereas models should be reusable. So design
the dependencies such that the view depends on the model,
not the other way round.
That way you can create different views for CLI, GUI and web,
with no change to the model.

> *If* I am on the right track, where does the InputManager get
> instantiated?  DisplayManager?  Is this where in the MVC pattern, the
> program's main() would instantiate a Controller and then the
> Controller would be responsible for coordinating these?

I am answering your issues as I go and in each case you have
already guessed the response. I think thats a good thing! :-)

> Anyway this is where I am at now staring at a mostly empty split
> Neovim screen, split between reading_tracker.py (Name change per Alan
> ~(:>)) ) and test_reading_tracker.py with
> 
> ============================================================================
> #!/usr/bin/env python3
> """Pages per day needed to complete a reading on time.
> 
> [Usage message]
> 
> [Third-party or custom imports]
> 
> [A list of any classes, exception, functions, and any other objects
> exported by the module]
> class ReadingMaterial
> 
> """
> 
> class ReadingMaterial:
>     """A class to represent reading material such as books."""
> 
>     pass
> 
> 
> if __name__ == "__main__":
>     pass
> ============================================================================

That's a good start.

Now start writing some usecases. Forget about code for now.
How does the user interact with this application?
That should start to give you clues as to what methods
the classes need to support.

As a start the context diagram consists of the SUD
with Reader and ReadingMaterial/PaperBook objects.(Assume
the MVC objects are all part of the same thing for use-case
purposes)
The only(?) Actors are the Admin and User.

Users are associated with a reader which is associated with 0..N books

Books are set up by Admin. Depending on your vision Readers
(or ReadingGoals? or ReadingProjects?) may beset up by admin
(if it was a book-club or Language class maybe) or by the users.
And a user and admin could be different roles of a single person.

It is really important to have it clear in your head how
this app will work as a dialog with the user. That is fundamental to
understanding what operations it needs to support. And operations
translate to methods. And then you decide which object supports
which operations. That in turn will tell you which attributes are
needed in which class.


-- 
Alan G
Author of the Learn to Program web site
http://www.alan-g.me.uk/
http://www.amazon.com/author/alan_gauld
Follow my photo-blog on Flickr at:
http://www.flickr.com/photos/alangauldphotos



More information about the Tutor mailing list