Is it problematic to have a dependency between objects of the same layer in a layered software architecture?

softwareengineering.stackexchange https://softwareengineering.stackexchange.com/questions/388927

Pergunta

Considering a medium-big software with an n-layer architecture and dependency injection, I am comfortable to say that an object belonging to a layer can depend on objects from lower layers but never on objects from higher layers.

But I'm not sure what to think about objects that depend on other objects of the same layer.

As an example, let's assume an application with three layers and several objects like the one in the image. Obviously top-down dependencies (green arrows) are ok, bottom-up (red arrow) are not ok, but what about a dependency inside the same layer (yellow arrow)?

enter image description here

Excluding circular dependency, I'm curious about any other issue that could arise and how much the layered architecture is being violated in this case.

Foi útil?

Solução

Yes, objects in one layer can have direct dependencies among each other, sometimes even cyclic ones - that is actually what makes the core difference to the allowed dependencies between objects in different layers, where either no direct dependencies are allowed, or just a strict dependency direction .

However, that does not mean they should have such dependencies in an arbitrary manner. It depends actually on what your layers represent, how large the system is and and what the responsibility of the parts should be. Note that "layered architecture" is a vague term, there is a huge variation of what that actually means in different kind of systems.

For example, lets say you have a "horizontally layered system", with a database layer, a business layer and a user interface (UI) layer. Lets says the UI layer contains at least several dozens different dialog classes.

One may choose a design where none of the dialog classes depend on another dialog class directly. One may choose a design where "main dialogs" and "sub dialogs" exists and there are only direct dependencies from "main" to "sub" dialogs. Or one may prefer a design where any existing UI class can use/reuse any other UI class from the same layer.

These are all possible design choices, maybe more or less sensible depending on the type of system you are building, but none of them makes the "layering" of your system invalid.

Outras dicas

I am comfortable to say that an object belonging to a layer can depend on objects from lower layers

To be honest, I don't think you should be comfortable with that. When dealing with anything but a trivial system, I'd aim to ensure all layers only ever depend on abstractions from other layers; both lower and higher.

So for example, Obj 1 should not depend on Obj 3. It should have a dependency on eg IObj 3 and should be told which implementation of that abstraction it is to work with at runtime. The thing doing the telling should be unrelated to any of the levels as it's job is to map those dependencies. That might be an IoC container, custom code called eg by main that uses pure DI. Or at a push it could even be a service locator. Regardless, the dependencies do not exist between the layers until that thing provides the mapping.

But I'm not sure what to think about objects that depend on other objects of the same layer.

I'd argue that this is the only time you should have direct dependencies. It's part of the inner workings of that layer and can be changed without affecting other layers. So it's not a harmful coupling.

Lets look at this practically

enter image description here

Obj 3 now knows Obj 4 exists. So what? Why do we care?

DIP says

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

OK but, aren't all objects abstractions?

DIP also says

"Abstractions should not depend on details. Details should depend on abstractions."

OK but, if my object is properly encapsulated doesn't that hide any details?

Some people like to blindly insist that every object needs a keyword interface. I'm not one of them. I do like to blindly insist that if you're not going to use them now you need a plan to deal with needing something like them later.

If your code is fully refactor-able on every release you can just extract interfaces later if you need them. If you have published code that you don't want to recompile and find yourself wishing you were talking through an interface you'll need a plan.

Obj 3 knows Obj 4 exists. But does Obj 3 know if Obj 4 is concrete?

This right here is why it's so nice to NOT spread new everywhere. If Obj 3 doesn't know if Obj 4 is concrete, likely because it didn't create it, then if you snuck in later and turned Obj 4 into an abstract class Obj 3 wouldn't care.

If you can do that then Obj 4 has been fully abstract all along. The only thing making an interface between them from the start gets you is the assurance that someone wont accidentally add code that gives away that Obj 4 is concrete right now. Protected constructors can mitigate that risk but that leads to another question:

Are Obj 3 and Obj 4 in the same package?

Objects are often grouped in some way (package, namespace, etc). When grouped wisely change more likely impacts within a group rather than across groups.

I like to group by feature. If Obj 3 and Obj 4 are in the same group and layer it's very unlikely that you'll have published one and not want to refactor it while needing to change only the other one. That means these objects are less likely to benefit from having an abstraction put between them before it has a clear need.

If you are crossing a group boundary though it's really a good idea to let objects on either side vary independently.

It should be that simple but unfortunately both Java and C# have made unfortunate choices that complicate this.

In C# it's tradition to name every keyword interface with an I prefix. That forces clients to KNOW they are talking to a keyword interface. That messes with the refactoring plan.

In Java it's tradition to use a better naming pattern: FooImple implements Foo However, this only helps at the source code level since Java compiles keyword interfaces to a different binary. That means when you refactor Foo from concrete to abstract clients that don't need a single character of code changed still have to be recompiled.

It's these BUGS in these particular languages that keeps people from being able put off formal abstracting until they really need it. You didn't say what language you're using but understand there are some languages that simply don't have these problems.

You didn't say what language you're using so I'll just urge you to analyze your language and situation carefully before you decide it's going to be keyword interfaces everywhere.

The YAGNI principle plays a key role here. But so does "Please make it hard to shoot myself in the foot".

Licenciado em: CC-BY-SA com atribuição
scroll top