How to solve that last “ask” in a complex calculation to conform “tell! don't ask!”? [closed]

StackOverflow https://stackoverflow.com/questions/12060993

  •  27-06-2021
  •  | 
  •  

Question

today i was thinking about "tell! don't ask!" and experimenting with this code.

interfaces:

interface IValidationContext
{
  void AddMessage(string text);   
  bool IsValid { set; }
}

interface IValidation
{
  void ValidateInput(Input input, IValidationContext context); 
  void ValidateOutput(Output output, IValidationContext context); 
}

interface ICalculator
{
  Output Calculate(Input input);
}

implementations:

class CalculationService
{
  private readonly ICalulator _calculator;
  private readonly IValidation _validation;

  public CalculationService(ICalculator calculator, IValidation validation)
  {
    _calculator = calculator;
    _validation = validation;
  }

  public Output Calculate(Input input)
  {
     var context = new CalculationContext();
     _validation.ValidateInput(input, context);

     if (context.IsValid)
     {
        var output = _calculator.Calculate(input);
        _validation.ValidateOutput(output, context);
        return output
     }
     return null;
  }
}

class CalculationContext : IValidationContext
{
  public CalculationContext()
  {
    Messages = new List<string>();
  }

  public IList<string> Messages { get; private set; }

  public void AddMessage(string text)
  {
    Messages.Add(text);
  }

  public bool IsValid { set; get; }
}

i know it is not always possible conform a design-principle. but in the end i stuck with this code where i'm asking an object:

 if (context.IsValid)
 {
   var output = _calculator.Calculate(input);
   _validation.ValidateOutput(output, context);
 }

is it possible solve it whether it is practical or not?

edit 1:
if i modify my IValidationContext and rename it:

interface ICalculationContext
{
   void AddMessage(string text);   
   Output Calculate(ICalculator calculator, Input input);
   bool IsValid { set; }
}

the context don't need to be askd:

public Output Calculate(Input input)
{
  _validation.ValidateInput(input, context);        
  var output = context.Calculate(_calculator, input);
  _validation.ValidateOutput(output, context);
  return output;
}

now the context is responsible to call calculation based on its internal state. ...it dont feel right...

edit 2:
i read a small article about "tell! don't ask!" and it states that: asking an object for its internal state and then tell that object something depending on that state, would violate "tell! don't ask!" but it's ok to tell another object something. does this apply here?

btw. introducing an boolean-isvalid-result for ValidateInput and ValidateOutput. could change the code to this, which is nice and nobody gets "asked" something:

public Output Calculate(Input input)
{
  var isValid = _validation.ValidateInput(input, context);

  if (isValid)
  {
    var output = _calculator.Calculate(input);
    _validation.ValidateOutput(output, context);
    return output
  }
  return null;
}
Was it helpful?

Solution

This line is the source of the issue

var context = new CalculationContext();

You should be injecting the CalculationContext into the CalculationService so you can interrogate it outside of the class. The Calculate method tells the validator to validate the input and output. Asking in this context would be code like this:

public Output Calculate(Input input)
{
   var validator = _context.Validator;
   if (validator.IsInputValid(input)) {
       // ... snip ...
   }
}

Here we're asking the validator whether a particular input is valid, rather than telling it to validate something. Also, we are limiting ourselves to working with the IsValid getter on the IValidationContext object. This is a little bit of a fuzzy situation, because accessing _context.Validator could be seen as violating the Law of Demeter, but this property is defined in an interface and only returns an interface so we are not coupled to any concrete implementation of these classes.

Here's a suggestion, assuming the following modifications to the interfaces

interface IValidationContext
{
    void AddMessage(string text); 
    IValidation Validator { get; }
    bool IsValid { get; }
}

interface IValidation
{
    void ValidateInput(Input input); 
    void ValidateOutput(Output output); 
}

interface ICalculator
{
    Output Calculate(Input input);
}


class CalculationService
{
    private readonly ICalulator _calculator;
    private readonly IValidationContext _context;

    public CalculationService(ICalculator calculator, IValidationContext context)
    {
      _calculator = calculator;
      _context = context;
    }

    public Output Calculate(Input input)
    {
       _context.Validator.ValidateInput(input);

       if (_context.IsValid)
       {
          var output = _calculator.Calculate(input);
          _context.Validator.ValidateOutput(output);

          return output;
       }
       return null;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top