Your concerns are very much valid and they tell me your original easy caching solution is eventually becoming part of your architecture which naturally brings a new level of issues as you described yourself.
A good architectural solution to caching is to use annotations combined with IoC which solves several problems you described. For example:
- Allow you to better control the life-cycle of your cached objects
- Allow you to replace the caching behavior easily by changing the annotations (instead of changing the implementation)
- Let you easily configure a multi-layered cache where you could be storing in memory then disk cache for example
- Let you define the key (hash) for each method in the annotation itself
In my projects (Java or C#) I use Spring caching annotations. You can find a brief description here.
IoC is a key concept in this solution because it allows you to configure your caching system anyway you want.
In order to implement a similar solution in Python you have to find how to use annotations and search for a IoC container that allows you to build proxies. That's how the annotations work in order to intercept all method calls and provide you this particular solution for caching.
The logic was supposed to be implemented inside the entities
Yes, although it's usually more specific than that - the logic that computes the new state of an entity is supposed to be "inside" the entity.
Employees are hidden inside the Employer aggregate root
That may not be an effective model to use for this kind of problem; it might make more sense to focus on the transactions (exchanges of money), or the ledgers, rather than on the people.
I find it helpful to remember that aggregates are digital things; they are (parts of) documents that describe something, not the thing itself.
I cannot use IOC to inject stuff into my entities
Remember, inversion of control "is really just a pretentious way of saying taking an argument." We don't usually inject stuff into entities, that's true. Instead, we pass stuff to the entities as arguments (domain services).
Injecting the some implementations of SalaryCalculator to the Employer entity might be a bad idea, as the calculators might not be 'pure'. They might have references to some external resources (e.g. issue tracker)
The early discussions of domain model (Evans in the blue book, Fowler in Patterns of Enterprise Application Architecture) don't call for it to be pure. It's objects calling other objects, with the expectation of side effects.
As best I can tell, you really have two options - you can make the orchestration among objects part of the domain model, or you can treat the model itself as just a single collaborator, and manage the orchestration elsewhere.
How would you model it?
"It depends". I'd probably begin by separating out different processes
- Calculating how much compensation has accrued during some pay period
- Calculating the dispersal of some amount of compensation
- Tracking and confirming the asset transfers
Additionally, pay close attention to correctly modeling subtle distinctions; if salaried compensation, hourly compensation, and commissioned sales exist in your domain, then they should be separately identifiable in your model. "Almost the same" is just a wishy-washy way of spelling different.
Best Answer
I have seen the implementation of caches using a Proxy pattern. Particularly frameworks like AOP make use of proxies for most of these things.
According to the book Design Patterns and Elements of Reusable Object-Oriented Software the decorator and proxy pattern may look alike/
Page 216:
It looks like your purpose with the cache is to avoid giving direct access to the real subject, so it sounds more like a proxy to me.
The comment above also clearly makes evident that an important difference between a proxy and a decorator is that the proxy may be responsible for instantiating or getting access to the real subject, whereas in the case of the decorator is kind of expected that such reference will be dynamically provided.
It would seem that the relationship between the proxy and real subject is more static than in the case of the decorator.
That being said, ultimately in you case is a matter of intent more than how the design will look like. At the end the solution is to have a wrapper object (either called Proxy or Decorator) that will intercept the method and allow you to control when to gain access to a cache or not depending on your cache policy.