-3

First, let me explain what I mean by inversion of control and tell, don't ask in this context. I have 4 classes MailController, UserData, Subscription and MailService. MailController is a consumer of object MailService.

Inversion of control here means two things:

  • passing in required arguments into each method on the object MailService
  • assumes that MailService is made up of atomic methods to be controlled by either MailController the consumer or public methods on MailService
   class UserData {
        public User authenticated(int id) {
            // pull from session/db
        }
    }

   class Subscription {
        public string getCustomMessage(User user) {
            // fetch for user
        }
   }

Tell, don't ask here the following scenario:

  • we have task MailService::send() and can either fetch arguments User and subscription messages on MailController, then inject them into MailService::send(). Or, the alternative that is instantiate UserData inside MailService. Then on MailService::send(), we pull and modify the encapsulated UserData instance and use it however we deem fit, be it by calling other local methods or what not ( utilize other classes), as opposed to returning a value after send's atomic operation for it
   class MailService {
        public void send(User user, string message) {
            // do low level stuff
        }
   }

   class MailController {
        public MailController(MailService mailService, UserData userGetter, Subscription subscriptionFetcher) {
            // assign to local properties
        }

        public Response sendToUser(int userId) { // atomic. mailService delegate control of sourcing its dependencies to its caller and only focuses on what it was created to do

            User user = userGetter.authenticated(userId);

            string message = subscriptionFetcher.getCustomMessage(user);

            mailService.send(user, message);

            return Response.ok();
        }
   }

Are these terms/paradigms mutually exclusive? I got here by comparing both patterns. There is pattern 1 with atomic methods, which I appreciate for their testability. I don't have to bother about complex dependency chains.

However, in pattern 2, Tell Don't Ask - TDA (given this understanding of it) is obviously cleaner and the consumer only ever worries about MailService. This reveals pattern 1 equally creates a kind of dependency problem as regards testing. At what point is acceptable for an action in a class to be responsible for deciding what parameters it runs with.

   class MailController {
        public MailController(MailService mailService, UserData userGetter, Subscription subscriptionFetcher) {
            // assign to local properties
        }

        public Response sendToUser(int userId) {

            mailService.send(userId); // tells, doesn't ask
        }
   }

   class MailService {
        public MailService(UserData userGetter, Subscription subscriptionFetcher) {
            // assign to local properties
        }
        public void send(int userId) {

            User user = userGetter.authenticated(userId);

            string message = subscriptionFetcher.getCustomMessage(user);

            // do low level stuff
        }
   }

One of my primary motives in all this is creation of an enabling environment for unit tests and a clear API (non-cluttered) for consumers.

7
  • 1
    I dont know what you mean by target objects or tasks. I think your question is far easier understood if you provde some pseudo-code. – marstato Apr 8 at 12:54
  • 3
    I don't think your definition of IoC is right. IoC is about allowing some generic code to be responsible for flow-control and/or orchestration so that modules you write merely receive messages from that generic code. For example, GUI Widgets often use event handler methods to receive Keyboard/Mouse messages from a GUI framework so that a Widget doesn't need to be responsible for hardware input logic. This is an example of IoC as well as 'Tell, don't ask' because the GUI framework doesn't contain any code for keyboard/mouse inputs, it tells a Widget what to do using a message. – Ben Cottrell Apr 8 at 12:54
  • 1
    In a similar vein, task schedulers are also generic bits of code which are responsible for deciding when and how to trigger a task to run, but the scheduler does not contain any task logic, the tasks are separate modules which simply receive a message from the scheduler which tell them to run. – Ben Cottrell Apr 8 at 12:57
  • @BenCottrell Yes, I know IoC tends more towards containers. I also know TDA's definition isn't exactly as I described here. Which is why the first thing I did was clarify my what I mean within this context. In literal terms, inverting control to the consumer's caller – I Want Answers Apr 8 at 12:59
  • @marstato Done, sir – I Want Answers Apr 8 at 13:44
2

Your question is perhaps a bit unfocused in the sense that I can't quite identify what I could write about that would be most helpful to you, but I'll try. I know you came in with these specific two notions of yours, but as your motivation is "creation of an enabling environment for unit tests and a clear API (non-cluttered) for consumers", you should look more generally into how you might achieve that, without being constrained to the two patterns you started with.

To that end, it actually may be helpful to understand properly what IoC and Tell-Don't-Ask are. Falter's answer provides some good insight, but I have to disagree about the two concepts being unrelated. They are not the same, but are somewhat related.

Inversion of Control was originally a term denoting a style where some framework (e.g. a GUI framework) controls the overall "behind the scenes" program flow, allowing a specific application to define components that can be plugged into the framework (e.g., you derive MyWindow from a framework-provided Window base class).

That's somewhat too specific to be a principle; generalizing from that, it describes an idea of a component that implements some sort of logic that affords extension points, where other components can be plugged in. For this to work, you need dependency inversion: extension points are implemented as well-defined abstractions that both the 'controlling' component and the 'plugin' component can depend on. The dependency is inverted in the sense that the abstraction is owned/defined by the 'controlling' component (e.g. it comes with the framework), and the 'plugin' has to conform to it. This is typically done using a technique called dependency injection - i.e., you polymorphically pass dependencies into a constructor, or (?perhaps less often) to a method. This last point is to a certain extent related to principles (or rather, heuristics) such as Tell-Don't-Ask and Law of Demeter (but it is not the only motivation for them).

Tell-Don't-Ask is supposed to remind us to create cohesive & decoupled objects that we can delegate parts of the work to (as opposed to pulling data from them and doing all the work in one place). You don't have to do it that way everywhere, though. Another important thing about it is that it's not of much use if it only applies at one level. For readability, I'll write C#-like pseudocode; assume types are polymorphic when needed. I'll also mostly ignore things like asynchrony. Look at this example:

// Suppose this obtains some remote data and shows it on a GUI panel
// (I'm using 'controller' as a general/informal name, 
// I'm not implying a particular pattern here)

controller.FetchAndShowData(userId);     // is this Tell-Don't-Ask applied? Not quite

Well, this is pretty "fire and forget", however, that misses the point. For one, inside the method there's very likely a lot of asking and very little telling. You've said:

"TDA (given this understanding of it) is obviously cleaner and the consumer only ever worries about MailService"

But let's unpack that. Sure, the learning curve is not steep, and it's easy to use - as long as the consumer only wants to do what you originally envisioned. If they wanted to do something that's outside of that - well, they are out of luck. E.g., they can't reuse this code in a console application, where data should be shown in a terminal. BTW, that unorthodox consumer may be you some time in the future. This code is inflexible, and hard to test.

One way to add some flexibility is to make the code more granular, and pass data around.

// in Controller
MyData data = dataSource.FetchData(userId);   // asking for stuff is not always bad
ShowData(data);

Assuming that MyData class is not dependent on the GUI, FetchData can be reused in other contexts. This is also more testable, and provides separation of two distinct concerns.

Another way to provide flexibility is to do something like this (note: I'm not saying that this is the better or the go-to approach; more on that later):

controller.FetchDataAndSendTo(userId, myDataView);   // inject the view

// NOTE: This requires the view to implement an interface that can 
//       accept a MyData instance; alternatively, you could use a lambda, 
//       but it's essentially the same thing.

// BTW: I'm using method injection here because it's more obvious in a one-liner 
//      than constructor injection. Method injection lets you change the view 
//      on every call; if that is not necessary, constructor injection will do.

What the caller is saying here is: "Fetch some data for this userId and then send it over to this other guy (myDataView) to handle". In other words, the calling code is only controlling the high-level flow of things, and it's delegating the actual work to two other objects (the controller, and the view). It's telling other objects what to do; it's not asking for data five levels deep. It's not micromanaging them. It trusts the other objects to do the right things. That's what Tell-Don't-Ask means.

Note that I'm not saying that it has to have this form (e.g., there are certainly cases where you don't have to pass in an additional dependency), just that Tell-Don't-Ask is about this overall philosophy.

This is also flexible, because it lets you inject a view of a different kind (e.g., one that prints your data on a console/terminal), or a test double.

When this high-level flow is complex, or otherwise of interest (e.g. you want to reuse it in a different setting - a different part of the app, or in a test), this approach lets you encapsulate that behavior in an object, and explicitly name it (and thus introduce it as a named concept in your code).

class DataLoadingController {
  private IDataSource dataSource;
  public DataLoadingController(IDataSource aDataSource, /* ... */) {
    // save as member vars
  }
  
  // This encapsulates the high-level flow of data fetching and presentation
  public FetchDataAndSendTo(int userId, IMyDataView view) {
    // do some stuff:
    view.Prepare();
    view.IndicateLoading();
    // or whatever...

    MyData data = await dataSource.FetchData(userId);
    view.Accept(data);

    // do some more stuff 
    view.IndicateReady();
  }
}

interface MyDataView {
  void Prepare();  // A GUI could do nothing, a console could clear the screen
  void Accept(MyData data);
  void IndicateLoading();  // A GUI could show the loading indicator widget,
                           // a console could do the — \ | / — "animation"
  void IndicateReady();
}

// Now, since you have this new concept represented in code, 
// you can have other higher-level controllers make use of 
// this DataLoadingController to provide consistent 
// data loading behavior across the app: 
// it's now a small, focused reusable component.
// (My naming could probably be better, though.)

Now, sometimes you'd use one approach, sometimes the other - you need to weigh pros and cons against each other, e.g actual benefits for the needs of your particular project vs increased complexity and/or cognitive load. You'd also apply this at multiple levels (objects could internally delegate to their own dependencies), but be careful to find a good granularity, so that you end up with a reasonable amount of classes (by some criterion of your own).

If you do this very coarsely, you'll get inflexible code, if you go overboard you'll get a system with too many moving parts (lean on the side of smaller classes, but note that NOT everything needs to be dependency-injected everywhere).

One of my primary motives in all this is creation of an enabling environment for unit tests and a clear API (non-cluttered) for consumers.

So here you face what appears to be a dilemma. A more straightforward API is initially easier and clearer for consumers, but it may turn out to be too inflexible. On the other hand, if you go all in with Separation of Concerns, Tell-Don't-Ask, Dependency Injection, you may end up with a flexible system that scares the consumers away because it has a steep learning curve and a million moving parts.

What you can do is: (1) decide on the level of flexibility you want to support (and thus constrain proliferation of classes/components), and (2) provide a number of wrappers that let consumers do common tasks in a straightforward and easy way, but don't prevent them to dig deeper and pull out the parts and recombine them for their own purposes when they need to.

1
  • Thanks for taking the time to see things from my side. Although a large part of the answer seemed like repetition of points already raised in my question, I find (2) of your last paragraph as a new perspective I didn't think of – I Want Answers Apr 9 at 5:27
2

"He can call me a flower if he wants to"

Reference for the quote, if you're interested.

Inversion of control here means two things:

  • passing in required arguments into each method on the target object ClassB
  • assumes that target objects are made up of atomic methods to be controlled by either ClassA the consumer or public methods on ClassB

That is not what inversion of control is. I'm aware that you said:

Yes, I know IoC tends more towards containers. I also know TDA's definition isn't exactly as I described here. Which is why the first thing I did was clarify my what I mean within this context.

But that doesn't justify calling it "inversion of control" when that name is already being used for a very specific concept that is ubiquitously understood. Reusing the same name for a completely different meaning is simply not helpful or productive, especially when engaging in a discussion with others, as you are doing here by posting this question.

Especially when discussing abstract or high-level concepts, it's very important to first make sure that everyone is using the same words, or else your communication is at best going to be hampered, and at worst going to cause others to understand something completely different from what you intended to say.


Data & Dependencies

"Tell, don't ask" focuses on how input data and logic should be structured, i.e. by not separating them when they rely on each other.

Inversion of control focuses on dependencies (and the logic that depends on them) of a given class.

The two concepts (which I may have slightly oversimplified for brevity's sake) are completely unrelated. How you handle dependencies is completely different from how you handle input data and the logic that acts on it. I could easily violate TDA while being IOC-friendly, or I could violate IOC while sticking to TDA. Or I could violate both. Or neither.

While it is correct that passing data and passing dependencies both happens using constructor/method parameters, they are conceptually very different and therefore in most clean coding discussions are never really referred to under the same moniker.

In a class that has both dependencies and input data, it tends to be the case that dependencies are set via constructor parameters, whereas data is input using method parameters. That is not a given, but it is a general tendency.


Your presumed question

It seems to me that the actual question you're asking is something along the lines of:

How do I make an IOC-friendly codebase that doesn't just offload the complex dependency juggling onto my end user?

I infer this because you mentioned:

I don't have to bother about complex dependency chains.

It is an unfortunate reality that despite of its many benefits, IOC tends to bring complex dependency chains to the table. However, this is often alleviated by using DI containers which handle the nitty-gritty injection logic for you.

The answer to your presumed question is middleware. It's admittedly a bit of a vague term, but at its very core it refers to something that sits inbetween the end user and your logic, and that's precisely what you'd need. Something that allow the logic to be IOC-heavy but shields the end-user from the dependency juggling that IOC entails.

A DI container is the precise kind of middleware that handles the dependency juggling for you, while allowing you to go full IOC in your codebase and reap the benefits there (such as promoting both unit testing and loose coupling).

I'm no C++ dev, so I'm probably not the guy to ask for DI container recommendations. But googling "DI container for C++" seems to yield plenty of results for both third party and make-your-own DI containers.

5
  • Please tell me the correct term for the concept I'm referring to, so the question can be edited accordingly. I'm simply using it because it seems most appropriate for the concept in my head – I Want Answers Apr 8 at 13:48
  • @IWantAnswers: It doesn't particularly sound like any specific principle. It's just a misinterpretation of what IOC does focus on, as explain in the second section of this answer. – Flater Apr 8 at 14:01
  • tell don't ask is more about not exposing state rather than just about input data. Otherwise this is a good answer. – Ryathal Apr 8 at 17:24
  • @Ryathal: If your logic needs to read X (exposed elsewhere), then X is really just part of the input required for your logic. At least, that was my reasoning at the time of writing the answer :) – Flater Apr 9 at 0:11
  • @Flater yes you have to provide data to things that are doing the work, but the important part of tell dont ask it you just do Reader.Read(X) not if(Reader.CanRead(x)) Reader.Read(X); else Error; – Ryathal Apr 9 at 12:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.