Composition vs Aggregation: is this a “hidden my dependencies” case?

I have been thinking about this for a while and could not get to a conclusion.
I need to convert a object to another (that will end up serialized as XML).
Both objects have many fields, let’s have an example:

SourceObject
 - String field1
 - Integer field2
 - SourceObjectChild1 field3
   - String child1Field1
   - String child1Field2
 - SourceObjectChild2 field4
   - and it goes

I need to convert it to

TargetObject
  - TargetObjectChild1
    - some fields
  - TargetObjectChild2
    - some fields
  - TargetObjectChild3
    - some fields
  - TargetObjectChild4
    - some fields

Of course the real objects are bigger than this.

Then I present 4 possible solutions. The only hard requeriment is that all classes must be unit tested.
I don’t have any need to swap nor mock implementations of the child converters.

What are the pros and cons about each approach? I’m currently using approach 1, but not feeling too confortable about creating the objects directly inside the convert method.
Are approachs 1 and 2 considered to be have hidden dependencies? Are approachs 1 and 2 (too) harmful?

1)

class TargetObjectChild1Converter {
  public TargetObjectChild1Converter(String sourceField1, SourceObjectChild1 child1) {
     // set private fields here
  }
  public TargetObjectChild1 convert() {
     // do some calculations using private fields
     return new TargetObjectChild1(...);
  }
}

class TargetObjectConverter {
   public TargetObject convert(SourceObject source) {
      var child1 = new TargetObjectChild1Converter(source.getXXX(), source.getYYY()).convert()
      var child2 = new TargetObjectChild2Converter(source.getAAA(), source.getBBB()).convert()
      // some more
      return new TargetObject(child1, child2, ...)
   }
}
class TargetObjectChild1Converter {
  public TargetObjectChild1Converter() {
  }
  public TargetObjectChild1 convert(String sourceField1, SourceObjectChild1 child1) {
     // do some calculations using parameters
     return new TargetObjectChild1(...);
  }
}

class TargetObjectConverter {
   public TargetObject convert(SourceObject source) {
      var child1 = new TargetObjectChild1Converter().convert(source.getXXX(), source.getYYY())
      var child2 = new TargetObjectChild2Converter().convert(source.getAAA(), source.getBBB())
      // some more
      return new TargetObject(child1, child2, ...)
   }
}
class TargetObjectChild1Converter {
  public TargetObjectChild1Converter() {
  }
  public TargetObjectChild1 convert(String sourceField1, SourceObjectChild1 child1) {
     // do some calculations using parameters
     return new TargetObjectChild1(...);
  }
}

class TargetObjectConverter {

  public TargetObjectConverter(TargetObjectChild1Converter converter1, TargetObjectChild2Converter converter2, ....) {
     // set privated fields here
  }

  public TargetObject convert(SourceObject source) {
      var child1 = converter1.convert(source.getXXX(), source.getYYY())
      var child2 = converter2.convert(source.getAAA(), source.getBBB())
      // some more
      return new TargetObject(child1, child2, ...)
  }
}
// I don't like this option as it is possible to child converters to access data they don't need
class TargetObjectChild1Converter {
  public TargetObjectChild1Converter {
  }
  public TargetObjectChild1 convert(SourceObject source) {
     // do some calculations using parameter
     return new TargetObjectChild1(...);
  }
}

class TargetObjectConverter {

  public TargetObjectConverter(TargetObjectChild1Converter converter1, TargetObjectChild2Converter converter2, ....) {
     // set privated fields here
  }

  public TargetObject convert(SourceObject source) {
    var child1 = converter1.convert(source)
    var child2 = converter2.convert(source)
    // some more
    return new TargetObject(child1, child2, ...)
  }
}

1

None of these options is particularly good from an object-oriented design perspective (barring exceptional requirements), because all of them require an outside agent, the “converters” to know all about the objects they convert, including their full internal state (i.e. data) in them. This leads to all sorts of problems, like tight coupling, unmaintainability, etc.

Object-orientation is about having (requirements-related) behavior. The only behavior seems to be to eventually convert it to XML. So why not leave all the converting out, and have a method toXML() or something in the original object and/or its components?

The different between option 1 and 2 is essentially the child-converter:

  • Option 1 uses throw-away child-converters: every child-converter must be instantiated for every single source object, to create every child (in convert()).

  • Option 2 is more homogeneous: the child-converter uses a similar semantic than the object converter. You could easily improve it and reduce the construction overhead by instantiating the child converters only once, in the object-converter constructor. If you have millions of object to convert, it’s a million less temporary converter-objects to manage in memory.

Between the two, I’d go for the option 2 (preferably with the recommended improvement). The main weakness of both approach is their dependency to the target-object constructors: this is a strong coupling. You could not for example not reuse the converter with different target objects offering the same interface.

Option 3 improves option 2 by injecting the child converters into the object-converter, which is another way to address the improvement I mentioned above. Personally, I’d have injected the object using an interface, but I understand that with many child classes, this might create an explosion of code.

Until now, these option keep the mapping logic in the TargetObjectConverter. If the mapping changes (because of either a change in the source or the target), this is the place where you would handle it. Changes to the target might propagate to a few target child-converters as well, but this seems logical in view of their purpose.

Option 4 has the advantage of uniformizing the interface of all the child-converters. You could then easily use a table logic to drive the process and add as many children as needed without big effort. But this simplification comes at the cost of a lack of interface segregation: whenever the source object changes, you need to review all the child-converters for a potential impact. This makes dependency management less transparent. I don’t know if it is relevant here, but sometimes some a source-child may have a live of it’s own, and have a close match with one or two target child. In option 3 you could reuse easily reuse partial converters in other contexts. But in option 4 you would not have this advantage.

Nevertheless, despite the the many arguments against option 4, there are possibly a (very) few cases in which it could be of benefit: for example if source and target objects are primarily a containers of their children and there is a many-source-children to many-target-children of the fields.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *