2

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
5
  • I've concluded that my desired approach is probably impossible, but I'm leaving my question open for feedback. Feb 11 at 15:23
  • Does the consumer of this factory know what data type they expect? Feb 11 at 18:09
  • You could separate the COM port/reader side by having a RawTagData class which gets passed to all the various interpreters. Think of it like deserialising JSON: you have something which takes a string or a stream and emits classes, but doesn't know where that data comes from.
    – pjc50
    Feb 18 at 12:00
  • @pjc50 that's what happens internally when the DataInterpreter.GetStoredData() method is called Feb 18 at 15:06
  • This question is like the Bible OMG
    – X.Otano
    Feb 18 at 16:40

3 Answers 3

2

The reason this isn't easily possible is related to how the structure of the data storage medium is combining unrelated data requiring unrelated business logic, but must be read via a unified approach.

Since the data you might get is arbitrary and doesn't appear to share a common interface of interacting with it, the caller can't know what specifically it's asking for.

You're trying to uniformly handle different cases, when in reality your interpreter is getting handed a black box. This works if the base type for all of your forms of data could actually be handled over the same interface, with e.g. the consumer of the library calling upon that shared interface of the base type which could then proceed to perform the correct operation using polymorphism, but from what you've described the consumer needs to know what exactly they're calling.

Since the specific application of your software appears to be "There is a black box of data on this medium, give it to me", the caller doesn't know what exactly it's requesting, and for a unified interface to work it has to not matter - for example by the caller immediately handing execution of the actual business logic over to the interpreter itself. Think DataInterpreter.DoYourBusinessStuff() instead of LanguageCodeDataInterpreter.DoYourLanguageCodeBusinessStuff(). This appears to defeat the purpose of your library, however.

Non-base-type-specific actions that are fully outside of the base type interface like that naturally require you to know the specific type, and the only ways to my knowledge of getting around this are asking for it from the library (for example with a dedicated out-parameter defining the type returned), which would just be a cast and defeat the point of your question, or elaborate reflection-setups which also conflict with your question.

Basically: A handling of the data centered around a unified base type only makes sense if all the calling location needs to work with is that base type. Anything more than that requires information on the type that the caller can't possibly have, and must either be handed over to the callee itself or specifically requested by type name (cast).

1
  • Hey, it's been a week since I asked this question and I've included an update on what I ended up doing for this particular task. Your answer helped me in distinguishing the various layers of abstraction, thanks! Feb 18 at 9:40
1

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.

1
  • Hi and thanks for your answer, I've not been very active on this question and I realise now that I've poorly worded the scope of my task. Regardless, your answer was helpful because you ended up predicting what I was actually going to do and need. I've added an update to my question with the final implementation I had in mind. Feb 18 at 9:45
1

The crux of the issue is here:

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

You are staring from a point where you have undefined data, and you expect your factory (or anything else in your code) to tell you what it is, so you can then use logic that is specific to the data type. Boiling it down, you want your code to tell you which type to use.

This doesn't quite work elegantly, and you'd need to violate OCP in order to get it working. You need a better abstraction here.

You cannot separate the "identify the data" and "handle the data" logic because the original caller does not know the specific type, nor should it contain type-specific logic for every possibility (as that is an OCP violation).

Note that you can of course further subdivide that handler into composed components, but your consumer cannot be the one calling the "handle" logic when it is type specific.

This leads to a pattern along the lines of:

foreach(var handler in handlers)
    if(handler.CanHandle(data))
        handler.Handle(data);

handlers can be found e.g. using reflection or explicit configuration. That's your call. You can wrap this in a factory, or e.g. use a DI container for that purpose. Also your call. In some way, you need to aggregate a list of all needed handlers and run the unknown data by all of them so the handlers themselves can figure out if they can do something with it, and what to do with it.

And then your handler resembles something along the lines of:

public class AppleHandler : IHandler
{
    public bool CanHandle(byte[] data)
    {
         // confirm it is apple data
    }
 
    public void Handle([] data)
    {
         // do what needs to be done
    }
}

Feel free to use an abstract base class instead of an interface if there is meaningful base logic that the handlers can share, e.g. related to the byte array conversion.

This ensures that your original caller does not need to know what specific data you were handling, which prevents OCP violations.

1
  • Thanks for your answer, it's been almost a week of complete radio silence on my end, but I updated my question with the final implementation I had in mind. I've used your handler strategy for the internals of a specific class, it's very handy to know! Feb 18 at 9:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.