I’m writing code to import sales information from a variety of sources. The data from each source is first uploaded and normalized, then imported into our main database for reporting and royalty calculations.
Uploading the data involves ingesting a binary file (typically a spreadsheet) and normalizing it depending on the data source. The import into the main system is also source-dependent, as different business rules apply to each.
In the past, my go-to approach was to have a base class, with a subclass for each source.
These days, though, I favor functions over object instance methods, and modules over classes. As I’m integrating this into an existing system, I’m sticking with Ruby.
I experimented with a couple of approaches before settling on one I liked.
I use the following directory and module structure:
lib/
royalties/
dispatcher.rb Royalties::Dispatcher
source_one/
import_handler.rb Royalties::SourceOne::ImportHandler
upload_handler.rb Royalties::SourceOne::UploadHandler
source_two/
import_handler.rb Royalties::SourceTwo::ImportHandler
upload_handler.rb Royalties::SourceTwo::UploadHandler
source_three/
import_handler.rb Royalties::SourceThree::ImportHandler
upload_handler.rb Royalties::SourceThree::UploadHandler
I also have a model object representing an upload (regardless of source). (And, yes, it’s an object, because I’m using ActiveRecord
.) Each upload object has an attribute that identifies its source.
Elsewhere in the code, I want to be able to work with these upload objects regardless of their source, so I write something like:
Royalties::Dispatcher::handle_upload(upload)
The dispatcher handles the polymorphic aspects of this by using the upload type to select the module to invoke:
module Royalties::Dispatcher
extend self
def handle_import(upload)
find_handler(upload.source)::ImportHandler.handle(upload)
end
def handle_upload(upload)
find_handler(upload.source)::UploadHandler.handle(upload)
end
private
SOURCE_TO_HANDLER = {
Upload::SOURCE_ONE => Royalties::SourceOne,
Upload::SOURCE_TWO => Royalties::SourceTwo,
Upload::SOURCE_THREE => Royalties::SourceThree,
}
def find_handler(source)
SOURCE_TO_HANDLER[source] ||
fail("No handler found for upload source #{source}")
end
end
Because the source-specific implementations are just functions, testing them in isolation is easy.