Search code examples
oopdesign-patternsdrysolid-principlessingle-responsibility-principle

A way around instantiating sub classes in super class


I have a base abstract class, which aggregates a bunch of items in a collection:

abstract class AMyAbstract
{
    List<string> Items { get; private set; }

    public AMyAbstract(IEnumerable<string> items)
    {
        this.Items = new List<string>(items);
    }
}

There are a lot of subclasses, let's name them Foo, Bar, Baz, etc. They all are immutable. Now I need a merge() method, which will merge items of two objects like this:

abstract class AMyAbstract
{
    // ...
    public AMyAbstract merge(AMyAbstract other)
    {
        // how to implement???
    }
}

Foo foo1 = new Foo(new string[] {"a", "b"});
Bar bar1 = new Bar(new string[] {"c", "d"});
Foo fooAndBar = foo1.merge(bar1);
// items in fooAndBar now contain: {"a", "b", "c", "d"}

Since the objects are immutable, the merge() method should not change the state of items field, but instead it should return a new object of the class uppon which it is called. My question is: how to judiciously implement the merge() method?

Problem 1: AMyAbstract is clearly not aware of specific constructors of the subclasses (dependency inversion principle), thus I cannot (or can I?) create instance of the sub class in a super class.

Problem 2: Implementing merge() method in each of the subclasses is a lot of code repetition (DRY rule).

Problem 3: Extracting the merge() logic to a entirely new class does not solve the DRY rule problem. Even using the visitor pattern it is a lot of copy/paste.

The problems presented above rule out any idea of implementation I might have had before I read about SOLID. (my life has been miserable since then ;)

Or is there an entirely different, out-of-the-box approch to achieve the merge of such objects?

I'd appreciate answer in C#, Java or even PHP.

EDIT: I think I left out a piece of valid information: event though there are a lot of different sub classes, they can (should) only be constructed in two, maybe three ways (as an implication of the single responsibility principle):

  • parameterless constructor
  • a constructor which accepts one IEnumerable<T> argument
  • a constructor which accepts array and some other modifier

This would put the visitor pattern back on the tablie if I could put a constraint on the constructors - for example by defining a constructor in an interface. But this is possible only in PHP. In Java or C# a constructor signature cannot be enforced, thus I cannot be certain of how I would instantiate a subclass. This is a good rule in general, because one could never predict of how author of the subclass would like the object be constructed, but in this particular case it might have been helpful. So a helper question would be: can I somehow enforce how a class is instantiated? Builder pattern sounds like way too much in this simple case, or does it?


Solution

  • A neat solution based on @AK_'s comment:

    tldr: The basic idea is to create a multiple merge methods for each aggregated filed instead of using a merge method for entire object.

    1) we'd want a special list type for the purpose of aggregating the items inside AMyAbstract instances, so let's create one:

    class MyList<T> extends ReadOnlyCollection<T> { ... }
    
    abstract class AMyAbstract
    {
        MyList<string> Items { get; private set; }
    
        //...
    }
    

    The advantage here is that we have a specialized list type for our purpose, which we can alter later.

    2) instead of having a merge method for entire object of AMyAbstract we would want to use a method which merly merges the items of that object:

    abstract class AMyAbstract
    {
        // ...
    
        MyList<T> mergeList(AMyAbstract other)
        {
            return this.Items.Concat(other.Items);
        }
    }
    

    Another advatage we gain: decomposition of the problem of merging entire object. So instead we break it into a small problems (merging just the aggregated list in this case).

    3) and now we can create a merged object using any specialized constructor we might think of:

    Foo fooAndBar = new Foo(foo1.mergeList(bar1));
    

    Instead of returning the new instance of entire object we return only the merged list, which in turn can be used to create object of target class. Here we gain yet another advantage: deferred object instantiation, which is the main purpose of creational patterns.

    SUMMARY:

    So not only this solution solves the problems presended in the question, but provides additional advantages presented above.