views:

89

answers:

2

I'm look for good alternatives to invoking a specific interface from a generic framework. I'll exemplify with code. Look to the question part, the example code is primarily included for thoroughness, and for putting the example into a real scenario.

Example

Assume we want to build a report based on a list of components. Say we have two specific component types:

public interface Component { ... }
public class PDFComponents extends Component { ... }
public class WordComponents extends Component { ... }

Each component has a ReportBuilder implementation, e.g.,

public interface ReportBuilder { ... }
public class PDFReportBuilder extends ReportBuilder { ... }
public class WordReportBuilder extends ReportBuilder { ... }

Which builds specific report implementations

public interface Report { ... }
public class PDFReport extends ReportBuilder { ... }
public class WordReport extends ReportBuilder { ... }

Finally we have the service which locates components and produces a report out of the components.

public class ReportService {
    ReportComponentRepository repo;
    List<ReportBuilder> builders;

    public <T extends Report> T getReport(Class<T> reportType) {
        // Get report components. E.g., this might return List<PDFComponent>
        List<Component> reportComponents = repo.getReportComponents(id);

        // Build report from components using one of the registered builders
        for (ReportBuilder builder : builders) {
            if (builder.buildFor(reportType) {
                return builder.buildReport(report);
            }
        }
    }
}

Example using the service

List<PDFReport> reports = new ReportService().getReport(PDFReport.class);

Question

Now to the question. How can I design a generic ReportBuilder interface which allows typesafety to it's implementations?

E.g., choosing the interface:

public Report buildReport(List<? extends Component> components);

would cause ugliness in it's implementations:

public class PDFReportBuilder implements ReportBuilder {

    @Override
    public Report buildReport(List<? extends Component> components) {
         PDFReport report;

         for (Component component : components) {
            if (component instanceOf PDFComponent) {
                // assemble report ... 
                report.includeComponent(component);
            }
        }

        return report;
    }
}

when we really desire the interface for PDFReportBuilder to be e.g.,

 public Report buildReport(List<PDFComponent> component) { ... }
+2  A: 

It works if you make the type of Component into a type variable for ReportBuilder:

public interface ReportBuilder<T extends Component> {
    public Report buildReport(List<T> components);
}

public class PDFReportBuilder implements ReportBuilder<PDFComponent> {
    public Report buildReport(List<PDFComponent> components);
}

You'll have to evaluate whether you really want a type variable in ReportBuilder though. It's not always the correct choice. Additionally, if you also want PDFReportBuilder.buildReport to have a return type that is PDFReport, then you need to have that as a type variable as well (i.e., public interface ReportBuilder<T extends Component, S extends Report>).

waxwing
Spot on. Sometimes I forget the fundamentals of generics while applying some of the more advanced techniques. I agree that it's most powerful when both a parameter T and a returntype S is specified.
Johan Sjöberg
Your statement about the return type is not true. An overriding (or implementing) method is allowed to specify a more specific return type since Java 1.5.
ILMTitan
+2  A: 

It seems to me like you're setting yourself up for a messy implementation by having three parallel inheritance hierarchies. May I ask, why can't you merge the Component and ReportBuilder's shared behavior? Effectively, you lose any abstraction on the components by forcing the service caller to know the subclass of the Report they want.

I'd suggest simplifying the interface by minimizing or eliminating the parameters to buildReport()

 public class ReportService {
    ReportComponentRepository repo;
    List<ReportBuilder> builders;

    public <T extends Report> T getReport(Class<T> reportType) {

        // Build report from components using one of the registered builders
        for (ReportBuilder builder : builders) {
            if (builder.buildFor(reportType) {
                //don't pass components - if there's a requirement 
                //for a strongly typed subclass of Component, just 
                //let the Report instance figure it out.
                return builder.buildReport();
            }
        }
    }
}


//example use
public class PDFReportBuilder implements ReportBuilder {

    ComponentSource componentSource;

    @Override
    public Report buildReport() {
         PDFReport report;

         for (PDFComponent component : componentSource.getPDFComponents()) {
            // assemble report ... 
            report.includeComponent(component);
            // no instanceof operations!
        }

        return report;
    }
}
Anthony Bishopric
What an excellent idea, this is definately a great approach to minimize application logic complexity.
Johan Sjöberg