Loose coupling is about how much a class knows about the objects it's using. The less a class knows about the components it is using; the better the decoupling. Or they are loosely coupled.
How do µObjects fair with being loosely coupled? That's what I want to show today.
µObjects are loosely coupled by coding to an interface
A good start to classes being loosely coupled is coding to the interface. Only using methods as defined by an interface limits how much the consumer can know about the concrete instance.
There are a lot of 1:1 interfaces in the µObjects world. A situation I used to abhor. Now it's normaly. It's required. The level of unit testing and creating fakes (not Mocks - different post for a later time) requires that we use interfaces for everything. Even if it creates a 1:1 interface:class situation.
A µObject should never hold reference to any object that is not an interface. In the C#/Java world no concrete instances are parameters to anything.
This requires a few other principles of µObjects; like no getter/setters. µObject interfaces focus on behavior, not data. Who cares what it has; focus on what it does.
This is what being loosely coupled is about - Acting on only behaviors.
µObjects apply Single Responsibility for isolating change
To maintain loose coupling µObjects must not be affected when other classes change. µObjects change for one and only one reason. If we have to change a class because another class required a change - then we've failed in being loosely coupled. It will have introduced tighter coupling. A good thing from this is that it will identify an area of improvement. :)
µObjects are Single Responsibility to the Extreme. Doing this creates a lot of very small (ya know - micro) objects that do a single thing. If all an object does is a single thing, then it will only have one reason to change. If we've done a proper interface, then no one else will ever know about this change. If we change what the class is composed of, no one knows because we never return our data. No other classes are affected as long as we do the behaviors our interface(s) define - We're doing everything we should.
Another interesting aspect of being Single Responsibility to the Extreme is that most of the time classes don't modify them. They do A THING. If you modify what they do; you're creating a new class. The behavior of a µObject is very narrowly defined which almost always pushes any change into just being a new class.
This is extremely powerful in that it really limits change affecting other classes. If you don't change, you don't impact other classes. :)
While this may seem counter-intuitive, or bring about, "Why not just put it all into a single class?" - Because you can't refactor. Because it's harder to change. Becuase it doesn't allow extracting duplication. Because it's leads to code rot.
There's a lot of benefit that having a lot of single purpose objects gives us. The code is HUGELY flexible and maintainable and highly resistant to code rot.
µObjects are ignorant to details of other behavior
Part of this is using interfaces, but it's much more. Hopefully obviously given the first point was about using interfaces.
An example of this from work is refreshing authentication. The components required to refresh against the Auth Server are:
- Authentication Info
- Data Storage
- Data Parsing
- Refresh Token
- Response Code Checking
- Error Handling
- Success Handling
There's potentially more; but there's an easy eight things we need to know to refresh our authentication.
When the single class (yes, there's only one class that handles this for all flows) that does token refreshing needs to perform; it invokes a
Refresh method on an
ITokenRefresh (names may not be accurate) interface. The class that handles refreshing authentication doesn't know anything more than when refreshing is required.
Pushing all of these components into classes that have a Single Responsibility allow us to have responsibilities funnel through a single class. It allows building re-usable components. These are reused, not always in new projects; but through other flows. It's not a class that can make a call, check a result, and refresh is needed. It's setting up a foundation framework that always does these things.
When we build a new endpoint; we don't worry about how to refresh our auth token; or what to do when that fails. We simply call our "network start" point with correct information and it's all handled. This is the win of reusability. We can access endpoints with barely being concerned with parsing the response.
Being ignorant of how other components actually accomplish what we ask of it allow building highly reusable foundational pieces of code.
As more code is developed, more refactoring done, the level of "the foundation" rises. More generic solutions show themselves. Our code becomes quicker and easier to develop because we can build upon classes that do what we ask; and we don't have to care how they do it. We are better for not knowing how.
Loose coupling is ignorance. We want the object we're working on at that moment to be as ignorant as possible about how things are done when it calls an interface.
When we have this, we end up building highly reusable and maintainable components that expedite feature development.
µObjects simplify developing loosely coupled code by never being aware of how other tasks are accomplished.