This section contains a reproduction of the original task list kata.
This kata is an example of code obsessed with primitives.
A primitive is any concept technical in nature, and not relevant to your business domain. This includes integers, characters, strings, and collections (lists, sets, maps, etc.), but also things like threads, readers, writers, parsers, exceptions, and anything else purely focused on technical concerns. By contrast, the business concepts in this project, “task”, “project”, etc. should be considered part of your domain model. The domain model is the language of the business in which you operate, and using it in your code base helps you avoid speaking different languages, helping you to avoid misunderstandings. In our experience, misunderstandings are the biggest cause of bugs.
Try implementing the following features, refactoring primitives away as you go. Try not to implement any new behaviour until the code you’re about to change has been completely refactored to remove primitives, i.e. Only refactor the code you’re about to change, then make your change. Don’t refactor unrelated code.
One set of criteria to identify when primitives have been removed is to only allow primitives in constructor parameter lists, and as local variables and private fields. They shouldn’t be passed into methods or returned from methods. The only exception is true infrastructure code — code that communicates with the terminal, the network, the database, etc. Infrastructure requires serialisation to primitives, but should be treated as a special case. You could even consider your infrastructure as a separate domain, technical in nature, in which primitives are the domain.
You should try to wrap tests around the behaviour you’re refactoring. At the beginning, these will mostly be high-level system tests, but you should find yourself writing more unit tests as you proceed.
You should be able to give the list with tasks the following commands:
- Deadlines
- Give each task an optional deadline with the
deadline <ID> <date>
command. - Show all tasks due today with the
today
query.
- Customisable IDs
- Allow the user to specify an identifier that’s not a number.
- Disallow spaces and special characters from the ID.
- Deletion
- Allow users to delete tasks with the
delete <ID>
command.
- Views
- View tasks sorted by date with the
view by date
query. - View tasks sorted by deadline with the
view by deadline
query. - Optional: don’t remove the functionality that allows users to view tasks by project, but change the query to
view by project
.
Please also take into consideration the Considerations and Approaches section that can be found in the original description of this kata. At least alwyas segregate commands (that don't return anything and modify the state of the system) and queries (returning values but leaving the state of the system invariant).
Last but not least: verify all the time you have 100% test coverage. As soon as you get below 100%, you wrote production code that you did not specify/test yet!
Let's implement the requirements one by one. With this kata, keep in mind not accidentally to fall into the trap of an analysis paralysis, so keep the necessary & sufficient guideline in mind.
As discussed in the first course, let's first make a plan.
Like the stack kata, we'll start with an empty task list first. The simplest thing that could possibly work! Do not forget to run your tests/specificatons as frequently as possible, preferrably after changing each and every line!
- An new/empty task list
- Contains zero tasks / has a zero task count (hint!)
- Throws an exception when we request a task by ID
- Implement the possibility to add one or more tasks to the task list
- Verify that the task(s) is/are returned when getting the list of tasks
- Verify that the right task(s) is/are returned when retrieved by ID
- Specify we expect an exception when you try to add another task with an existing ID
- Make the task list accept the
deadline <ID> <date>
command- Throw an exception when the command token is not
deadline
- Throw an exception if the
deadline
is not followed by an integer and date - Throw an exception when task with
<ID>
does not exist - Should fire and forget when task with
<ID>
exists, but task due date must be set
- Throw an exception when the command token is not
- Make the task list accept the
today
query, which returns all tasks due today- Make the query return an empty list when invoked on an empty task list
- Make the query return the task of today after setting deadline of a task to today
- Make the query return an empty list after setting deadline of a task to tomorrow
- In order to implement the
view by date
query, we have to store the date of creation too- Add a creation date to a task
- Implement the
view by date
query - Implement the
view by deadline
query, and make sure you are able to deal with tasks that haven't set the deadline date
The main challenge here is to generalze the existing code base in (extremely) small steps. A possible solution is to use Python's unions:
class Task:
def __init__(self, id:Union[int, str], description: str) -> None:
These type cases can then be distinguished like so:
def validate_id(self, id:Union[int, str]) -> None:
if type(id) is int:
self.validate_int_id(id)
if type(id) is str:
self.validate_string_id(id)
During the implementation, you may watch to keep a very close eye on applying the following additional principles during your refactoring phase:
- Command query separation
- Single responsibility principle (from SOLID Design Principles)
Note that in the previous katas we have mainly applied the DRY and KISS principles.
You may want to move the task ID related logic into its own (value) object.
Command handling should not be a responsibility of the task list. The command handling logic should therefore also be moved to its own classes.