Tell, don't ask
Getters & setters are a bad practice! Objects should ask each other to do something, not asking data and then do that “something” by themselves - this is the principle of "Tell, don't ask".
Hello, developers! 🚀
Welcome back to the Learn Agile Practices newsletter, your weekly dose of insights to power up your software development journey through Agile Technical Practices and Methodologies!
Let's dive into today's micro-topic!
Why is OOP still so complex?
I often say this: Object-Oriented Programming has become hard to understand and master. The main reason for this, in my opinion, is that OOP today is full of misconceptions that have spread over decades of Software Development. After all, Software Development is still a relatively young industry, and we probably have to improve how we teach programming to new people.
As you can imagine, these OOP misconceptions created a lot of difficulties in learning and becoming fluent with this paradigm even after years and years - most times, programmers believe they know OOP well but still fall into such misconceptions in their daily coding.
Today we focus on those kinds of misconceptions related to one of the most important things we do when developing software: moving/transforming data.
Some examples of such misconceptions are:
Treating objects as mere data structures
Let data go around with getters, setters, and public properties
Using primitive types such as string/arrays/int as parameters
Back to Canon OOP
The original definition of Object Oriented Programming states that we should:
Build a system made of objects that communicate with each other through messages.
We can understand this definition more if we try to understand what it implies by stating the consequences:
All software should belong to an Object
Objects should NOT communicate through data
The principle of “Tell, don’t ask” focuses on this definition and states that:
Objects should ask each other to do something, not asking data and then do that “something” by theirself.
This means that the “messages” that objects expose and use to communicate with each other should be built around behaviors, not data. In other words, every object should hide its state (internal data, which means internal properties) and expose a behavior.
A good way to identify behavior is by following the Command-Query Separation principle (CQS), which states that:
Every Object should only expose 2 types of methods: commands or queries.
A command is a type of method that changes the internal state of the object but does not return any data from it (void
return type). A query is a type of method that does not change the internal state but returns it, partially or entirely.
It’s important to note that when implementing a command method we can accept some little exceptions when really needed (think creating a record in a table and returning it’s id
) but no exceptions are allowed for queries: methods that we expect to only return data should only do that and never change the state.
A simple example
The main examples of wrong implementation of the “Tell, don’t ask principle” are getters/setters/public properties: we should avoid them because they are exposing data without any behavior around it. Even when we implement a query
method, it should have the form of a behavior (reading is a behavior, after all).
For example, imagine we have a SmartLightbulb class having a simple state composed by a boolean property to store the ON/OFF value; the class should also expose a method to turn the light ON and OFF.
We could implement that with getters and setters:
With the above class, we can turn the light on/off with the setLighted
method and then access the current value via the getLighted
method.
This looks very technical and data-driven, but not much behavior-driven.
Have a look at this different version:
With the turnOn
and turnOff
methods we are exposing the two behaviors we want to offer with our smart light bulb: turning it on and off. We also expose a way to access the information of whether it is currently turned on or off.
The difference at first sight might look only semantical (isLighted
is a more behavior-driven name than getLighted
) but this also changes our perspective and naturally identifies a home for any behavior related to checking if the light bulb is currently on or off.
For example, we may want to consider if electricity reaches the bulb: if this is not the case, the bulb might have an internal state of TRUE because it was ON when the lights went out, but we still want to return FALSE when a request to the isLighted
method comes in, because the light is currently OFF due to missing electricity.
This kind of behavior related to the electricity working or not would be moved outside of the class in the first data-driven example, while naturally belonging to the isLighted
method in the second, behavior-driven example.
But ORMs do this all the time!
Some of you might have noticed that most ORMs break the “Tell, don’t ask” principle: this is just an exception justified by the specific use case of such tools. ORMs are an abstraction over SQL to write and read to/from a database, and they usually do this by implementing the Active Record pattern.
Quoting Wikipedia:
The Active Record pattern is an approach to accessing data in a database. A database table or view is wrapped into a class. Thus, an object instance is tied to a single row in the table. After creation of an object, a new row is added to the table upon save. Any object loaded gets its information from the database. When an object is updated, the corresponding row in the table is also updated. The wrapper class implements accessor methods or properties for each column in the table or view.
The fact is that ORMs are all about reading and writing data to DB, and they abstract the SQL we use to do that by mapping the table to an object. By the way, those objects still have behaviors (write and read from DB, for example) but accept exposing the internal state as a compromise.
That’s why we must consider ORMs an (acceptable?) compromise, and an exception, not a rule to generally follow.
🧠 Test Yourself in 1 minute:
💡 Did you know? An interactive activity, like quizzes or flashcards, can boost your learning!
Take a 1-minute quiz and test how clear it was what you just read! 🤔 Don't miss out on the opportunity to boost your learning—try now!
What's the original definition of Object-Oriented Programming?
Insights Recap
Object-oriented programming is hard to understand: the main reason is that OOP today is full of misconception
These OOP misconceptions created a lot of difficulties in learning and becoming fluent with this paradigm because wrong beliefs persist
Some examples of misconceptions about moving/transforming data:
Treating objects as mere data structures
Let data go around with getters, setters, and public properties
Using primitive types such as string/arrays/int as parameters
Original definition of Object Oriented Programming: Build a system made of objects that communicate with each other through messages.
All software should belong to an Object
Objects should NOT communicate through data
“Tell, don’t ask” principle: Objects should ask each other to do something, not asking for data, and then do that “something” by themselves.
The public methods of our classes should be behavior-driven, not data-driven
The CQS principle is a great way to organize our methods:
Commands: a method that changes internal state but does not return any data (exceptions allowed for very specific situations)
Queries: a method that returns data from the internal state (all or part of it) but does not change that state (exceptions NOT allowed)
Getters/Setters/Public properties are the main examples of not respecting the “Tell, don’t ask principles”
The Active Record Pattern used in most ORMs is a very specific exception for a very specific need - not something to follow for other use cases
Until next time, happy coding! 🤓👩💻👨💻
Go Deeper 🔎
Legenda
📚 Books
📩 Newsletter issues
📄 Blog posts
🎙️ Podcast episodes
🖥️ Videos
👤 Relevant people to follow
🌐 Any other content