Context for this question
I'm currently working with small data storage media (e.g. RFID tags) that contain some fixed number of bytes that can be manipulated. The drivers that allow reading bytes from or writing bytes to the storage medium already exist, so now I'm trying to create a wrapper around the driver that will contain methods that better reflect the business logic.
However, the bytes on the storage medium do not always represent the same type of data, and different data types require different business logic. I therefore need a service that returns the right wrapper (interpreter) for the detected data type on the medium.
With something like a factory, I would have to return an abstract interpreter that still needs to be cast to the suitable subclass to expose the business logic, and this is not something the consumer of the library should have to do.
Alternatively, I could use a builder, but I have no idea how to split a generic builder into a more specialised builder that yields an instance of the specific interpreter subclass that's designed to work with the data detected on the medium.
Is there a nice way of doing this, i.e. without getting into reflection or overcomplicating the code?
Example of data that can be stored on the medium
For the sake of simplicity, let's assume some storage medium contains exactly 5 bytes. I have a few data types that could be stored using those 5 bytes:
- a string of two characters, for example an ISO 639-1 language code (e.g.
- an RGB colour, one byte per colour value for red, green and blue
- a 2D coordinate with one byte for the x-value and another for the y-value
To ensure there's a way to distinguish the various data types, I assign one byte to hold a flag value that corresponds with exactly one data type (e.g. 1 for ISO, 2 for colour and 3 for coordinate).
|Example data||Corresponding bytes||Trailing empty bytes|
|ISO 639-1 for English in Unicode:
|ISO 639-1 for French in Unicode:
|Granny Smith Apple (colour)||
|Point: P=(3, 4)||
Lower-level and higher-level handling of the stored data
Each of the data listed above could be represented as classes, and because the byte capacities of each of the properties is known, it should also be possible to convert instances of those classes to arrays of bytes and vice versa. Here's the way I see each component's role in this story:
|Driver||Reads bytes from or and writes bytes to the storage medium.|
|Converter||Converts byte array to an instance of a class or vice versa.|
|Interpreter||Uses a converter to allow byte conversions and wraps a driver with business logic.|
|Interpreter Factory?||Uses the driver to read the flag byte, then produces an interpreter that is compatible with that driver for working with the detected data type.|
The problem lies with the Interpreter Factory. On the one hand—regardless if it's an abstract factory or a factory method—I will end up calling a method that returns a base
DataInterpreter and I will have to cast the output to a
ColourDataInterpreter or a
CoordinateDataInterpreter somewhere else in the code. On the other hand, I could work with generics and create type-specific
InterpreterFactory<TDataType> factories, but there would once again have to be an external service that can tell when to return what type of interpreter factory.
What would be an appropriate way to deal with this fork in the possible data types? (How) Can I ensure that the consumer of this factory gets the right type of interpreter without having to cast it to the right subclass before using it?
Update: what I ended up doing
The idea was to hide the type of data that was read from the connected medium, but to still allow the software to decide what to do with it internally.
This is the part where the binary data is read and interpreted into a class:
// COM port in which the medium is plugged in SerialPort port = new(); // Driver for the hardware that reads the data from the medium TransceiverDriver reader = new(port); // Factory to get the right interpreter DataInterpreterFactory interpreterFactory = new();
What I neglected to mention in my original question is that there is a service with a common behaviour that works regardless of what data was read exactly, so I created an abstract factory for that specific service. The final calls look a bit like this:
// COM port in which the medium is plugged in SerialPort port = new(); // Driver for the hardware that reads the data from the medium TransceiverDriver reader = new(port); // Factory to get the right interpreter DataInterpreterFactory interpreterFactory = new(); // Gets the right interpreter and converts the data IDataInterpreter interpreter = interpreterFactory.CreateInterpreter(reader); IStoredData data = interpreter?.GetStoredData(); // Factory to create services that use the interpreted data SomeDataServiceFactory serviceFactory = new(); IDataService dataService = serviceFactory.CreateDataService( data, // Additional parameters for the service ); // The service has methods that work for all interpreted data types IServiceFunctionResult someResult = dataService.DoSomething(); // etc.
This works, but I still feel uneasy having to get the low-level serial port and driver involved for creating a class that shouldn't care what port and what driver are being used. I could hide the interpreter business altogether and make the data service factory entirely dependent on just the port and the driver, perhaps that would be the better option here.
Either way, thank you for the answers, I tried to use a bit of all answers, despite having worded my question poorly (in terms of libraries and consumers when it's a strictly internal API):
- Robert Bräutigam: you were absolutely right, there certainly was a common functionality and I will probably use your
parse -> do somethingstructure for the latter approach
- Flater: I used your handler approach for the internals of the
DataInterpreterFactoryto determine which type of data was stored on the medium
- Buschwichtel: I must apologise for poorly wording my question where I made it seem I was talking about a public API, nonetheless your answer helped me see the various layers of abstraction that would be involved for this particular task