Is your code asking too many questions? Learn how the «Tell, Don’t Ask» principle can make your objects do the talking

Is your code asking too many questions? Learn how the «Tell, Don’t Ask» principle can make your objects do the talking

Although programming is one of my two greatest passions, I had no academic training in it, beyond introductory courses while studying Electronics Engineering.

Along the way, while I was self-taught as a Software Engineer, I learned that in Object Oriented Programming each attribute should have a method to change its value and a method to read its value; the famous setters and getters, respectively.

Languages ​​like C# (and Ruby, among others) have even incorporated them within the language itself (the so called C# properties or the Ruby attr_reader), which reinforces the idea that we should always have the set/get pair for each attribute in our classes.

However, the idea that each attribute must have a reading method can create programming bah habits, such as using getters for the simple fact that we have them available, then moving their own logic outside of the objects. .

The programming principle I want to talk about today: “Tell, Don’t Ask!” suggests us to avoid the (mostly irrational) use of getters and look for alternative ways to write cleaner, easier to read and more secure software.

Tabla de contenidos

Tell, Don’t Ask! principle

This principle was proposed by Martin Fowler:

Tell-Don’t-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data. It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do. This encourages to move behavior into an object to go with the data.

Martin Fowler

(I took the liberty of highlighting what I consider the fundamental part of this principle.)

Let’s see how this principle works in real-life embedded systems.

A microwave oven

To better understand this principle I want to use my microwave oven as an example: my oven (and most likely yours too) allows me to add or subtract time while food is being cooking; that is, each time I press the (+) button or the (–) button on the panel I can add or subtract 10 seconds, respectively.

I can add as much time as I want, but it’s important to notice that when it comes to deducting time the oven does nothing when there are less than 10 seconds left. Let’s see how to implement this logic without and with the principle.

Direct solution, without using the principle

A direct solution, WITHOUT USING THE PRINCIPLE, looks more or less like this:

  • The (+) button is pressed while the oven is operating. The program:
    • READ the remaining time.
    • ADD 10 seconds.
    • WRITE BACK the new time.
  • The (-) button is pressed while the oven is operating. The program:
    • READ the remaining time.
    • If the remaining time is greater than 10 seconds, then REMOVE 10 seconds; Otherwise it does nothing.
    • WRITE BACK the new time.

In code it would look like this:

   Device mw_oven;

   // ...

   if( key.read() == eKey::Button::Plus )
   {
      uint32_t remaining_time = mw_oven.get_rem_time(); // READ

      remaining_time += 10;                             // ADD SOME TIME

      mw_oven.set_new_time( remaining_time );           // WRITE BACK
   }
   else if( key.read() == eKey::Button::Minus )
   {
      uint32_t remaining_time = mw_oven.get_rem_time(); // READ

      if( remaining_time > 10 )
      {
         remaining_time -= 10;                          // SUBSTRACT SOME TIME

         mw_oven.set_new_time( remaining_time );        // WRITE BACK
      }
   }

Solution using at the principle Tell, don’t Ask!

  • The (+) button is pressed while the oven is operating. The program:
    • SHOWS that you want to add 10 seconds using a special function for this.
  • The (-) button is pressed while the oven is operating. The program:
    • SHOWS that you want to remove 10 seconds using a special function to do so.

In code it would look like this:

   Device mw_oven;

   // ...

   if( key.read() == eKey::Button::Plus )
   {
      mw_oven.add_time( 10_s );              // Tell, don't ask!
   }
   else if( key.read() == eKey::Button::Minus )
   {
      mw_oven.sub_time( 10_s );              // Tell, don't ask!
   }

In the first example (without using the principle) you asked the oven for its status, then you calculate the new time (outside the object!), and finally you write that new value back to the oven. Why do you, the client, have to be the one to carry out that logic? What happens if you do something wrong? And certainly, you will.

In the second example (using the principle), the oven is provided with operations that will try to carry out your request (.add_time() and .sub_time() operations) without your intervention!, You even don’t know how it does it.

Now the oven is doing what it is supposed to do. Who but the microwave oven itself knows how to behave as a microwave oven?

By the way, if adding time also has its own limitations, then you could do something similar to the .sub_time() method.

Did you notice that the version that uses the principle is clearer, more succinct, easier to read and maintain, does not expose the inside of the object, and, very importantly, is more secure? In a moment I will touch on the topic of secure code.

Let’s see how the .sub_time() method might look like. Pay attention to the fact that proving that there is enough time to deduct the amount indicated by the customer it’s the object’s responsibility and not yours.

class Device
{
   // ...
   uint32_t _remaining_time{0};

public:
   // ...

   bool sub_time( uint32_t time )
   {
      bool ret_val = false;

      if( _remaining_time > time ) // This logic belongs here!
      {
         _remaining_time -= time;

         ret_val = true;
      }

      return ret_val;
   }
};

(You may have noticed that I used 10_s, rather than simply the value 10. C++11 and later include a mechanism called User-defined literals that allows us to be more explicit about the units of measurement that we use in our programs. I talk about them in this entry of my blog.

What’s wrong with getter methods?

Getters expose the objects‘ internals

Getters have several problems: notice that in the first example the oven (or rather, the mw_oven object) exposes its internals by returning the remaining time. In what format has time been returned? mm:ss? seconds? Exposing the internal parts of an object is a clear violation of the principle of information hiding.

Imagine that a first version of the Device class returns the total number of seconds remaining and, consequently, you base all your logic on it. But later, in the next version, the author of the code changes his mind and returns the time as mm:ss, what are you going to do?

Getters could make you write unsafe code

That’s the main reasong why it is important that the object itself is responsible for adding or subtracting time and, where appropriate,, taking the necessary actions when the user makes an error.

The operation could return the new time for you to print it on the screen, or perhaps return a boolean value to indicate that the operation succeeded (or failed, and the client beeps), or perhaps throw an exception (and the oven turns off).

Anecdote

In the Data Structures and Algorithms class that I teach at the university (in C language), I teach my students that the destructor operation, in addition to returning the memory that the object requested, must set the pointers to a safe state so that they are not used incorrectly once the objects no longer exist:

void List_Delete( List** pThis )
{
   assert( *pThis );

   // ...

   free( *pThis );
   *pThis = NULL;    // Avoid future problems!
}

In the APIs for all the data structures I code with my students, the destructor functions are the only ones that require the argument to be a double pointer (and students hate double pointers). The reason is that it is the object itself the one that leaves the system in a safe state after returning the memory. If by mistake the object is used after it has been destroyed, then the system will crash, under control. The only way to keep the pointer null when exiting the function is to pass its address; this is, a double pointer. The important thing here is that the object does not trust the programmer =)

Let me insist, trusting the client to do the right thing is not correct.

Me, I’ve been there!

Are getters the evil?

No, not at all! In our oven example we need to know the remaining time in order to be able to print it on a screen, but in this case we would not be carrying out any extra logic.

Other examples where asking the object about its status is essential:

  • A Keyboard class. Here we want to know which key the user pressed. In fact, we saw it in the previous examples. If we didn’t have the opportunity to ask about the keys that has been pressed, then the class would be useless.
  • A Sensor class. If we are measuring signal from the real world, the most natural thing is to want to know the magnitude of the measurement; otherwise, the class would also be useless. The Sensor class should be able to return the measurement to us in the most convenient units to prevent us from performing external conversions, which would take us back to the problem: READ->PROCESS->WRITE.

In these examples we are only asking, we are not performing any type of logic specific to the object, which is why it is completely valid to have and use getters.

Finally, the point is not to avoid them at all costs like the plague, but to use them responsibly and where it makes sense to use them (just like with global variables, which are sometimes unavoidable and very useful).

Before we leave

  • Remember: It’s not about never using getters; it’s about using them responsibly and with good judgment, and keeping the private parts of a class private.
  • In your current and previous projects, see if you did not follow the principle Tell, Don’t ask!
  • When you are designing a class, pay special attention to those operations whose responsibility is the object itself and not the client, and apply the principle.
  • Consider using at first in future projects.
  • What other principles have you used? Tell me in the comments.

Have you heard of the real-time operating system, FreeRTOS? I wrote a small course on it (in spanish), you might want to check it out:

Índice del curso

I hope this entry has been of interest to you. If so, you could subscribe to my blog or share this post with someone who you think may be of help.


Fco. Javier Rodríguez
Escrito por:

Fco. Javier Rodríguez

Soy Ingeniero Electrónico con 20+ años de experiencia en el diseño y desarrollo de productos electrónicos de consumo y a medida, y 12+ años como profesor. Egresado de la UNAM, también tengo el grado de Maestro en Ingeniería por la misma universidad. Mi perfil completo lo puede encontrar en: https://www.linkedin.com/in/fjrg76-dot-com/

Ver todas las entradas