So ran into an issue with circular dependency that looked like this:

public class A
{
    public List<B> Bs { get; private set;}

    public void AddB(B b)
    {
        Bs.Add(b);
    }

    public void RemoveB(B b)
    {
        Bs.Remove(b);
    }

    public int DoWork()
    {
        // Need details of B
        // Probably need to inject a service that gets B details...
    }
}

public class B
{
    public A A1 { get; private set; }
    public A A2 { get; private set; }

    public B(A a1, A a2)
    {
        A1 = a1;
        A2 = a2;
    }

    public void ChangeA1(A a1)
    {
        A1.RemoveB(this);
        A1 = a1;
        A1.AddB(this);
    }

    public void ChangeA2(A a2)
    {            
        A2.RemoveB(this);
        A2 = a2;
        A2.AddB(this);
    }
}

This creates a huge circular dependency and causes the entire object graph to be read into memory (multiple times). So my question is can/should this circular dependency be broken physically and still be fine logically?

For example, using IDs of the As and Bs? Obviously they will be circular dependent on each other logically, but physically it won't be required for the entire object graph to be read into memory.

I'm thinking this still isn't smart because my A class would need details of B to perform it's work so would need some type of service/repository injected. I assume the better refactoring would be to introduce a 3rd object to break the circular dependency.

I'm trying to wrap my head around solving this with a 3rd object or using value objects, but a use-case would be to delete a B from A (so I need to know which B to delete (i.e. have an identity)).

This is the general concept of what I'm trying to implement. It's for a double-entry accounting system that looks like this:

enter image description here

To clarify, A would be an Account and B would be a transaction.

So the user can view transactions from the account view and then delete a transaction or remove a different split. So it seems that a circular dependency is needed for this navigation ability.

  • You give some information about A needing to know about B but you don't explain why B needs to know about A. You should probably add that. – JimmyJames Nov 12 at 18:26
  • @JimmyJames, fixed. – keelerjr12 Nov 12 at 18:39
  • To clarify, B is a transaction (e.g., Salary) and A is a line item in your split (e.g., Expenses:Taxes:Federal)? – Dan Pichelman Nov 12 at 19:08
  • A would be an account and B would be a transaction. – keelerjr12 Nov 12 at 19:09
  • "I'm trying to wrap my head around solving this with a 3rd object or using value objects, but a use-case would be to delete a B from A (so I need to know which B to delete (i.e. have an identity))." You have to have a reference to B in order to call a method on it. I'm a little confused about the challenge you are seeing here. Can you elaborate? – JimmyJames Nov 12 at 19:15

From what I can see in the code you have provided, the only reason B needs to know about A is so that when you want to move a B to a new A, you know which A it is currently in.

You current solution is simple with the catch that it introduces this circular dependency. There are a number of other solutions to avoid this circular dependency.

One simple option is to simply iterate over all the A objects and remove the B object from any A that contains it. By using different structure to store the Bs, you can avoid the time spent iterating over every item in each list. However, since each B is in 2 As, that doesn't seem work here.

Another option is to keep a map/dict of all the Bs and which As they are in. You can keep two maps for your A1, A2 split. This might make sense here. You just need to figure out where you want to keep these maps and make sure that all changes are handled both in the A objects and in the maps consistently.

  • 1
    I think I may have solved it elegantly by introducing a Ledger object that holds Accounts and Transactions. This Ledger object acts as my API so accounts and transactions are created/removed through it. It provides a boundary from the client. Then I made a Transaction a Value Object that I can add/remove from both accounts. – keelerjr12 Nov 12 at 20:24
  • 1
    Sounds like a good solution. In general, these types of issues come from objects trying to manage how they are stored in compositions. Using a 3rd 'perspective' eliminates the need for that. – JimmyJames Nov 12 at 21:18

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.