Practical Clean Code 2: Reduce Coupling
Continuing from https://maziz88.medium.com/practical-clean-code-variables-methods-classes-f470eeffb98e, Software engineering in essence, strives to minimize coupling and increase cohesion. This in effect, minimizes impact of changes, and localize the changes made. The organization of the code will be in such a way where it’s logically grouped, reduces the gap in the mental model, which then helps with understanding.
All these are important especially inmaking software more maintainable and flexible by easily adding, removing and changing existing features according to business requirement. This translates to less time on understanding, coding, debugging and less chance for regression on existing features.
If you always dread of changes, amending features to your software, have a look the code base and reflect on the points we are going to discuss next.
Coupling in simpler term means how “connected” from one entity to another. This entity could be classes, components or packages. The more connected it is, the more it will be affected by changes from one entity to another. Highly coupled entities will result in changes in one entity to be propagated to another.
Minimizing coupling here, means ensuring isolation of one component from another and managing impact of changes to an acceptable level in order to prevent it from propagating uncontrollably.
Source Code Level Dependencies
When there is source code level dependencies, the code will have to change when changes are introduced to the primary code (the code it’s depending on). This happen when the dependent source code contains implementation details of the primary code such as names, objects and methods.
Dependent object will have an instance of Primary. It will call Primary’s calculateSomeData() within methodUsingPrimary().
Changing Primary’s calculateSomeData() name, parameter (if we have) will require changing Dependents’s implementation. This becomes worse if for instance the someData and someData2 in the primary code is public and use directly within Dependent. Changing type and name will involve more changes in Dependent.
Dependence on Stable Entity
To minimize changes from propagating is by reducing dependency on unstable entity. Unstable entity are entities which takes a small amount of effort in order to change and has a lot of dependencies with others. It takes less amount of effort to change as it doesn’t affect others and other entities changes will impact the unstable entity. We can also call this characteristic flexible.
Stable entity on the other hand possesses the opposite qualities, hence entities depending on it will be less likely to be affected by change. Both abstract classes and interfaces are what we call stable as both are extendable (able to be dependable by others) and is not affected by changes as there’s no implementation within it.
The next question is, what should be stable and what shouldn’t?
To be stable or flexible?
Not all entities must be stable. Having 100% stable makes your software unchangeable and rigid. But then the question is what should be stable and what should be flexible?
Every software has a purpose, be it to support some business functions in enterprise, features that’s a market differentiator or anything that provides non-technical value to the user that’s using the software. These are what we call business rules (from requirement perspective you can call it functional user story, functional requirement, etc).
Business rules are basically what makes money, regardless it’s computerized or manually done. For example, a bank, one of the business rule is calculation of the dividend for investment, a cinema company would be the process keeping track of seat booking in a theater, a supermarket may have the process of member point calculation based on the item purchased and etc. The data that involves with the business rule are business entities. These business entities are what models the business rules which in software become classes and objects.
As an example, we develop a software for the supermarket to calculate member points. These member points will be depending on the type of items and also custom mapping of items and custom points which will be defined separately. In conjunction, depending on time of the year, the member points multiplier may also be applicable and will be defined separately.
Points class is the business rule which determines how the member points are being calculated. is the class that has the mapping of items and points set. This could be from database or any external system. PointsMultiplierExternalSystem is also custom value set and will be used in multiplying the total points. This also could be from database or any external system. From the Figure B, you can see the dependency direction is pointing towards both PointsMappingExternalSystem and PointsMultiplierExternalSystem , which mean, Points entity is highly dependent on the low level implementation such as the two classes mentioned just now.
Low level here describes how close an object to the input / output (IO), which in this case very close. The closer the object is to the IO, the lower the level it’ll be categorized. In the example case, IO basically the sockets that funnels the data either through HTTP or TCP from another external rest endpoint or database.
Changes on PointsMappingExternalSystem or PointsMultiplierExternalSystem regardless or reason (change of data structure due to changes in database structure, technology used for database, the rest client perhaps for both, any other technical reason) shouldn’t affect the Points class. However, in this case it does, which we need to prevent it. See Dependency Inversion Principle in the next section for how to manage this.
To summarize, business rules are always there and rarely changes. The code or the implementation that implements the business rules on the other hand, changes all the time, for many reason such as (not limited to) due to algorithm changes (better way? mistake? anything), version upgrades, anything. From here we can see we need to keep the business rule stable and the implementations flexible.
Consequently, we have to make these implementations dependent on business rules and not the other way around. We don’t change business rules just because OpenAPI, database client, or http client has a bug in the version you are using (doesn’t make sense :P).
Dependency Inversion Principle
As mentioned, based on Figure B, the dependency direction is pointing towards PointsMappingExternalSystem and PointsMultiplierExternalSystem. Dependency Inversion basically we reverse that direction, in this case againts the low level implementation direction. Something like the following.
Points is dependent on ExternalSystem interface which is a stable dependency. PointsMappingExternalSystem and PointsMultiplierExternalSystem inherits the ExternalSystem with its specific implementation.
This de-couples Points from being dependent on the detail implementations of the external systems. This minimize the impact of the changes made on the external systems from propagating to the Points, which is apart of the business rules.
Out of convenience, we might use the same data models (class) for database, request, response, message publication and consumption. This creates coupling between all the clients of that model. Changing the model of one component forces changes to be made to other components. It becomes worse when the same model is used within a service in micro-service architecture and for inter-communication between services via message broker. Changes made would propagate among the services which is a messy coupling to have.
To de-couple between the clients of the model, we could create models for each clients or at least clients that we want to de-couple. Each client will have their own model and mappers that will map from one model to another. Example, having a dedicated model for request, response, database (domain models) and messages for message brokers. To reduce the mapping overhead we could opt third party mappers such as ModelMapper, http://modelmapper.org/ to do the mapping instead of manually doing it via setter and getter.
Never share the same database between 2 services. This is typical advice given when coming up with an architecture especially when using micro-services. When 2 services share the same database, changes on the structure of the data in the database will cause changes to all the services that’s using the database. Database sharing here means for example sharing the same table (SQL based) or collections (mongo, document based). Having the same physical database with different tables or collections are fine.
As we can see here, reducing coupling is being applied at source code level. This concept is applicable on multiple levels. At component level, we have a centralize registry that registers the implementation. The component would use it via interfaces and the registry will inject these implementations. This allows segregation between core module and addons and de-couples the 2, thus enabling extensibility (new feature via addons) without compromising the main core of the software (limiting regression and controls coupling).
At package level we can put the interface at the “edge” and inter-package integration would only be done via these interfaces. This creates a boundary of dependency between one package and another. Imagine having one package with a collection of business rules, we can isolate it so that it shouldn’t be dependent on another package that’s low level (IO and other details such as framework implementations). This inverts the dependency so that the low level entities will be dependent on the business rules (it extends the interfaces from the business rule package) and the business rules uses the interfaces for it’s processing.
At architecture level, (with the assumption of proper coupling being applied at lower levels), would strive to de-couple components at macro level. These components would take in the form of services (micro-services), micro-kernal, pipeline, filter and layers (if choose so) or monolithic (big ball of mud anyone? no? layered then). Different architectures give different (sometimes unique) characteristics in terms of flexibility and expand-ability of features (changes without restarting?, adding feature without affecting core? and etc.)
All in all these are the gist of what coupling is. Throughout this series this point will be illustrated further. Next we will see more on what’s cohesion.
Some of the books used for reference.
- Clean Code, by Robert C Martin
- Clean Architecture, by Robert C Martin
- Refactoring: Improving the Design of Existing Code (2nd Edition) (Addison-Wesley Signature Series (Fowler))