Reputation: 401
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.
Upvotes: 0
Views: 686
Reputation: 17066
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);
}
Upvotes: 0
Reputation: 43718
I think the proper way could be to use generics here.
e.g. 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();
}
}
}
Upvotes: 1