I have a domain layer, which my application architecture holds "sacred"; that is to say, the domain layer has no references to either data storage, or presentation concerns (vice-versa is allowed). I would like to keep it this way!
Everything was going quite well until I came to one particular requirement. Given:
public class A
{
public string IdA { get; private set; }
public void DoWork()
}
public class B
{
public string IdB { get; private set; }
public string IdA { get; private set; }
public Invaidate()
}
Under certain uncommon conditions, which A
decides, when DoWork
is called all associated B
must be invalidated. The problem, this invalidation requires persistence. Note: this edge case is the only time A
would need to be concerned with its associated B
s. The requirement for this invalidation would not be obvious to a consumer of A
.
I have come up with, and implemented, a solution (1), as well as thought of other solutions. However, I am not quite happy with it, or any of the others, and would like input.
Solution 1: A
Invalidates B
s, Calls Delegate For Persistence
Advantage: Transparent to consumer, Easy to consume
Disadvantage: Strange and inconsistent save behavior, no other class in the domain layer saves when a method is called
class A
{
private Action<B> SaveB; // Populated by ARepository when instance is retrieved, delegate points to sister repo BRepository
public List<B> Bees { get; private set; }
public DoWork()
{
// if edgeCase:
foreach (B b in Bees)
{
b.Invalidate();
SaveB(b);
}
}
}
Solution 2: A
Invalidates B
s, A
's Repository Saves Associated B
s
Advantage: Transparent to consumer, Easy to consume, Consistent Domain Layer
Disadvantage: Inconsistent persistent layer, Monolithic Persistence Antipattern*
*Perhaps the wrong anti-pattern name?
class A
{
public List<B> Bees { get; private set; }
public DoWork()
{
// if edgeCase:
foreach (B b in Bees)
b.Invalidate();
}
}
//...
class ARepository
{
Update(A a)
{
Sql.SaveA(a);
bRepository.Update(a.Bees);
}
}
Solution 3: Trigger Event On Edge Case
Advantage: No inconsistencies in domain or persistence layer behavior, No cross entity concerns
Disadvantage: Hard to consume, Exposes logic to consumer, who is supposed to be responsible for wiring up the event?!
class A
{
public Event SaveBs/EdgeCaseHappened;
public List<B> Bees { get; private set; }
public DoWork()
{
// if edgeCase:
foreach (B b in Bees)
b.Invalidate();
SaveBs.Fire();
// or just...
EdgeCaseHappened.Fire()
}
}
While I have already implemented 1, none of these three solutions seem quite right to me, is there something here that I am missing? Have I made a faulty assumption somewhere?
What is the best course of action for me too keep cross entity concerns like this maintainable and usable, while strictly isolating my business logic?
Note: this is not a DDD project
1 Answer 1
It depends on whether B
s must be invalidated right away when A
's edge case happened, or not.
If yes -- I'd try to make the project more DDD-like, at least on tactical level. In that case A
would be an aggregate root, and B
s would hide behind an aggregate boundary. So all you need is to merge the responsibilities of ARepository
and BRepository
.
If not -- I would implement something like Saga, though I'm not aware of your specifics. It's closest to your Solution 3. Since, again, I'm not aware of your semantics, I'll try to put it abstractly. Say, you have some process consisting of several steps -- hence, we have eventual consistency. One of that steps, say, A::doWork()
, could go wrong. So it requires some rollback logic to be implemented. You fire an event in that case, which is listened by some controller, who delegates the rollback domain logic to A.Bees
, so each B::invalidate()
can be invoked. Since you know in advance what business-logic is invoked, you know what repository you need -- BRepository
. Probably this solution is easier to implement if you invert the references: if it's not A
who knows its B
s, but B
s know about their A
.
-
While this is a good well thought out answer, it seems to show a DDD bias XD What I don't like about DDD is that it is a one size fits all architecture guide imo. In DDD you avoid tricky situations like the one in my question, but you greatly reduce reusibility as well.TheCatWhisperer– TheCatWhisperer2017年12月11日 13:24:09 +00:00Commented Dec 11, 2017 at 13:24
-
Um wow. I've never encountered such opinion. What do you mean by "one size fits all architecture guide"? Do you imply the prescriptive use of aggregates, entities, value objects, etc? If yes -- it's nothing but a decent OOP practices, resulting in simplicity and code maintainability. And this is (should be imo) a goal of any developer. And on that reusability thing -- it was never a goal of OOP. It's a fallacy (God forbid!)Vadim Samokhin– Vadim Samokhin2017年12月11日 13:38:56 +00:00Commented Dec 11, 2017 at 13:38
-
I mean, it works for almost any project, but also, generally the same structure. You just form your aggregate routes, and put the value objects around it. You don't have to deal with nuanced issues like this. I don't really care if reuse is a goal of OOP or not, it is a goal for me. My project would be huge if I used proper DDD.TheCatWhisperer– TheCatWhisperer2017年12月11日 13:51:40 +00:00Commented Dec 11, 2017 at 13:51
-
1Huge and maintainable :)Vadim Samokhin– Vadim Samokhin2017年12月11日 13:55:31 +00:00Commented Dec 11, 2017 at 13:55
-
Personally, I feel maintainability and amount of code are inversely related. Yes, the advantages of DDD prbly more than offset the extra code by isolating the different functions of a system. However, given in exchange, are the ability to rapidly add new features by leveraging existing structures.TheCatWhisperer– TheCatWhisperer2017年12月11日 14:03:37 +00:00Commented Dec 11, 2017 at 14:03
Explore related questions
See similar questions with these tags.