I need to create an extraction batch job today at work. The goal is to extract an existing report in my application and send it to another system periodically for analytical purposes. Some fields in the report does not exists in the database, they are computed runtime by backend server. Additionally, not all records in database are valid, selection rules must be applied to filter the records when generating the report.
I can use the existing report class and make use the method to generate report. But the problem? The method returns Output Stream. I can’t use the method as is. I need to extend the extraction report with some additional fields and I need to compute it based on some fields.
This is a common problem in programming. You want to make use of existing methods, but you don’t want to change it and increase required effort on regression test, and worse, break the method. Especially if it’s not protected by automated test.
This is a good scenario to make use of adapter pattern.
Adapter Pattern
This pattern is easy to understand. Think of adapters in real life. If you have a US phone charger, but the wall plug is UK, what do you do? You buy a UK universal adapter plug. The adapter converts from one interface to another. This example is analogous to Object Oriented adapter. In practice, we have a class (client) that expects some kind of object, and an adapter converts the interface of the service class to the object that the client can accept.
Benefit of adapter pattern:
- The code is reusable and flexible. The same code can now be extended to serve multiple client.
- Clean and readable code. The conversion logic between 2 different interfaces are encapsulated in a class. Resulting in a more readable client class.
Implementation Structure
There are 2 ways to implement adapter pattern:
- Object adapter
- Class adapter
Object adapter implementation uses the object composition principle: the adapter implements the interface of one object and wraps the other one. It can be implemented in all popular programming languages.
Class adapter implementation uses inheritance: the adapter inherits interfaces from both objects at the same time. Note that this approach can only be implemented in programming languages that support multiple inheritance, such as C++.
I will discuss about Object adapter implementation because I use Java and it doesn’t support multiple inheritance.
Object Adapter Implementation
Class diagram above is how I implement my solution.
CSVReportService class is the existing service that generates the report file.
ExtractionClient is the new service I am writing that extracts the report from my app and sends it to another source. It requires some changes to existing report to meet security and business requirements.
I create 3 new class: ReportServiceInterface, ReportServiceImpl, and Record. Record
is just the POJO which is the result from CSVReportService
that ReportServiceInterface
will return back to Extraction Client. ReportServiceImpl
is where I store an instance of CSVReportService
and perform data transformation.
Whenever ExtractionClient
calls ReportServiceInterface.generate(..)
method, ReportServiceImpl
will pass the invocation to CSVReportService.generate(..)
. It will then store the result in a StringBuffer
. Then it will read the StringBuffer line by line and convert it to Record
object. The Record
object makes it possible for ExtractionClient
to do further enrichment on the data before sending it out. With this simple approach, I have extended CSVReportService functionality further.
I won’t go in-depth into how I convert from String to Object. If you’re interested, I can let you know that I use Reflection to avoid hard coding and make it make the conversion configurable. I have to convert over 10 schemas to a single object. Comment below if you’re interested!
Benefit of the following approach:
- Code is flexible. I can swap the ReportServiceImpl to another if I need to change the implementation in the future.
- Code is not complex and easy to debug. We have achieved the Single Responsibility principle with this solution. ExtractionClient class will be simpler to debug with less code, and if there’s a problem with the conversion, it will be obvious the problem lies within ReportServiceImpl class.
- Achieved other SOLID principles:
- Open-closed principle. We did not modify existing class: CSVReportService, which would have cost us more time for regression test and risk breaking existing functionalities.
- Interface segregation principle. We used ReportServiceInterface and hide the actual implementation class away from ExtractionClient. It will be easy for us to make changes to the conversion method without affecting ExtractionClient.
Disadvantages:
- CPU overhead on string manipulation and conversion of string to object may fail.
Conclusion
You may ask me “Why not just create a new method in CSVReportService?”. It’s because the existing class has many complex business logic to compute certain attributes on runtime. I skipped the implementation complexities and water them down to highlight the importance of incorporating design patterns in code solutions.
With this simple approach, I wrote a flexible, maintainable, easy to debug code and cut down on regression test. All of this eventually translates to less time spent on fixing bugs, and do more enhancements to help our users with their tasks and value add to their work.
Read more
We have discussed a specific implementation here, to learn more design pattern, and SOLID principles. I recommend the following links:
- Design Pattern https://refactoring.guru/design-patterns/what-is-pattern
- SOLID Principles https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design