18

I've got a case where "Tell, don't Ask" seems to conflict with the "Single responsibility" principle. I've looked at other discussions on the subject but not yet been able to work out the most appropriate object oriented approach for this situation.

I have a program which reads and manipulates collections of data from various sources. I have created a class to hold and manipulate the data (a "DataSet" class). It includes methods for performing various operations on the datasets, such as comparing two datasets to generate a new one which contains the differences, and writing datasets to a file.

I now want to perform some analysis on a dataset and output the results to a report. My first attempt at coding this interrogates the dataset to extract information from it and then constructs the report but this seems to go against the "Tell, don't ask" principle. So: should I put the analysis methods inside the DataSet class and tell the dataset to analyse itself and generate a report? Does this break the Single Responsibility principle? What if I want to perform other types of analysis in future - by DataSet class could become very bloated with lots of different analysis routines which are nothing to do with its core purpose.

Can anyone suggest the best approach here? Is there a particular design pattern which addresses this issue?

Bob
  • 503
  • 4
  • 11

4 Answers4

17

Whenever you design software, you always have to balance different principles, because many of them conflict. For instance, the DRY (Don't Repeat Yourself) Principle often conflicts with Single Responsibility Principle, particularly when two things do similar, but not exactly the same thing.

Often times you have to decide which principle is more important and emphasis that principle over another one (although you should try to adhere to as many as possible). Often times, principles work together, sometimes they work against each other.

In this case, Tell Don't Ask works with other principles, such as the Law of Demeter (which despite it's name is still a principle as far as software goes and is better described as the principle of least knowledge).

What the LoD tells us is that a method of an object should only call other methods

  • On Itself
  • On objects passed into it as a parameter
  • Any objects created/instantiated with an object passed by parameter
  • the objects direct component objects
  • Or a global variable

It's not specifically said, but I feel that the order of preference of selection of methods to call should be in that order as well, with global variables being last resort. But, that's neither here nor there.

So, if we combine Tell, Don't Ask with LoD then it's perfectly ok to pass objects to another object for "asking". Meaning, You have an Analysis object which you "Tell" to do something, passing the DataSet object as a parmeter. That's adhering to TDA. Inside the Analysis object's method you're adhering to LoD by only accessing "close friend" data.

This also conforms to SRP, since your DataSet is still just a DataSet and your Analysis object is an Analysis object.

The key take away here is that these principles are often "relativistic". Meaning that, from the perspective of the parent object that gets the data and wants to perform the analysis, you're "telling" the analysis object to do something.

The purpose of TDA is that your parent code should not query your DataSet for its state and then make decisions based on that. It should instead pass objects to other objects and have those objects perform their responsibilities, which may including querying those objects for their state, but that's ok because it's in the context of their responsibility.

Further reference here:

http://pragprog.com/articles/tell-dont-ask

EDIT:

If you would like a more authoritative source, there's no one better than Martin Fowler himself (read towards the end, you'll find this commentary)

http://martinfowler.com/bliki/TellDontAsk.html

But personally, I don't use tell-dont-ask. I do look to co-locate data and behavior, which often leads to similar results. One thing I find troubling about tell-dont-ask is that I've seen it encourage people to become GetterEradicators, seeking to get rid of all query methods. But there are times when objects collaborate effectively by providing information. A good example are objects that take input information and transform it to simplify their clients, such as using EmbeddedDocument. I've seen code get into convolutions of only telling where suitably responsible query methods would simplify matters 1. For me, tell-don't-ask is a stepping stone towards co-locating behavior and data, but I don't find it a point worth highlighting

Erik Funkenbusch
  • 90,480
  • 27
  • 178
  • 274
  • 4
    Although your `Analysis` object is getting *told* to perform the analysis, it will need to *ask* `DataSet` for some data. So you propose to violate TDA, if it's "in the context of objects responsibility". Am I getting this right? – Vladimir Keleshev Jun 07 '14 at 12:37
  • 2
    @Halst - No, it's not "violating TDA". It would be impossible to write code that didn't ask other objects for things. TDA is about the appropriate time to do that. It is not appropriate to query an object and then call different methods of that object based on its state, the object itself should decide that. Read the link I gave you at the end. – Erik Funkenbusch Jun 07 '14 at 15:27
  • 1
    @Halst - The part you're getting confused on is that TDA talks about encapsulating data with the actions that operate on it. But remember, objects you pass to another object are, in effect, data as well In many cases, It's just data that's already existing in a different form. So in effect, creating an analysis object, and passing it the data object creates an encapsulation of data/function in the new Analysis object that adheres to TDA. Again, it's all about relativity and perspective. – Erik Funkenbusch Jun 07 '14 at 15:33
  • So in the end, instead of saying that you would violate TDA, you add a bunch of exceptions in the definition... – Phil1970 Dec 15 '16 at 16:16
1

I would create a DataAnalyzer object who's responsibility would be to generate the report based on some analysis of the input data.

interface DataAnalyzer {

    public function analyze($input);

    public function report();
}

Now we can have different kinds of analysis we require

class AnalyzerOne implements DataAnalyzer {
    //one way of analyzing and reporting
}

class AnalyzerTwo implements DataAnalyzer {
   //other way of analyzing and reporting
}

I can make my DataSet object populate the Analyzer with some input for analysis and then delegate the reporting to it.

class DataSet {

    private $data;

    //... other methods

    public function report(DataAnalyzer $analyzer) {
        //prepare input for the analyzer from the current state
        $analyzer->analyze($input);
        return $analyzer->report();
    }

}

Finally the client would look like so

$dataSet = new DataSet();
//...

$firstReport = $dataSet->report(new AnalyzerOne());
$secondReport = $dataSet->report(new AnalyzerTwo());

So each object is responsible for separate tasks, dataSet does it's own business and analyzer is responsible for reports. However we do tell DataSet to use the analyzer to generate reports. DataSet then tells the analyzer what kind of input to use and returns the report.

Of course this is not the only way, but in general with this amount of information I think it's the right idea.

Weltschmerz
  • 2,076
  • 1
  • 14
  • 17
  • 2
    But then `AnalyzerOne` and `AnalyzerTwo` would most likely need to *ask* for some data of `DataSet` to perform their action. So your solution is to violate tell-dont-ask. Do I understand right? – Vladimir Keleshev Jun 07 '14 at 12:34
  • It wouldn't ask DataSet for anything. Analyzer requires some input to do work. DataSet tells the Analyzer to do work with some input. DataSet provides this information from within itself (DataSet class), so there is no encapsulation violation. Asking for information would be something like passing the entire DataSet object to the Analyzer and then from within the Anlyzer going dataSet.getThis, dataSet.getThat ... – Weltschmerz Jun 07 '14 at 15:40
  • If you have many analysis, then each one might need some part on the data... thus `$input` might again be the whole data so in the end there would be no advantage to add a `report` function over being able to get the whole data and directly pass it to `analyse` function. – Phil1970 Dec 15 '16 at 16:12
0

Sounds like perhaps a ViewModel is what you want. You have a Model (the DataSet), which is responsible for maintaining the state of your data and what that data represents. You have a View (the Report), which is responsible for displaying various pieces of data to the user but you want to transform the DataSet into a representation of the data appropriate for Viewing?

You could encapsulate the responsibility of preparing DataSet for viewing - DataSetViewModel for example. It could have functions on such as GetDataInReportFormat().

I think the main change is to shift your thinking to consider preparation of data for viewing as a separate responsibility.

Michael Parker
  • 4,559
  • 6
  • 23
  • 37
  • 2
    I think the question is not about generating reports, but about performing some analysis on `DataSet`. If we assume that this analysis needs several instance variables to perform then there are 2 options: if you make the analysis as part of `DataSet`, then the object would confirm to tell-dont-ask. If the analysis will be external to the object, it would not confirm it, but would confirm to single-responsibility-principle instead. – Vladimir Keleshev Jun 04 '14 at 22:14
0

May be a very simple application of inheritance can solve your problem.

So, you create a child class of Dataset to perform the analysis. In future if you need arise, you can create another Child class to performa that analysis.

The main benefit here, is Child classes inherit Dataset, internals, so they are from same family and can access the data Dataset.

I can give some code example. But let's first see what comments have to say here.

Darshan Joshi
  • 362
  • 1
  • 11
  • Inheritance would generally not works well. You might already have an object of one type when you get them from the database and assuming that you use almost all data, it would not make much sense that the database would have function to return a dozen of similar objects that only vary by actual type... – Phil1970 Dec 15 '16 at 15:14
  • May be may be not! There is a lot of assumption here. If you wish to start a new conversation, I would suggest to ask a new question. – Darshan Joshi Dec 15 '16 at 18:57