javaoopdesign-patternsobject-design

Suggestion on Pattern to solve concrete class specialization


Suppose that you have several data objects that share some common fields and inherit from a base object.

class BaseProduct {
String productId;
String productType; // i.e. {A, B}.
}

class ConcreteProductA extends BaseProduct {
...
String concreteAttA;
}

class ConcreteProductB extends BaseProduct {
...
String concreteAttB;
}

Now suppose I can create packages containing a specific type of product, and that the implementation of the function that creates the package is different depending on the concrete type being packaged. However, Ideally, I'd want to encapsulate that specific behavior behind an abstraction, as I'd always want to take the same steps (i.e. preparePackage) for any type of product. In other words, I want to be calling packageManager.preparePackage from the following excerpt:

interface PackageBuilder<T extends BaseProduct> {
      Package preparePackage(T product);
}

class ProductAPackageBuilder implements PackageBuilder<ConcreteProductA> {

     @Override
    Package preparePackage(ConcreteProductA productA) {
        return new Package(productA.concreteAttA);
    }
}

class PackageManager {
    Map<String, PackageBuilder> packageBuilders;

    Package preparePackage(BaseProduct product) {
      return packageBuilders.get(product.productType).preparePackage(product);
    }
}

This code results in a downcast being made from BaseProduct when calling the prepare package. This also raises a compiler warning. I understand the disadvantages in downcasting and having calls to preparePackage not being statically typed. However, I'm also struggling to find a solution that's strictly better than this approach.

The obvious solution would be to move the preparePackage function inside the ConcreteProductA and expose the method in BaseProduct, so that I can take advantage of polymorphism. However, the thing I dislike about this approach is tha the product classes are mostly data objects, and the preparePackage function can contain lots of business logic. Ideally I'd like the product classes to be as thin as possible and contain just data and not a lot of behavior.

Is there any alternative that allows me to keep the product objects being data-only, and avoid this downcast? Would this be an acceptable case where downcast could be useful?


Solution

  • The obvious solution would be to move the preparePackage function inside the ConcreteProductA and expose the method in BaseProduct, so that I can take advantage of polymorphism

    Yes, that would be the typical solution.

    However, the thing I dislike about this approach is tha the product classes are mostly data objects, and the preparePackage function can contain lots of business logic

    That doesn't follow. Just because you have a method in BaseProduct doesn't imply that this method must do all the work. It can call other methods, you know :-)

    So I'd have a bare-bones polymorphic method in BaseProduct that locates and calls the correct implementation, which contains the "lots of business logic" you don't want in your domain class.

    If you take this technique to its extreme (where the domain class contains nothing but delegation code), you end up with the visitor pattern.

    class BaseProduct {
        abstract <R> R visit(Visitor<R> visitor);
    }
    
    class ProductA {
        <R> R visit(Visitor<R> visitor) {
            return visitor.visit(this);
        }
    }
    
    // similar for ProductB
    
    interface Visitor<R> {
        R visit(ProductA productA);
        R visit(ProductB productB);
    }
    
    class Packager implements Visitor<Package> {
        public Package visit(ProductA productA) {
            // lots of business logic here
        }
    
        public Package visit(ProductB productB) {
            // lots of business logic here
        }
    }
    
    
    // and then you can write
    myProduct.visit(new Packager()); // type safe invocation of logic the domain knows nothing about