A Case for Composition
I’ve read way too many blog posts about inheritance versus composition. All of them revolving around the is-a versus has-a relationship. If that subtlety has left you scratching your head, here’s a real life example.
One of my first problems to solve at LendingHome was how to programmatically create reports for our investors. These reports would be used by investors to track a loan’s payment history, view loan information, and project loan performance across their portfolio. In total, I had three reports to create. All of them needed to be in CSV file format because, you know … finance folks love their excel.
Sounds like I need three different type of report generators, right? Since these are types, inheritance was calling my name. I could have a parent class ReportBuilder containing the bulk of the CSV creation. Then 3 sub-classes determining the format of the report, like the columns and the data to display in the column.
Now, like I said, finance people love their excel spreadsheets. It’s not just the investors who are clamoring for reports. Our internal operations need CSV reports. Our third party sub-servicer communicate via CSV reports. Even our scoring engine spits out a CSV report. Lots of CSV reports, and if I wanted a true hierarchal report system, I would need intimate knowledge of each report to properly understand what functionality is common among all the types, and therefore what the sub-classes should be inheriting. Many CSV reports in the system with a high probability of new reports being added. It would, at best, lead to a shallow, wide hierarchal structure, but a real possibility of a deep, wide hierarchal structure (the worst kind).
Switching gears to composition, consider a generic ReportBuilder class, composed of a CsvWriter, an Adapter and the collection of objects. In our example, a collection of objects is an array of ActiveRecord objects of class Loan.
Why Composition?
- Each component of my composed classes has a single responsibility. The ReportBuilder is building the report for a given set of objects. The CsvWriter is only involved with the logistics of creating a CSV file. The Adapter is only concerned with producing the required data, and could be reused for another report format, think XML or JSON.
- Easier to test because the functionality of each part is encapsulated. With inheritance, you might be tempted to skip testing functionality in a subclass because it is tested somewhere else on the hierarchal chain. Do not be lulled into this false sense of security. Each subclass is technically different and all of its functionality should be tested.
- Flexible for different use cases because the design is focused on what and how it’s creating the report versus how it relates to each other
- The ability to pass additional context to a specific adapter, without changing the others. It would look something like this:
class LoanAdapter
def initialize(investor)
@investor = investor
end
def object_to_hash(loan)
# stuff that requires both loan and investor
end
end
After more than a year of usage at LendingHome, the ReportBuilder has been a success. The CSVWriter and ReportBuilder hasn’t changed much, as expected. The adapters have been changed several times as business needs changed. Updating the adapters has been a breeze with little trepidation. Want another column? No problem. Want to display this other data? I got you.
Our engineering team is also growing! We’re hiring engineers in our San Francisco and Columbus, OH offices. See our careers page to learn more. We look forward to hearing from you!