Nate Meyvis

Python task manager from scratch, part 42: Resolving a circular import

In the last installment I noted that I'd run into a circular-dependency problem and fixed it the wrong way, by removing a type annotation instead of addressing the actual circularity. The problem, again, is:

  1. There is a command called AddTask. It has a field for a TaskCoordinator.
  2. The only existing subclass of TaskCoordinator, KickoffCoordinator, generates commands, including AddTask, which it checks for explicitly.

What is the circular dependency here? There are at least two good answers here:

  1. commands.py imports from coordinator.py and coordinator.py imports from commands.py.
  2. What AddTask is depends on task coordinators: that's one of its fields. But coordinators depend on commands, because they need to know the commands that exist to implement the domain-specific logic required for processing them.

These dependencies are related but different. You can remove one without removing the other (as I did before the last installment). Do not settle for removing dependency problems at the code level but leaving problems at the logical level. Dependency problems of all sorts are signs of mistakes in structure, and those will always cause more and worse problems if you don't address them.

The problem I got myself into is of a fairly simple structure: A requires or uses B, and B requires or uses A. Here are some generic causes of that problem:

  1. A and B should actually be one thing: they should be combined, or one of them should not exist at all. (Here: was I wrong to think that tasks and coordinators need to be distinguished?)
  2. A should not depend on B. (Here: perhaps our command processor should just be like a woodchipper, not needing to know much about what it's fed. Some objects work like that.)
  3. B should not depend on A. (Here: perhaps AddTask doesn't need to know what a TaskCoordinator is.)

The second of these is the easiest to eliminate. Our command processor needs to know what AddTask is. It's just not a woodchipper.

The first is also, I think, wrong. I thought pretty hard before adding the coordinator object.

That leaves the third--and, I think, revisiting my reasoning for adding a coordinator object shows why the third solution is right. What information should AddTask contain? Roughly, information about the task. Because coordinators and tasks should be different, it's too much information to add a whole TaskCoordinator as a field. Coordinators exist in part because all sorts of complicated domain-specific logic can go into scheduling tasks. A lot of it won't be relevant to the process of adding a task.

So, for example, I'd eventually like to implement coordinators that schedule new tasks based on when I have more time available and based on when I'll be doing other tasks. Certainly not all of this information, or the mechanisms to handle it, should be involved in the process of creating a new task, even if that task will be governed by that coordination logic.

If coordinators weren't intended to include other information, the first solution would probably be better: simply get rid of them and let tasks reschedule themselves.

It often happens, as it did here, that code concerns are really metaphysical concerns. I made this problem by losing track of which things needed to exist, and why.

So, I've implemented the last alternative. Here's the current commit in the veery/ repository.


Published 2022-06-05.