27
votes

If I have those two projects:

MyCompany.ERP.Billing
MyCompany.ERP.Financial

Billing asks/sends information to Financial and vice-versa. Both are too big so I don't want to put them in a single project. Visual Studio doesn't allow circular references. How would you deal with that?

4
While you can employ several tricks already mentioned, this sounds like an architectural issue. How do you make a distinction between Billing and Financial? I find it somewhat hard to draw a line here, and my feeling is "Financial" should never refer to billing, because it is much less abstract. Is it possible some items reside in the wrong project? What exactly is 'too big'? - mnemosyn
That's just an example. The intention is to know how to create an ERP following good OOP practices as circular references are very common on that kind of software because it's very huge and split in many modules. - Eduardo
Circular references come from flawed design not from domain complexity. No pun intended, im not saying that this is the situation here. ERP's are not something you can develop as you go down the path, you need to have a very clear image of what you're trying to accomplish long before you write a single line of code. - devnull
I have the feeling that circular references between projects are an indication of catastrophic architectural deficiencies. Whenever I had that problem, I had to sit down and re-think my abstractions. I always found an error in them that lead to the cyclic dependency. Think law: "You're guilty!" - "What does that mean" - "You're guilty if you're convicted!" - "But why was I convicted" - "Because you're guilty". Now we have Kafka, and you don't want Kafka in your code... - mnemosyn
@Eduardo: Put simply, because one assembly must be entirely built and runnable before any assembly that references it can be. The same is not true of classes. Classes within an assembly could, in theory, be built in arbitrary order, as they're considered a part of a single unit. There is no actual unit higher than the assembly (a solution file is Visual Studio concept, not anything that relates to the generated IL). - Adam Robinson

4 Answers

25
votes

Extract interfaces from your classes and put them into a core project referenced from both Billing and Financial projects. You can then use those interfaces to share data between assemblies.

This only allows you to pass objects between those 2 assemblies, but you can't create objects from the other since you don't actually have a reference to begin with. If you want to be able to create objects you need a factory, external to those 2 projects, that handles object creation.

I would extract the business logic that needs to share the data back and forth between Billing and Financial into another project. This would make things a lot easier and would save you from resorting to all sort of tricks that make maintainability a nightmare.

5
votes

Having too large of a project shouldn't be an issue. You can keep your code structured with namespaces and different folders for the source code. In this case circular references are no longer an issue.

3
votes

The answer mentioning interfaces is correct - but if you need to be able to create both types from both projects, you'll either need to farm a factory out into yet another project (which would also reference the interfaces project but could be referenced by both of your main projects) or change the structure you're using significantly.

Something like this should work:

Finance: References Billing, Interfaces, Factory
Billing: References Finance, Interfaces, Factory
Factory: References Interfaces

Factory would have a BillingFactory.CreateInstance() As Interfaces.IBilling and also the abstract Billing class which implement Interfaces.IBilling.

The only issue I can see is if you need to do something clever when instantiating an object and don't want that logic to end up in a separate project - but as you haven't mentioned any clever logic to instantiate, this should be sufficient

0
votes

This solution could end up as a workaround for the circular reference problem. Basically you use #if logic around the code that doesn't compile unless the reference exists, and you use conditional compilation in the project file to define a variable only if the needed assembly exists. As a result, on first download from source, or after a solution clean, you must compile twice. Subsequent builds/rebuilds only require 1 build as normal. The nice thing about this is you never have to manually comment/uncomment #define statements.