Start with the application logic
Up to now, your programs have been a class or two, or a handful of functions. Real applications grow to hundreds or thousands of lines, and nobody can work on that much code at once. A big program stays manageable when you can work on one part of it and trust the rest to do its own job.
Think of a restaurant. The cook doesn't take orders and the waiter doesn't cook, and when the soup is too salty, everyone knows whose job it is to fix it. A program can be organized the same way. Dividing it into parts that each look after one area of the work is called separation of concerns. Functions are one tool for it and classes are another. For classes, the idea has a name of its own, the single responsibility principle: give each class one kind of work, so that there's only one kind of reason to ever change it.
Most applications have at least three kinds of work to divide:
- the application logic: the data the program is about, and the rules for handling it
- the user interface: everything the user sees and types
- storage: keeping the data in a file between runs of the program
Each of them gets a class of its own. This module builds one application this way, a step at a time: a reading log that keeps track of the books you're reading and how far you've got in each one.
Start with the logic
You can start anywhere, but the logic is a good first part. It's what the whole program is for, and you can try it out before there's any user interface at all. The reading log needs to know each book's length and how many pages of it have been read. Two dictionaries, both with the title as the key, can hold that:
class ReadingLog:
def __init__(self):
self.__lengths = {}
self.__pages_read = {}
def add_book(self, title, length):
self.__lengths[title] = length
self.__pages_read[title] = 0
def read(self, title, pages):
self.__pages_read[title] += pages
def pages_left(self, title):
return self.__lengths[title] - self.__pages_read[title]
log = ReadingLog()
log.add_book("Dune", 412)
log.add_book("Emma", 474)
log.read("Dune", 50)
log.read("Dune", 38)
print(log.pages_left("Dune"))
print(log.pages_left("Emma"))
324
474The lines after the class are there only to try it out.
Try each method right away
Run a few lines like these as soon as you've written a method. It takes a minute, and it often shows you a case you didn't think of while planning. Here, trying read with a book that was never added shows that the log has no answer for it:
class ReadingLog:
def __init__(self):
self.__lengths = {}
self.__pages_read = {}
def add_book(self, title, length):
self.__lengths[title] = length
self.__pages_read[title] = 0
def read(self, title, pages):
self.__pages_read[title] += pages
log = ReadingLog()
log.add_book("Dune", 412)
log.read("Ulysses", 20)
Traceback (most recent call last):
File "main.py", line 16, in <module>
log.read("Ulysses", 20)
~~~~~~~~^^^^^^^^^^^^^^^
File "main.py", line 11, in read
self.__pages_read[title] += pages
~~~~~~~~~~~~~~~~~^^^^^^^
KeyError: 'Ulysses'self.__pages_read[title] looks for a key that isn't there. Reading a book that isn't in the log is a mistake, but sooner or later a user will make it, so the code that calls read needs a way to check first. That's one more small method for ReadingLog:
def has_book(self, title):
return title in self.__lengths
When the class works, delete the test lines, or move them under if __name__ == "__main__":.
The logic returns its answers
ReadingLog never prints and never calls input. It gets what it needs as parameters and returns its answers, and how they're shown is up to the code that called it. That's why the same class could serve a text interface today and a web page or a test program later, without a change.