简体   繁体   中英

Best way to serialize composite - (design pattern)

I have following java code that is implementation of Composite Design pattern:

//composite designed for type safety (all Leaf-only operations only in leaf)
interface Component extends Visitable {
  void enable();
  void disable();
}

class CompositeA implements Component {
  private String compositeData;
  private boolean enabled;
  private Set<Component> components = new HashSet<>();

  CompositeA(String compositeData) {
    this.compositeData = compositeData;
  }

  void addChild(Component component){
    this.components.add(component);
  }

  String getCompositeData() {
    return compositeData;
  }

  Set<Component> getComponents() {
    return components;
  }

  @Override
  public void enable() {
    this.enabled = true;
  }

  @Override
  public void disable() {
    this.enabled = false;
  }

  @Override
  public Object accept(ComponentVisitor visitor) {
    return visitor.visit(this);
  }
}

class CompositeB implements Component{
  private int compositeData;
  private boolean enabled;
  private Set<Component> components = new HashSet<>();

  CompositeB(int compositeData) {
    this.compositeData = compositeData;
  }

  void addChild(Component component){
    this.components.add(component);
  }

  int getCompositeData() {
    return compositeData;
  }

  Set<Component> getComponents() {
    return components;
  }

  @Override
  public void enable() {
    this.enabled = true;
  }

  @Override
  public void disable() {
    this.enabled = false;
  }

  @Override
  public Object accept(ComponentVisitor visitor) {
    return visitor.visit(this);
  }
}

class Leaf implements Component {
  private boolean enabled;
  private String[] leafData;

  Leaf(String[] leafData) {
    this.leafData = leafData;
  }

  String[] getLeafData() {
    return leafData;
  }

  @Override
  public void enable() {
    this.enabled = true;
  }

  @Override
  public void disable() {
    this.enabled = false;
  }

  @Override
  public Object accept(ComponentVisitor visitor) {
    return visitor.visit(this);
  }
}

There are 2 possible composite roots here ( CompositeA and CompositeB ) and one leaf component ( Leaf ).

Here I define DTOs that will hold serialized data:

class WholeCompositeASerialized {                     
  String content;                                     
  List<Object> serializedChildren;                    
}                                                     
                                                      
class WholeCompositeBSerialized{                      
  String content;                                     
  List<Object> serializedChildren;                    
}                                                     
                                                      
class WholeLeafSerialized{                            
  String content;                                     
}                                                     

Now if I use visitor pattern for serialization, I get something like this:

interface ComponentVisitor {
  WholeCompositeASerialized visit(CompositeA compositeA);
  WholeCompositeBSerialized visit(CompositeB compositeB);
  WholeLeafSerialized visit(Leaf leaf);
}

class SerializableComponentVisitor implements ComponentVisitor{
  @Override
  public WholeCompositeASerialized visit(CompositeA compositeA) {
    WholeCompositeASerialized wcas = new WholeCompositeASerialized();
    wcas.serializedChildren = compositeA
        .getComponents()
        .stream()
        .map(c -> c.accept(this))
        .collect(Collectors.toList());
    wcas.content = compositeA.getCompositeData();
    return wcas;
  }

  @Override
  public WholeCompositeBSerialized visit(CompositeB compositeB) {
    WholeCompositeBSerialized wcbs = new WholeCompositeBSerialized();
    wcbs.serializedChildren = compositeB
        .getComponents()
        .stream()
        .map(c -> c.accept(this))
        .collect(Collectors.toList());
    wcbs.content = String.valueOf(compositeB.getCompositeData());
    return wcbs;
  }

  @Override
  public WholeLeafSerialized visit(Leaf leaf) {
    WholeLeafSerialized wls = new WholeLeafSerialized();
    wls.content = Arrays.toString(leaf.getLeafData());
    return wls;
  }
}

interface Visitable{
  Object accept(ComponentVisitor visitor);
}

and if I use instanceof this is the code that does the same thing:

class SerializerUsingInstanceOf {
  Object decide(Component component){
    if(component instanceof CompositeA){
      return serialize((CompositeA)component);
    }
    else if(component instanceof CompositeB){
      return serialize((CompositeB)component);
    }
    else{
      return serialize((Leaf)component);
    }
  }

  WholeCompositeASerialized serialize(CompositeA compositeA) {
    WholeCompositeASerialized wcas = new WholeCompositeASerialized();
    wcas.serializedChildren = compositeA
        .getComponents()
        .stream()
        .map(this::decide)
        .collect(Collectors.toList());
    wcas.content = compositeA.getCompositeData();
    return wcas;
  }

  WholeCompositeBSerialized serialize(CompositeB compositeB) {
    WholeCompositeBSerialized wcbs = new WholeCompositeBSerialized();
    wcbs.serializedChildren = compositeB
        .getComponents()
        .stream()
        .map(this::decide)
        .collect(Collectors.toList());
    wcbs.content = String.valueOf(compositeB.getCompositeData());
    return wcbs;
  }

  WholeLeafSerialized serialize(Leaf leaf) {
    WholeLeafSerialized wls = new WholeLeafSerialized();
    wls.content = Arrays.toString(leaf.getLeafData());
    return wls;
  }
}

                                                  

I guess also that visitor is preferred here because when we add new Component , we are required to implement Object accept(ComponentVisitor visitor) method also - so we cannot forget that we need a code for serialization of this new component. If we do the same when we use instanceof we would possibly forget to add it to that check.

Now - my question is - is there any way that we can get rid of that ugly Object return type in Object accept(ComponentVisitor visitor) method signature? The only other option that comes to my mind is to use some marker interface (eg. interface SerializedComponent {} ) and then have all serializer classes implement that empty interface like this class WholeCompositeASerialized implements SerializedComponent but it still does not seem right.

I think the proper way could be to use generics here.

eg https://onlinegdb.com/r1m5Eg4DP

public class Main {

     public static void main(String []args){
        ComponentVisitor<SerializedComponent> serializer = new ComponentSerializer();
        Component componentA = new ComponentA();
        SerializedComponent serializedA = componentA.accept(serializer);
        System.out.println(serializedA);
        
        Component component = new ComponentB();
        SerializedComponent serializedB = component.accept(serializer);
        System.out.println(serializedB);
     }
     
     static interface Component {
        public <V> V accept(ComponentVisitor<V> visitor);
     }
     
     static class ComponentA implements Component {
        public <V> V accept(ComponentVisitor<V> visitor) {
            return visitor.visit(this);
        }
     }
     
     static class ComponentB implements Component {
         public <V> V accept(ComponentVisitor<V> visitor) {
            return visitor.visit(this);
         }
     }
     
     static interface SerializedComponent {}
     
     static class SerializedComponentA implements SerializedComponent {
     }
     
     static class SerializedComponentB implements SerializedComponent {
     }
     
     static interface ComponentVisitor<V> {
        public V visit(ComponentA component);
        public V visit(ComponentB component);
     }
     
     static class ComponentSerializer implements ComponentVisitor<SerializedComponent> {
        public SerializedComponent visit(ComponentA component) {
            return new SerializedComponentA();
        }
        public SerializedComponent visit(ComponentB component) {
            return new SerializedComponentB();
        }
     }
}

You are attempting to return concrete type information from the Visitor. This is not the purpose of the pattern. The Visitor encapsulates (and handles) the concrete type internally.

The solution here is to move all logic specific to ComponentA (or any A-specific type you might convert it to) into the visit(ComponentA) method, and likewise for ComponentB .

If you don't want the type encapsulation of the Visitor, then a different design is more suitable, such as pattern matching.


Comments on the comment...

public static void main(String[] args) {
    // Using a concrete type here defeats the purpose of these patterns.
    // Instead, program to an interface:
    // Component c1 = new CompositeA("root");
    CompositeA c1 = new CompositeA("root");
    c1.addChild(new Leaf(new String[]{"leaf11", "leaf12"}));

    CompositeA c2 = new CompositeA("composite1");
    c2.addChild(new Leaf(new String[]{"leaf21", "leaf22"}));
    c1.addChild(c2);

    SerializableComponentVisitor scv = new SerializableComponentVisitor();
    // Clients never invoke visit methods directly,
    //  because they do not have the type information to make these calls.
    // A client would execute, c1.accept(scv)
    WholeCompositeASerialized wcas1 = scv.visit(c1);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM