43

We have been developing code using loose coupling and dependency injection.

A lot of "service" style classes have a constructor and one method that implements an interface. Each individual class is very easy to understand in isolation.

However, because of the looseness of the coupling, looking at a class tells you nothing about the classes around it or where it fits in the larger picture.

It's not easy to jump to collaborators using Eclipse because you have to go via the interfaces. If the interface is Runnable, that is no help in finding which class is actually plugged in. Really it's necessary to go back to the DI container definition and try to figure things out from there.

Here's a line of code from a dependency injected service class:-

  // myExpiryCutoffDateService was injected, 
  Date cutoff = myExpiryCutoffDateService.get();

Coupling here is as loose as can be. The expiry date be implemented literally in any manner.

Here's what it might look like in a more coupled application.

  ExpiryDateService = new ExpiryDateService();
  Date cutoff = getCutoffDate( databaseConnection, paymentInstrument );

From the tightly coupled version, I can infer that the cutoff date is somehow determined from the payment instrument using a database connection.

I'm finding code of the first style harder to understand than code of the second style.

You might argue that when reading this class, I don't need to know how the cutoff date is figured out. That's true, but if I'm narrowing in on a bug or working out where an enhancement needs to slot in, that is useful information to know.

Is anyone else experiencing this problem? What solutions have you? Is this just something to adjust to? Are there any tools to allow visualisation of the way classes are wired together? Should I make the classes bigger or more coupled?

(Have deliberately left this question container-agnostic as I'm interested in answers for any).

WW.
  • 23,793
  • 13
  • 94
  • 121

10 Answers10

35

While I don't know how to answer this question in a single paragraph, I attempted to answer it in a blog post instead: http://blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx

To summarize, I find that the most important points are:

  • Understanding a loosely coupled code base requires a different mindset. While it's harder to 'jump to collaborators' it should also be more or less irrelevant.
  • Loose coupling is all about understanding a part without understanding the whole. You should rarely need to understand it all at the same time.
  • When zeroing in on a bug, you should rely on stack traces rather than the static structure of the code in order to learn about collaborators.
  • It's the responsibility of the developers writing the code to make sure that it's easy to understand - it's not the responsibility of the developer reading the code.
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 11
    +1. Part of your issue with understanding the whole may be just in the fact that your codebase isn't organised nicely. Your collaborators should probably be in the same folder/nearby folders. if your top-level architecture looks like /models /views /controllers, it's no wonder you are confused, as you've likely got a truckload of files per folder. Cluster modules together by their relation to the problem domain, not by 'file type'. For more on this topic: http://blog.8thlight.com/uncle-bob/2011/09/30/Screaming-Architecture.html – timoxley Feb 03 '12 at 17:02
  • Perhaps our abstractions are too abstract. We largely use 4 interfaces which contain one method each: run(), get(), appendXML(XMLElement e), modify( thing). – WW. Feb 07 '12 at 03:48
  • Those sound like they fit the RAP perfectly, but in themselves, they don't much communicate the *roles* they play. Perhaps you can use parameter/variable naming for that. – Mark Seemann Feb 07 '12 at 06:14
  • 1
    The one problem with relying on stack traces is when you don't have one, and can't easily get one, and are at a stage of trying to form hypotheses to explain a failure, before you start (potentially intrusive) investigations to test them. If you're diagnosing a production-only bug with async code, there often is no meaningful stack trace available. These moments make diagnosis of loosely coupled systems especially painful. – Ian Griffiths Sep 05 '14 at 13:44
12

Some tools are aware of DI frameworks and know how to resolve dependencies, allowing you to navigate your code in a natural way. But when that isn't available, you just have to use whatever features your IDE provides as best you can.

I use Visual Studio and a custom-made framework, so the problem you describe is my life. In Visual Studio, SHIFT+F12 is my friend. It shows all references to the symbol under the cursor. After a while you get used to the necessarily non-linear navigation through your code, and it becomes second-nature to think in terms of "which class implements this interface" and "where is the injection/configuration site so I can see which class is being used to satisfy this interface dependency".

There are also extensions available for VS which provide UI enhancements to help with this, such as Productivity Power Tools. For instance, you can hover over an interface, a info box will pop up, and you can click "Implemented By" to see all the classes in your solution implementing that interface. You can double-click to jump to the definition of any of those classes. (I still usually just use SHIFT+F12 anyway).

Igby Largeman
  • 16,495
  • 3
  • 60
  • 86
  • 4
    +1. We use [ReSharper](http://www.jetbrains.com/resharper/) for this. It makes a big difference. – TrueWill Jan 14 '12 at 02:06
  • 3
    I could not live without ReSharper. I've convinced the last three teams I've been on to invest in it and it has changed their lives as well. – scottm Feb 06 '12 at 15:18
8

I just had an internal discussion about this, and ended up writing this piece, which I think is too good not to share. I'm copying it here (almost) unedited, but even though it's part of a bigger internal discussion, I think most of it can stand alone.

The discussion is about introduction of a custom interface called IPurchaseReceiptService, and whether or not it should be replaced with use of IObserver<T>.


Well, I can't say that I have strong data points about any of this - it's just some theories that I'm pursuing... However, my theory about cognitive overhead at the moment goes something like this: consider your special IPurchaseReceiptService:

public interface IPurchaseReceiptService
{
    void SendReceipt(string transactionId, string userGuid);
}

If we keep it as the Header Interface it currently is, it only has that single SendReceipt method. That's cool.

What's not so cool is that you had to come up with a name for the interface, and another name for the method. There's a bit of overlap between the two: the word Receipt appears twice. IME, sometimes that overlap can be even more pronounced.

Furthermore, the name of the interface is IPurchaseReceiptService, which isn't particularly helpful either. The Service suffix is essentially the new Manager, and is, IMO, a design smell.

Additionally, not only did you have to name the interface and the method, but you also have to name the variable when you use it:

public EvoNotifyController(
    ICreditCardService creditCardService,
    IPurchaseReceiptService purchaseReceiptService,
    EvoCipher cipher
)

At this point, you've essentially said the same thing thrice. This is, according to my theory, cognitive overhead, and a smell that the design could and should be simpler.

Now, contrast this to use of a well-known interface like IObserver<T>:

public EvoNotifyController(
    ICreditCardService creditCardService,
    IObserver<TransactionInfo> purchaseReceiptService,
    EvoCipher cipher
)

This enables you to get rid of the bureaucracy and reduce the design the the heart of the matter. You still have intention-revealing naming - you only shift the design from a Type Name Role Hint to an Argument Name Role Hint.


When it comes to the discussion about 'disconnectedness', I'm under no illusion that use of IObserver<T> will magically make this problem go away, but I have another theory about this.

My theory is that the reason many programmers find programming to interfaces so difficult is exactly because they are used to Visual Studio's Go to definition feature (incidentally, this is yet another example of how tooling rots the mind). These programmers are perpetually in a state of mind where they need to know what's 'on the other side of an interface'. Why is this? Could it be because the abstraction is poor?

This ties back to the RAP, because if you confirm programmers' belief that there's a single, particular implementation behind every interface, it's no wonder they think that interfaces are only in the way.

However, if you apply the RAP, I hope that slowly, programmers will learn that behind a particular interface, there may be any implementation of that interface, and their client code must be able to handle any implementation of that interface without changing the correctness of the system. If this theory holds, we've just introduced the Liskov Substitution Principle into a code base without scaring anyone with high-brow concepts they don't understand :)

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 1
    I'm not saying the interface goo is better, but OnNext(TransactionInfo value) is communicating ```we're observing transactions that happened``` whereas SendReceipt communicates ```this will cause a receipt to be sent```. 'purchaseReceiptService' is not telling me that in either case. My 2 cents. – Yves Reynhout Sep 04 '14 at 07:27
  • The politically-correct answer to "These programmers are perpetually in a state of mind where they need to know what's 'on the other side of an interface'. Why?" is probably "because the unit tests are lacking". I'm a big fan of documentation when needed and readable naming to reduce the instance of documentation so, no to "Could it be because the abstraction is poor?". I think it far more likely that the concrete names are poor, obscuring semantics of the abstraction. – Andy Dent Sep 04 '14 at 13:30
  • On this basis, you would create interfaces based only on the signature. Combined with generics, you only need a few. Run, Process, Provider, Calculator. The interface names cease to convey any meaning. But managing complexity is about finding *useful* abstractions. These are abstractions so abstract they start becoming useless. – WW. Sep 04 '14 at 23:12
  • @WW. The abstractions are still named. They are just named by their variable names instead of their types. Code written in lots of programming languages (i.e. all dynamically typed languages) work like this. – Mark Seemann Sep 05 '14 at 03:14
7

However, because of the looseness of the coupling, looking at a class tells you nothing about the classes around it or where it fits in the larger picture.

This is not accurate.For each class you know exactly what kind of objects the class depends on, to be able to provide its functionality at runtime.
You know them since you know that what objects are expected to be injected.

What you don't know is the actual concrete class that will be injected at runtime which will implement the interface or base class that you know your class(es) depend on.

So if you want to see what is the actual class injected, you just have to look at the configuration file for that class to see the concrete classes that are injected.

You could also use facilities provided by your IDE.
Since you refer to Eclipse then Spring has a plugin for it, and has also a visual tab displaying the beans you configure. Did you check that? Isn't it what you are looking for?

Also check out the same discussion in Spring Forum

UPDATE:
Reading your question again, I don't think that this is a real question.
I mean this in the following manner.
Like all things loose coupling is not a panacea and has its own disadvantages per se.
Most tend to focus on the benefits but as any solution it has its disadvantages.

What you do in your question is describe one of its main disadvantages which is that it indeed is not easy to see the big picture since you have everything configurable and plugged in by anything.
There are other drawbacks as well that one could complaint e.g. that it is slower than tight coupled applications and still be true.

In any case, re-iterating, what you describe in your question is not a problem you stepped upon and can find a standard solution (or any for that manner).

It is one of the drawbacks of loose coupling and you have to decide if this cost is higher than what you actually gain by it, like in any design-decision trade off.

It is like asking:
Hey I am using this pattern named Singleton. It works great but I can't create new objects!How can I get arround this problem guys????
Well you can't; but if you need to, perhaps singleton is not for you....

Peter O.
  • 32,158
  • 14
  • 82
  • 96
Cratylus
  • 52,998
  • 69
  • 209
  • 339
  • +1. Maybe the interfaces just need better documentation so you know what you can expect from classes that were injected? – Ocelot20 Feb 01 '12 at 20:46
  • In my question I should probably have said "nothing about the implementation of the classes around it". Intellectually I know that this shouldn't matter, but it still makes the code slower to read and understand when looking around for the right way to add a new feature or fix a bug, etc. – WW. Feb 03 '12 at 02:55
  • Your update is interesting. You appear to be saying it's obvious. Nothing I read about loose coupling mentioned this as a downside and thus we discovered it once code was written. I think it's worth understanding how to improve problem areas or perhaps we have made the coupling too loose. – WW. Feb 06 '12 at 22:03
  • @WW.:As mentioned all approaches has pros and cons.Even OO does not fit everywhere e.g. microprogramming.Everything has a trade off.Reflection, DI, MVC, Design Patterns etc.For all these you know the benefits but there are downsides as well.E.g. you get flexibility with design patterns but also end up with a really big number of classes.Reflection = less speed etc.All these are indeed issues to a some degree.When you design you choose the trade off.In your case,you have read only the pros.If you start googling for the cons you'll see that they are mentioned.You just didn't read the whole story – Cratylus Feb 06 '12 at 22:23
  • @WW.:Just start searching for full review and read cons and drawbacks.You just hit one of them.Hopefully the benefits are more than your problems.Otherwise read my analogy for Singleton – Cratylus Feb 06 '12 at 22:25
5

One thing that helped me is placing multiple closely related classes in the same file. I know this goes against the general advice (of having 1 class per file) and I generally agree with this, but in my application architecture it works very well. Below I will try to explain in which case this is.

The architecture of my business layer is designed around the concept of business commands. Command classes (simple DTO with only data and no behavior) are defined and for each command there is a 'command handler' that contains the business logic to execute this command. Each command handler implements the generic ICommandHandler<TCommand> interface, where TCommand is the actual business command.

Consumers take a dependency on the ICommandHandler<TCommand> and create new command instances and use the injected handler to execute those commands. This looks like this:

public class Consumer
{
    private ICommandHandler<CustomerMovedCommand> handler;

    public Consumer(ICommandHandler<CustomerMovedCommand> h)
    {
        this.handler = h;
    }

    public void MoveCustomer(int customerId, Address address)
    {
        var command = new CustomerMovedCommand();

        command.CustomerId = customerId;
        command.NewAddress = address;

        this.handler.Handle(command);
    }
}

Now consumers only depend on a specific ICommandHandler<TCommand> and have no notion of the actual implementation (as it should be). However, although the Consumer should know nothing about the implementation, during development I (as a developer) am very much interested in the actual business logic that is executed, simply because development is done in vertical slices; meaning that I'm often working on both the UI and business logic of a simple feature. This means I'm often switching between business logic and UI logic.

So what I did was putting the command (in this example the CustomerMovedCommand and the implementation of ICommandHandler<CustomerMovedCommand>) in the same file, with the command first. Because the command itself is concrete (since its a DTO there is no reason to abstract it) jumping to the class is easy (F12 in Visual Studio). By placing the handler next to the command, jumping to the command means also jumping to the business logic.

Of course this only works when it is okay for the command and handler to be living in the same assembly. When your commands need to be deployed separately (for instance when reusing them in a client/server scenario), this will not work.

Of course this is just 45% of my business layer. Another big peace however (say 45%) are the queries and they are designed similarly, using a query class and a query handler. These two classes are also placed in the same file which -again- allows me to navigate quickly to the business logic.

Because the commands and queries are about 90% of my business layer, I can in most cases move very quickly from presentation layer to business layer and even navigate easily within the business layer.

I must say these are the only two cases that I place multiple classes in the same file, but makes navigation a lot easier.

If you want to learn more about how I designed this, I've written two articles about this:

Steven
  • 166,672
  • 24
  • 332
  • 435
4

In my opinion, loosely coupled code can help you much but I agree with you about the readability of it. The real problem is that name of methods also should convey valuable information.

That is the Intention-Revealing Interface principle as stated by Domain Driven Design ( http://domaindrivendesign.org/node/113 ).

You could rename get method:

// intention revealing name
Date cutoff = myExpiryCutoffDateService.calculateFromPayment();

I suggest you to read thoroughly about DDD principles and your code could turn much more readable and thus manageable.

Dimitri
  • 121
  • 4
2

I have found The Brain to be useful in development as a node mapping tool. If you write some scripts to parse your source into XML The Brain accepts, you could browse your system easily.

The secret sauce is to put guids in your code comments on each element you want to track, then the nodes in The Brain can be clicked to take you to that guid in your IDE.

Mark Robbins
  • 2,427
  • 3
  • 24
  • 33
2

Depending on how many developers are working on projects and whether you want to reuse some parts of it in different projects loose coupling can help you a lot. If your team is big and project needs to span several years, having loose coupling can help as work can be assigned to different groups of developers more easily. I use Spring/Java with lots of DI and Eclipse offers some graphs to display dependencies. Using F3 to open class under cursor helps a lot. As stated in previous posts, knowing shortcuts for your tool will help you.

One other thing to consider is creating custom classes or wrappers as they are more easily tracked than common classes that you already have (like Date).

If you use several modules or layer of application it can be a challenge to understand what a project flow is exactly, so you might need to create/use some custom tool to see how everything is related to each other. I have created this for myself, and it helped me to understand project structure more easily.

Bran
  • 136
  • 1
  • 9
2

Documentation !

Yes, you named the major drawback of loose coupled code. And if you probably already realized that at the end, it will pay off, it's true that it will always be longer to find "where" to do your modifications, and you might have to open few files before finding "the right spot"...

But that's when something really important: the documentation. It's weird that no answer explicitly mentioned that, it's a MAJOR requirement in all big sized development.

API Documentation
An APIDoc with a good search feature. That each file and --almost-- each methods have a clear description.

"Big picture" documentation
I think it's good to have a wiki that explain the big picture. Bob have made a proxy system ? How doest it works ? Does it handle authentication ? What kind of component will use it ? Not a whole tutorial, but just a place when you can read 5 minutes, figure out what components are involved and how they are linked together.

I do agree with all the points of Mark Seemann answer, but when you get in a project for the first time(s), even if you understand well the principles behing decoupling, you'll either need a lot of guessing, or some sort of help to figure out where to implement a specific feature you want to develop.

... Again: APIDoc and a little developper Wiki.

FMaz008
  • 11,161
  • 19
  • 68
  • 100
0

I am astounded that nobody has written about the testability (in terms of unit testing of course) of the loose coupled code and the non-testability (in the same terms) of the tightly coupled design! It is no brainer which design you should choose. Today with all the Mock and Coverage frameworks it is obvious, well, at least for me.

Unless you do not do unit tests of your code or you think you do them but in fact you don't... Testing in isolation can be barely achieved with tight coupling.

You think you have to navigate through all the dependencies from your IDE? Forget about it! It is the same situation as in case of compilation and runtime. Hardly any bug can be found during the compilation, you cannot be sure whether it works unless you test it, which means execute it. Want to know what is behind the interface? Put a breakpoint and run the goddamn application.

Amen.

...updated after the comment...

Not sure if it is going to serve you but in Eclipse there is something called hierarchy view. It shows you all the implementations of an interface within your project (not sure if the workspace as well). You can just navigate to the interface and press F4. Then it will show you all the concrete and abstract classes implementing the interface.

The hierarchy view in Eclipse after pressing F4

Jagger
  • 10,350
  • 9
  • 51
  • 93
  • Testability in our code written this way is basically perfect. But, I'm trying to address the problems. Your answer is to use the debugger which means everything is concrete again. – WW. Feb 06 '12 at 22:01
  • 1
    @WW.: I do not know which IDE you use but in order to understand a big picture, the Eclipse Hierarchy View might be useful. – Jagger Feb 07 '12 at 13:18