C# Design Patterns – Use Abstract Factory to Produce an Instance of a Subclass

cdesign-patternsdomain-driven-designfactory

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. "en" for English)
  • 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: "en" [0x01, 0x65, 0x00, 0x6E, 0x00] 0
ISO 639-1 for French in Unicode: "fr" [0x01, 0x66, 0x00, 0x72, 0x00] 0
Granny Smith Apple (colour) [0x02, 0xA8, 0xE4, 0xA0, 0x00] 1
Point: P=(3, 4) [0x03, 0x03, 0x04, 0x00, 0x00] 2

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:

Component Role description
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 LanguageCodeDataInterpreter, 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 something structure for the latter approach
  • Flater: I used your handler approach for the internals of the DataInterpreterFactory to 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

Best Answer

So obviously you wouldn't know what kind of data is there on the medium. This means you can't use generics, or fixed types before you actually figure out the exact type.

There are a couple of solutions I've used in similar situations, for network protocols mostly:

  1. If there are a fixed number of things that can happen, register a listener, that'll receive a callback for the type the "factory" determines. Something like Factory.Parse(bytes, listener), and then have Listener.OnPoint(...), Listener.OnString(...).

  2. If the types of messages / data is not enumerable (for example completely dynamic), then register individual listeners per format. For example Factory.register(interpreter, listener), and then Factory.parse(data), which will call the appropriate listener.

  3. Have the "factory" return an object that has behavior independent of the format. For example in a network protocol which sends me commands, I often have something like Factory.Parse(data).Apply(...). Where Apply() is a method implemented differently by each type. Here the factory still can have individual interpreters registered, each with its own logic.

Also if you would try to get away from naming things with what they do (converter, interpreter, factory), and instead try to think about what things are in the domain (maybe RFID, Protocol, Format, Device, etc.), you'll usually have a much easier time figuring out how to think about it.

Related Topic