28

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?

Eduardo
  • 5,645
  • 4
  • 49
  • 57
  • 3
    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 Sep 15 '10 at 17:26
  • 1
    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 Sep 15 '10 at 17:43
  • 3
    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 Sep 15 '10 at 17:48
  • 4
    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 Sep 15 '10 at 17:48
  • @Eduardo: That's fine, just be aware that sometimes the issue isn't the real problem, it's the approach. You can run with your hands, but if you have feet that work then you might want to rethink your objective. – Adam Robinson Sep 15 '10 at 17:49
  • I think not allowing circular reference is a limitation. Why are they allowed in the same assembly but not between assemblies? – Eduardo Sep 15 '10 at 17:58
  • Are factories and interfaces a workaround that should be avoided? – Eduardo Sep 15 '10 at 18:09
  • @Eduardo: Circular class references are allowed. Circular assembly references are not. The problem has nothing to do with circular class references, it's with circular assembly references. – Adam Robinson Sep 15 '10 at 18:15
  • @Adam: but why are circular class references allowed but not assembly ones? – Eduardo Sep 15 '10 at 18:20
  • On the backstage VisualStudio could consider the two assemblies as a single assembly and not complain. – Eduardo Sep 15 '10 at 18:41
  • 1
    @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 Sep 15 '10 at 19:39

4 Answers4

26

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.

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
devnull
  • 2,790
  • 22
  • 25
  • And how do I create instances of Financial Objects inside Billing? Example: IAccountPayable a = new ???() – Eduardo Sep 15 '10 at 16:49
  • The data transfer objects that should be used as parameters and return values to pass between Billing and Financial should be defined in the shared assembly, along with the service interfaces. The service implementations would ideally be constructed via a dependency injection framework and provided via constructor arguments to the classes that depend on them. – StriplingWarrior Sep 15 '10 at 17:17
  • I agree this is one possibility, but it requires using a (smart) ServiceLayer and DTOs. However, if the OP is implementing DomainModel, that will not work. But whether DomainModel is the way to go or not depends on lots of criteria, you can't always choose them to be DTOs, in which the whole approach will lead to cumbersome code. Reminds me of the many perils that come with "Enterprise Integration Patterns"... – mnemosyn Sep 15 '10 at 17:34
  • The main drawback of this solution is that interfaces break encapsulation: Interfaces are public by definition and you need to expose much more than you want. Think immutable classes, value types, etc. Moreover, interfaces suggest there are different implementations, e.g. `IOrder` - are there different *types* of order? The same goes for `IOrderItem`, etc. - your code becomes unexpressive and hard to maintain. – mnemosyn Sep 15 '10 at 17:51
5

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.

PhilB
  • 11,732
  • 2
  • 19
  • 15
  • 1
    -1. While the statement is *accurate*, it misses the point of the question. – Adam Robinson Sep 15 '10 at 16:41
  • 1
    I was responding to the statement within the question "Both are too big so I don't want to put them in a single project.". I think that using folders and namespaces is the correct solution to resolve this problem. Creating additional assemblies should be done when there is a need to deploy the parts of the application independently. – PhilB Sep 15 '10 at 16:47
  • 3
    One of the issues of large projects is that many people make changes in the .csproj file, causing too many merges. – Eduardo Sep 15 '10 at 16:51
  • I'm not sure if this is the right solution for your project, but you could consider using wildcards in your csproj file rather than including the individual source code files. – PhilB Sep 15 '10 at 17:01
  • 2
    @PhilB: There are other reasons for creating additional projects aside from deployment. Are you saying that the entirety of an application should, if at all possible, be one and only one project? I think that's turning a blind eye to a number of other concerns, not the least of them being proper SOC. – Adam Robinson Sep 15 '10 at 17:34
  • 5
    +1 to try to get it back to neutral. The answer (as well as the comments) should at least be considered when circular references pop up. This doesn't deserve a downvote, IMO. – xofz Sep 15 '10 at 17:51
  • @Adam Robinson: No, I do not advocate attempting to create the entirety of the application in a single assembly. In general I would slice the application into horizontal layers that could be potentially deployed independently, but avoid vertical slices if at all possible. I will retract my original statement as it may be misleading. – PhilB Sep 15 '10 at 18:34
  • @PhilB: What do you mean with horizontal layers? – Eduardo Sep 15 '10 at 18:44
  • @Eduardo: Examples of "horizontal" layers I may use would be UI, Buisness, Data Access. An example of a vertical slice would be splitting the Customer business class into a separate assembly from the Order business class. In general I have found that the vertical slices lead to a lot of interdependent assemblies. This of course can be resolved by adding interface assemblies and factories, but it adds additional complexity. – PhilB Sep 15 '10 at 19:06
  • @PhilB: Your statement was that you would only "slice" the assemblies if there was a *need* to deploy them individually. I would argue that the vast majority of applications will *not* have these layers deployed individually, but I would still contend that the layers belong in their own assemblies. – Adam Robinson Sep 15 '10 at 19:41
  • @Adam Robinson: I agree, "need" was too strong of a word to use here. Maybe "potentially need" would have been better? I think we agree that these concerns should be separated, but I'm curious what the advantages are of separating by assembly rather than separating merely by namespace are (other than deployment of course). – PhilB Sep 15 '10 at 20:21
  • @PhilB: Separation by assembly aids in maintaining SOC, and you can define internal types and members that you can rely on being called only from within the assembly. – Adam Robinson Sep 15 '10 at 20:26
3

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

Basic
  • 26,321
  • 24
  • 115
  • 201
  • 1
    You mean something like this? MyCompany.ERP.Factories.Billing.CreateInstance(typeof(IAccountPayable)) – Eduardo Sep 15 '10 at 17:30
  • Yes - It's then the responsibility of the factory to create your concrete instance and return it. This can be called from any other location. Where the abstract AccountPayable class which implement IAccountPayable lives will depend on how complex it is - if it's simply a POCO, it can live somewhere simple (ie instead of an interfaces project, have a common or entities project). If it's complex and has its own logic, you're going to need to restructure slightly - as you wouldn't want all your business logic scattered around in common projects... – Basic Sep 15 '10 at 17:43
  • We use: a Project.BusinessLogic assembly which contains "Managers". Managers are classes which deal with entities in a Project.Entities assembly - The UI talks to managers which hold the logic for manipulating the entities. Managers can either manipulate or return entities. Entities in our case are very simple POCOs. Managers also have a CreateInstance() method which returns a new entity. – Basic Sep 15 '10 at 17:46
  • The solution in the above comment may not be appropriate in your case - as I said, it depends where your logic lives. – Basic Sep 15 '10 at 17:47
  • But if ManagerA calls ManagerB in some point and ManagerB calls ManagerA you have the same problem. – Eduardo Sep 15 '10 at 19:09
  • True - but you need to draw the line somewhere. You can't have 2 objects that refer to each other and can both manipulate each other - it's just not possible to compile. You either need to abstract using interfaces or avoid the issue with your design. The way we get around this is to have "Services" which reference multiple managers - ie managers are responsible for handling a particular entity, services manipulate multiple entities. Of course, again services can't reference each other - but this hasn't proved to be an issue for us... – Basic Sep 16 '10 at 09:57
0

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.

Community
  • 1
  • 1
TTT
  • 22,611
  • 8
  • 63
  • 69