Cross concerns such as logging, instrumentation and authorization are like bad contractors. One needs them, but they tend to leave a mess. A cross concern violates the single responsibility principle which states that a class or a module should have one and only one reason to change. Violating this principle results in code that is difficult to understand with intertwined rules. The core business of the application gets buried under tons of unrelated code, which extends to tests as well. A test case will either test multiple unrelated concerns or be duplicated for each concern.
In this post, I am exploring how proxy pattern can help ease this friction. The example given here is very trivial and meant to demonstrate the approach while not getting distracted by a complex business problem. In the end, we are going to go over the benefits of using such approach.
We have a simple code, a
RemoteClient that satisfies
Client interface. It has one method
RetrieveUser, which in turn calls some
IO library. This code is easy to read and understand.
We want to add some cross concern to this code. We will take logging as an example.
The business logic is buried under tons of log statements. The only reason we have the error check here, is to write the appropriate log. This problem would have been even worse if we had other concerns. Note that
RemoteClient now depends on logging. In this case it was by being a member of
RemoteClient, but it could have been via a static import, it is still a dependency.
One solution for this problem is using the proxy pattern. We create a new type
ClientLogger that satisfies the same interface
RemoteClient does. Then each method of the new type
ClientLogger delegates to the corresponding method in
RemoteClient. That is
RemoteClient.RetrieveUser with the same exact arguments, and return the same results. However, we wrap the call to
RemoteClient.RetrieveUser with our cross concern.
Since the code calling our
RemoteClient depends on
Client interface ,hopefully, we only need to make a minor change to the client instantiation from
If our application retrieves its implementation from a factory this change should be transparent to the business logic. Only the factory method will be updated.
If we have even more concerns, our code will not change. But our client instantiation would get just a little bit messy.
What did we gain?
- Each of
RemoteClienthas a single responsibility, and a single reason to change. If the IO library changes, the logging shouldn't change. If logging format changes, our
RemoteClientshouldn't care. If authorization rules change, neither logging nor
- We can have three different types of Clients, and we wouldn’t need to duplicate the cross concern code, also known as DRY (Don’t Repeat Yourself)
- Significantly decrease the cognitive load. One can easily understand and navigate through the code without being hindered by cross concerns or jumping from one concern to another.
- Each concern can be tested and verified independently.
Why write when you can generate!
GoWrap is a command line tool that would create wrappers based on your interfaces. So, you can use it to generate your cross concern handlers. It already has templates for most common concerns, but you can define your own. Gratitude goes to titpetric for pointing it out.
I think I would still write my wrappers instead of generating them because I don’t think generated wrappers are flexible enough, they might, however, be a good starting point.
Originally published at mfarag.com.