Until now we've been reading objects from our (primitive, flat-text-file) database and writing to that database and directly treating those objects as our tasks. That is:
get_all_tasks()function gives us strings;
- Our other functions accept strings;
- That's because strings are things our database knows how to persist;
- We map between tasks--the real-world things we're trying to manipulate by writing code in the first place--and those strings in our heads, on the fly.
This is a state of sin. (Though it is a very common state of sin: many large, mature codebases, written by hordes of well-respected and thoughtful programmers, in effect require their users to do the moral equivalent of this.)
You will do better:
- In part of your code, you'll do your best to represent tasks (and, later, other things) as they really are. Here, you'll mostly put aside questions of how to persist them. Rather, you'll whatever resources you have to create domain-specific objects that behave faithfully. (Faithfully to reality as you need to represent it.)
- Elsewhere, you'll write code to handle the persistence of those "domain objects."
- Then you'll write code to map between those layers.
You can think about this division functionally: The first layer does things like presenting tasks to users and prompting task objects to do computations. (A task might, e.g., calculate how long until it's due.) The second layer does things like saving and restoring tasks, handling the inevitable fiddly problems that need careful management at these stages.
You can also think about the division morally and/or metaphysically. The first layer is a Platonic realm wherein you create and manage things as they actually are. The second layer manages those objects' connection to a fundamentally dirty and clunky realm of disks and databases and CSV files.
Either way, it's essential to distinguish between these layers and do it early, right here in step 10. In practice, the trick is to do it flexibly enough to make it easy to change later--however carefully we plan, we'll be changing things often--but faithfully enough that our work isn't utterly pointless. (I've made many mistakes in both directions.)
Making a domain layer
- Create a
- Put a file called
task.pythere (so it lives at
- Put a
There's a lot to note here:
Taskis a class. We've been using classes like
listall along. In all modern programming languages, you can make your own classes also. Python is great at this. It's also great at documenting it.
- It's also a dataclass. On the one hand, it's a somewhat new feature that many programmers are not familiar with. On the other hand, (i) it's awesome and (ii) I think it makes class definitions more intuitive for the less experienced.
- The basic idea of this class is that: (i) there's such a thing as a
Task; (ii) a
description(represented by a string) and, optionally, a time when it's due (represented by a
- If you don't supply the
duefield, it will automatically be set to the
Noneobject (that's what the
= Nonepart of the last line is doing).
Adding some tests
We'll be writing at least a few lines of code to make sure that our
Task class basically works, so (in the spirit of the last post) why not preserve them as tests?
- Make a new file,
- Put some tests there:
Some notes about these:
- Again, Pytest will simply find these and run them when you run the
- The first task is, again, a sort of smoke test. It ensures that the most basic functioning of
Taskis what we think it is.
- The second test largely exists to remind us that
Taskcurrently handles a certain edge case a certain way. Maybe we'll want to preserve it, maybe not--but now it's documented and tested, and the test is easy to change later if need be. This sort of thing is an important practical aspect of testing.
- The last test ensures that behavior we definitely do want (the ability to have a
duedate/time) is there.