Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Tell, Don't Ask" over multiple domain objects

Question

How do I adhere to the "Tell, Don't Ask" principle when performing a function involving multiple objects.

Example - Generating a Report

I have the following objects (illustrative purposes only):

Car, Horse, Rabbit

There is no relationship between these objects, but I do want to generate a Report based on these objects:

createHtmlReport(Car car, Horse horse, Rabbit rabbit){
    Report report = new Report()

    report.setSomeField(car.getSerialNumber())
    report.setAnotherField(horse.getNumberOfLegs())
    // ...etc       
}

The problem with this method is that it has to "Pull" data from each object, which violates the "Tell, Don't Ask" rule. I would rather keep the insides of each object hidden, and have them generate a report for me:

car.createHtmlReport()   
horse.createHtmlReport()
rabbit.createHtmlReport()

... but then I get 3 partial reports. Furthermore, I don't think a Rabbit should have to know how to generate every single report I need (HTML, JMS, XML, JSON ....).

Finally, whilst generating the report I may want to switch on multiple items:

if (car.getWheels() == 4 || horse.getLegs() == 4)
    // do something
like image 846
djcredo Avatar asked Mar 09 '12 15:03

djcredo


4 Answers

The report should maintain the ability to create its self.

In this case, each IReportable object should Implement void UpdateReport(Report aReport).

When Report.CreateReport(List<Reportable> aList) is invoked, it iterates through the List and each object in its own implementation of UpdateReport invokes:

aReport.AddCar(serialNumber)
aReport.AddHorse(horseName)

At the end of CreateReport, the report object should produce its own result.

like image 177
lostinplace Avatar answered Nov 04 '22 20:11

lostinplace


The goal of "Tell don't ask" rule is to help you identify situations where the responsibility that should lie with the given object ends up being implemented outside of it (bad thing).
What responsibilities can we see in your case? What I see is:

1) knowing how to format the report (in xml, ascii, html, etc)
2) knowing what goes on which report

First one obviously does not belong with the domain object (Car, Horse etc.). Where should the 2) go? One could suggest the domain object but if there are multiple different reports in your system you end up burdening your objects with knowledge about differnt report details which would look and smell bad. Not to mention that it would violate the Single Responsibility Principle: being a Rabbit is one thing but knowing which parts of Rabbit information should go on report X vs. report Y is quite another. Thus I would design classes which encapsulate data contents that go on a specific type of report (and possibly perform necessary calculations). I would not worry about them reading the data members of Rabbit, Horse or Car. The responsibility this class implements is 'gathering the data for a specific type of a report' which you've consciously decided should lie outside of the domain object.

like image 23
ppietrus Avatar answered Nov 04 '22 19:11

ppietrus


That's exactly what the Visitor Pattern is for.

like image 3
Terry Wilcox Avatar answered Nov 04 '22 19:11

Terry Wilcox


I don't know exactly this pattern's name (Visitor, Builder, ...):

public interface HorseView {
    void showNumberOfLegs(int number);
}

public interface CarView {
    void showNumberOfWheels(int number);
    void showSerialNumber(String serialNumber);
}

public class Horse {

    void show(HorseView view) {
        view.showNumberOfLegs(this.numberOfLegs);
    }

}

public class Car {

    void show(CarView view) {
        view.showNumberOfWheels(this.numberOfWheels);
        view.showSerialNumber(this.serialNumber);
    }

}

public class HtmlReport implements HorseView, CarView {

    public void showNumberOfLegs(int number) {
        ...
    }

    public void showNumberOfWheels(int number) {
        ...
    }

    public void showSerialNumber(String serialNumber) {
        ...
    }

}

public XmlModel implements HorseView, CarView {
    ...
}

public JsonModel implements HorseView, CarView {
    ...
}

This way you can have multiple representations of the same domain object, not violating "Tell don't ask" principle.

like image 1
user2014029 Avatar answered Nov 04 '22 20:11

user2014029