Ultimately, this article is about a strategy built of programming principles for simple code integration. It is basically our way to reduce the cost of building and integrating new features or products designed from scratch into an existing codebase.
Design, build, integrate
Source
At Codavel, we have built a powerful network coding engine that allows us to easily explore the potentials of this technology in many contexts. Naturally, it is not uncommon that we start some sort of small exploratory project to test an idea or application of network coding. (Think about next-gen communication protocols, data storage systems, networking systems, network security - pretty much any tech that involves coding and transmission of information.) These projects are typically done by very small teams (one or two devs) with full freedom to execute.
Many times, we don’t know if the project is going to be successful, if it will generate a new product or simply if it will be a new feature in one of our existing solutions. So it seems rather heavy to enforce this new-born child to inherit a large portion of our code infrastructure (not to mention it could skew the actual development of the project). Our game plan, when we start something fresh, is to have minimal dependencies from our codebase.
When a project like this shows promising results, it is time to merge. Since dependencies are minimal, this would be a rather easy process. However, it is often the case that this new piece of software contains code that is more efficient doing some task than what we did before. So, while there is no code overlap, there is often functionality overlap. And of course, you want to replace that legacy code with the bright-and-shining super-efficient new piece of software.
This is the time when we do not want to be crossing fingers. We want the process to be smooth and fast. So a big question for us was “How can we jointly optimize (i.e. speed up) development and integration processes while working on the premise of a very small code overlap but high functionality overlap?”. For us, the answer lies in a set of programming principles for simple code integration.
Programming principles for simple code integration
These principles are a simple set of rules that every developer at Codavel should follow.
1. Plan carefully
Planning is always crucial in every project. We learned a long time ago that you should not rush any of the processes that could compromise quality. This means allocating a good amount of time for pre-production tasks such as discussing problem definition, architectural and technical decisions, testing and early refactoring.
The last thing you want is to be doing is paying-off technical debt that you don’t need to be paying if you keep things tightly controlled.
2. Avoid early stage pitfalls
Try to avoid early stage pitfalls. And what are these? Precisely the ones mentioned above.
- Problem definition: make sure that you understand the problem at hand and that you’re synchronized with the team/project lead on what you’re trying to achieve. Discuss, discuss, discuss… Iteration is key when defining a problem, and a solid problem definition will save you thousands of unnecessary lines of code.
- Architecture: make sure your architecture makes sense from the top. Here the keywords for us are: decouple, decouple and decouple (more on this later). Having the right architecture will save you hours of refactoring (and frustration).
- Testing: do your unit tests as early as possible. The fact is that unit tests can help you a lot of understanding if your architecture is spot on and if your system performance can be improved at some point. The earlier you get this information the better.
These three points are tightly correlated. Your architecture depends pretty much on your problem definition and unit tests will help you find the right architecture. An architectural constraint may also imply a problem re-definition. Always be prepared for change and plan for change.
3. Use the right design principles
The above principles are fairly general in what should be a proper way of designing software. However, there is another principle that is missing and that is most efficient oiling this machine. For us, design principles have been the piece of the puzzle that allows us to easily merge/replace any piece of software from our codebase.
In particular, we are keen on using SOLID. SOLID principles are intended to make software component as flexible and as maintainable as possible. That is precisely what we need, to be able to replace any piece of code easily and minimize the introduction of undesired behavior during this replacement. The principles are the following:
- Single responsibility principle: each class should own a single responsibility. It minimizes the chances that you have unexpected side effects when you perform changes to a class.
Our score: ✅ +1 for readability, ✅ +1 for maintainability. - Open/closed principle: classes should be open for extension but closed for the change. It allows for easy substitution of components when needed without compromising the expected behavior of the software.
Our score: ✅ +1 for flexibility - Liskov substitution principle: a subclass should have the same behavior as the respective superclass. It allows for easy extension of a component when needed without compromising breaking builds.
Our score: ✅ +1 for flexibility, ✅ +1 for maintainability - Interface segregation principle: having many client-specific interfaces is better than having a very general interface. A clear separation of components allows for easy extension of functionalities without compromising the expected behavior of the software.
Our score: ✅ +1 for readability, ✅ +1 for maintainability, ✅ +1 for flexibility - Dependency inversion principle: one should depend on abstractions rather than concretions. It decouples high-level functionality from low-level implementation, meaning that when your low-level components are changed, the behavior of your software is unaffected.
Our score: ✅ +1 for readability, ✅ +1 for maintainability, ✅ +1 for flexibility
What do we get from following the principles?
These principles helped us realize that what matters most for efficient integration processes is design. They ultimately force you to always question (and discuss) your designs. Following them created in us the urge to spend more time thinking about better high-level logic and proper interfaces.
In addition, these principles inherently created a codebase that is easy to extend and modify. They made it far more easy to write test code for our components. And more importantly, they ensured a much smoother build process.
So what is the outcome of following these principles? We got code that is easy to:
- read,
- extend,
- replace,
- test,
- debug,
- fix.
And this way, we don’t need to sweat every time that is "merge time".