This content originally appeared on Bits and Pieces - Medium and was authored by Shahar Shalev
In this article, I’ll share three techniques that have helped me integrate new features into existing code while improving its quality
It is a typical chore to integrate new features while maintaining backward compatibility. It is necessary for incremental releases, which are safer and prevent compatibility issues, but it also adds complexity that challenges even experienced developers.
In this post, I aim to give tips that make the process smoother from my experience.
Key points
💡 Another solution may be Bit, which offers versioning and a dependency graph to track and manage updates for each component. It also comes with a centralized repository for components and documentation, making it easier to understand and manage the state of the system. Find out more about Bit’s ‘component-compare’ feature here.
Learn more here:
Advanced Bit Dependency Management and Configs
DRY Principle
DRY stands for Don’t Repeat Yourself, a principle that aims to avoid redundancy and inconsistency in code,
It is not just about avoiding literal repetition of code, but also about avoiding duplication of meaning and purpose.
When the same logic or functionality is expressed in different ways or different places, it becomes harder to maintain and understand the code. DRY is about keeping the knowledge and intent of the code in one place and expressing it clearly and concisely.
An example that you are probably familiar with is a token validation middleware function that can be used in multiple routes.
import { tokenValidation} from './middlewares/tokenValidation';
// creating the route
...
//later in the code
router.post('/route1', tokenValidation, function(req,res) {
//handle route1 logic
});
router.post('/route2', tokenValidation, function(req,res) {
//handle route2 logic
});
You can read more about it in other blog posts like this one:
The DRY Principle: Benefits and Costs with Examples
Interface Sharing
In OOP, classes frequently share an interface through inheritance, composition, or interface implementation. Making sure that properties and methods are shared makes it simpler to develop a backward compatibility solution.
Here is a simple illustration of a user settings class with two variations,
Although the toResponse can return very different objects, we can easily support both variants because they have the same function type.
import { get } from 'lodash';
interface IUserSettings {
toResponse(): object;
}
class UserSettingsV1 implements IUserSettings {
constructor(settings: object) { }
toResponse(): object {
// Custom response for setting v1 object
}
}
class UserSettingsV2 implements IUserSettings {
constructor(settings: object) { }
toResponse(): object {
// Custom response for setting v2 object
}
public static isSettingsV2(settings: object): boolean {
return get(settings, '__VERSION__', 1) === 2;
}
}
class UserSettings {
public settingsInstance: IUserSettings;
// addional Metadata properties
public name: string;
constructor(settings: object) {
this.settingsInstance = UserSettingsV2.isSettingsV2(settings) ?
new UserSettingsV2(settings) :
new UserSettingsV1(settings);
}
userSettingsResponse() {
return this.settingsInstance.toResponse();
}
}
Strategy Pattern
The strategy pattern utilizes the shared interface logic discussed earlier but allows you to switch between the classes you’re working on at runtime. To illustrate this concept, let’s consider the previous example of user settings.
Using the strategy pattern, you can enable users to switch back and forth between two different versions of the user settings.
import { get, isNil } from 'lodash';
interface IUserSettings {
toResponse(): object;
// We added new migrate method
migrate(version: 1 | 2): object;
}
class UserSettingsV1 implements IUserSettings {
constructor(settings: object) { }
migrate(version: 1 | 2): object {
// Custom migration logic
}
toResponse(): object {
// Custom response for setting v1 object
}
}
class UserSettingsV2 implements IUserSettings {
constructor(settings: object) { }
migrate(version: 1 | 2): object {
// Custom migration logic
}
toResponse(): object {
// Custom response for setting v2 object
}
public static isSettingsV2(settings: object): boolean {
return get(settings, '__VERSION__', 1) === 2;
}
}
class UserSettings {
public settingsInstance: IUserSettings;
// addional Metadata properties
public name: string;
constructor(settings: object) {
this.setUserSettings(settings);
}
setUserSettings(settings) {
this.settingsInstance = UserSettingsV2.isSettingsV2(settings) ?
new UserSettingsV2(settings) :
new UserSettingsV1(settings);
}
migrate(version: 1 | 2) {
let result: object = null;
if (this.settingsInstance instanceof UserSettingsV1 && version === 2) {
result = this.settingsInstance.migrate(2);
}
if (this.settingsInstance instanceof UserSettingsV2 && version === 1) {
result = this.settingsInstance.migrate(1);
}
if (!isNil(result)) {
this.setUserSettings(result);
}
}
userSettingsResponse() {
return this.settingsInstance.toResponse();
}
}
It’s safe to say that the example above doesn’t quite follow the DRY principle.
For more information about the strategy pattern, check out this article by refactoring.guru.
Combine it all
- The DRY principle is about avoiding redundancy and inconsistency in code
- The shared interface guarantees that specific properties and method types are shared among classes that implement the interface.
- The strategy pattern utilizes the shared interface logic, allowing you to switch between classes at runtime.
By combining these principles, developers can integrate new features while maintaining backward compatibility, making the process more efficient and effective.
Testing backward compatibility
In the previously mentioned example, in addition to the existing UserSettings class, we added two additional classes, UserSettingsV1 and UserSettingsV2.
We need to revise our integration and unit tests to ensure backward compatibility.
Integration/e2e tests:
Regarding integration tests, the current test cases should continue to work as they do today, with only minimal changes required. We can group all the existing test cases related to version 1 of the user settings within a single “describe” statement.
Additionally, we need to include a new set of tests to test version 2 of the user settings. This way, we can ensure that both versions of the user settings are thoroughly tested and that our backward compatibility requirements are met.
Unit tests:
In the context of unit tests, we need to update some of our existing tests to validate UserSettingsV1 and supplement them with new tests that verify both UserSettingsV2 and UserSettings.
💡 Note: If you were using Bit for publishing, sharing and reusing your UI components, every component you publish would have unit tests associated with it. Using Bit’s Compositions feature, you can automatically create *.spec.* and recognize *.test.* files, and every component you publish and version on Bit will include tests as essentials, giving you confidence in your code.
Learn more here:
Conclusion
Integrating new features while maintaining backward compatibility can be a pain, but techniques like the DRY principle, interface sharing, and the strategy pattern can help. Bringing all of them together helps to reduce code redundancy, making your projects scalable and flexible, ultimately allowing you to have safer, more frequent incremental releases.
Build Apps with reusable components, just like Lego
Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:
→ Micro-Frontends
→ Design System
→ Code-Sharing and reuse
→ Monorepo
Learn more:
- Creating a Developer Website with Bit components
- How We Build Micro Frontends
- How we Build a Component Design System
- How to reuse React components across your projects
- 5 Ways to Build a React Monorepo
- How to Create a Composable React App with Bit
- How to Reuse and Share React Components in 2023: A Step-by-Step Guide
Revamp with Refactoring: Three Techniques for Seamlessly Integrating New Features was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Bits and Pieces - Medium and was authored by Shahar Shalev
Shahar Shalev | Sciencx (2023-04-27T06:02:09+00:00) Revamp with Refactoring: Three Techniques for Seamlessly Integrating New Features. Retrieved from https://www.scien.cx/2023/04/27/revamp-with-refactoring-three-techniques-for-seamlessly-integrating-new-features/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.