0

I have a design/architecture question. I am currently trying to elegantly parse data into a single data class. But the data comes in wrapped by various protocols: A(B(C)). The main idea is to be able to switch these at different layers (like A(D(E)) or B(C)). But in the end I just want to return a single object with everything in it.

My current approach is to use inheritance to abstract the layers/protocols with each class. But with this approach, I am not flexible in switching the layers without having to re-code the classes. See the simplified example and example data below.

So my question is: Is there a pattern to realise this deserialisation of layered data properly while keeping the flexibility to change the layers around?

{                                               // Layer A
  "timestamp": "2021-07-06T10:01:00.000Z",
  "value": {                                    // Layer B
      "timestamp": "2021-07-06T10:00:30.000Z",
      "src": "any",
      "data": "some complex data string"        // Layer C
  }
}
class _Base { public JsonNode parse(JsonNode data) { return data; } }
class A extends _Base {
  public Instant aTimestamp;
  public JsonNode parse(JsonNode data) {
    JsonNode remaining = super.parse(data);
    aTimestamp = Instant.parse(remaining.get("timestamp").asText());
    return remaining.get("value");
  }
}
class B extends A {
  public Instant bTimestamp;
  public String src;
  public JsonNode parse(JsonNode data) {
    JsonNode remaining = super.parse(data);
    bTimestamp = Instant.parse(remaining.get("timestamp").asText());
    src = remaining.get("src").asText();
    return remaining.get("data");
  }
}
class C extends B {
  public String[] data;
  public JsonNode parse(JsonNode data) {
    JsonNode remaining = super.parse(data);
    this.data = remaining.asText().split(" ").
    return null;
  }
}
Dave J
  • 475
  • 9
  • 18

1 Answers1

0

If I was doing this in C# I'd try using interfaces - so if you want to swap B for D then as long as the classes implement the interface that might work. Inheritance is fine but not as flexible as interfaces. Inheritance "is a" thing, interfaces "has a" thing. See: HAS-A, IS-A terminology in object oriented language

E.g. (pseudo code):

interface ILevel1{
  Instant bTimestamp;
  String src;
  ILevel2 iLevel2;
}

interface ILevel2{
  String[] data;
}

// your new pseudo code:

class B : ILevel1 {
  public Instant bTimestamp;
  public String src;
  public ILevel2 parse(JsonNode data) {
    JsonNode remaining = super.parse(data);
    bTimestamp = Instant.parse(remaining.get("timestamp").asText());
    src = remaining.get("src").asText();
    return remaining.get("data");
  }
}

class C : ILevel2 {
  public String[] data;
  public JsonNode parse(JsonNode data) {
    JsonNode remaining = super.parse(data);
    this.data = remaining.asText().split(" ").
    return null;
  }
}

class D : ILevel1 {
  public Instant bTimestamp;
  public String src;
  public ILevel2 parse(JsonNode data) {
    JsonNode remaining = super.parse(data);
    bTimestamp = Instant.parse(remaining.get("timestamp").asText());
    src = "SRC is totally overrated";
    return remaining.get("data");
  }
}

Does that make sense? My pseudo code is probably incorrect, but the key is that class B (and D - anything that implements ILevel1) will be returning something that implements ILevel2.

Normally I work in C#, and whilst I've done JSON deserialization I'm no expert.

Update in response to comment

I think I made a mistake in my pseudo code - have corrected.

In your example, what would super.parse(data) be, as the interface does not provide an implementation?

Correct the interface doesn't but the class does... so whilst the interface acts as placeholder, at runtime an actual implementation will get loaded, so it can still do some actual parsing. Make sense?

How would you combine those implementations into a single class/object in the end?

You need some logic which, based on some input, knows which implementation of a given interface to instantiate. Lots of ways to tackle that: a factory which might sit in some common library, or code in the class.

The assumption is that the rules which govern which implementation to load at runtime can be coded into your solution somewhere - e.g. embedded in the implementations themselves or data driven by a common library sitting outside of them.

Here's a pseudo example of each:

// Decide in the factory:
public ILevel1 Level1Factory(string someinput)
{
  if(droid=='K2SO')
    return new B()
  else
    return new C()
}

// Decide in-line in the class - but still abstract out the factory:
class X : ILevel1 {
  public Instant bTimestamp;
  public String src;
  public ILevel2 myLevel2;

  JsonNode parsedData = super.parse(data);
  if(droid=='K2SO')
    myLevel2 = Factory.MakeL2Type001(parsedData);
  else
    myLevel2 = Factory.MakeL2Type002(parsedData);

  ...
}
Adrian K
  • 9,880
  • 3
  • 33
  • 59
  • I get the idea and was also looking at interfaces, but the parsing would not take over. In your example, what would `super.parse(data)` be, as the interface does not provide an implementation? And also: how would you combine those implementations into a single class/object in the end? While my class `C` contains all definitions of `A` and `B` as well, in your example, I would need to re-implement everything again for each combination---or am I missing something? – Dave J Jul 07 '21 at 06:57
  • What would super.parse(data) be? To be honest I hadn't given it a lot of thought, I was just copying what you had. I'll update my answer to cover the other points. – Adrian K Jul 08 '21 at 02:52
  • Ah, that explains the confusion. It is the Java equivalent to C#'s `base`: It directs the call to the parental/inherited function. – Dave J Jul 08 '21 at 07:42