Yie old Reliable Data Structures and Their Controversial (Read) Access.
Using objects as data structures is an established practice that generates many problems associated with the maintainability and evolution of software. It misuses brilliant concepts that were stated five decades ago. In this second part we will reflect on the reading access of these objects.
In the first part of this article, we showed the transition from hidden information in data structures towards living objects responsibilities (the essential what) hiding the implementation (the accidental how).
In this second part, we will show the drawbacks of using getters.
Programmers conventionally use the names of the form getAttribute…() to expose (and lose control of) a previously private attribute. Due to the same arguments stated on setter’s article, this name cannot be mapped to a real-world equivalent through bijection.
The final conclusion regarding these names is:
There should never be methods of the form setAttribute…() or getAttribute…()
Many objects manage collections. The contents management, the invariants or the traversal method should be the sole responsibility of these objects.
Suppose we want to draw the polygon presented on Part I on a canvas. We will achieve it with the following code:
$triangle = new Polygon([(new Point(1,1), new Point(2,2), new Point(3,3))]);
$lastPoint = $triangle->getVertices()->last();
foreach ($triangle->getVertices() as $vertex){
$canvas->drawLine($vertex, $lastPoint);
$lastPoint = $vertex;
}
By exposing the vertices collection (and since collections are passed by reference in most languages) we lose control over that collection.
Nothing prevents this other code from running:
$triangle = new Polygon([(new Point(1,1), new Point(2,2), new Point(3,3))]);
array_shift($triangle->getVertices());
array_shift () removes the first value from the array
This causes the triangle to mutate, generating an inconsistency in real world bijection. Two-sided polygons would violate the principle of being a closed figure.
This defect will be noticed a long time later because it has not been detected in time, thus violating the fail fast principle.
In no case shall such objects expose their collections, thus enforcing Demeter’s law.
In case you need to return the collected elements, you should answer with a copy (shallow) to avoid losing control. With the current state of the art, copying collections is extremely fast. If they were very large collections, there are design solutions with iterators, proxies and cursors to avoid performing the full copy operation.
How do we solve the polygon draw operation ?
Iterating a collection is a well-known topic when working with design patterns:
If we want to go around our polygon, we can return an iterator (indicating what we need to do) without revealing our underlying data structure (how we traverse it).
public function iterator(): Iterator {
return new ArrayIterator($this->vertices);
}
In case of languages supporting anonymous functions or closures, we could take the responsibility of iterating elements without exposing an iterator outwards:
public function verticesDo($function) {
foreach ($this->vertices as $vertex) {
$$function($vertex);
}
}
Polygons must not mutate because vertices are part of their minimal essence: if we remove any of their vertices, they are no longer that polygon that makes them unique.
There are many business objects that can mutate in their accidental collections and there are mechanisms to manage such mutations.
If we wanted to model a Twitter account and keep its followers, knowing the business rules, the account is created without followers.
let’s ignore the suggestions that it offers us when creating the new account.
final class TwitterAccount {
private $followers;
private function __construct() {
$this->followers = [];
}
}
Using setters and getters, a novice programmer would be tempted to add a follower in this way:
$mcsee1 = new TwitterAccount('mcsee1');
$pontifex = new TwitterAccount('pontifex');
$mcsee1->getFollowers()[] = $pontifex;
A correct responsibility assignment guided by business rules suggests that it is the account’s responsibility to add a new follower, carry out validations (for example, that it is was not followed previously) and keep collection integrity.
Therefore, a better solution would be:
$mcsee1 = new TwitterAccount('mcsee1');
$pontifex = new TwitterAccount('pontifex');
$mcsee1->addFollower($pontifex);
In the 90s there was a tendency to create a double encapsulation of attributes as an extreme approach on privacy. This means that, even from the private methods of an object, direct access to variables would be avoided.
This practice does not generate any benefits. Adds unnecessary indirection, and expose setters and getters in languages that have no distinction between public and private methods.
In addition, it hides the coupling between an attribute and the direct methods that reference it, avoiding possible refactorings.
There is a principle that states:
Don’t ask for the information you need to get the job done; ask the object that has the information to do the work for you
This principle is known as: Tell, don’t ask.
It reminds us that, instead of requesting data from an object and acting on this data, we should tell an object what to do. This encourages movement of behavior along with the knowledge that the object is responsible for managing.
Paraphrasing Demeter’s Law and minimum coupling and maximum cohesion laws:
Adding accidental complexity with setters and getters implies generating coupling, violating these rules and generating a greater ripple effect in case of possible changes.
Photo by
Macau Photo Agency
on
Unsplash
Let’s go back to our only design rule that asks for a bijection between the model we are building and the real world and respecting the principle of Anthropomorphism (giving a living entity to each object).
In doing so, we will discover that the responsibilities we give to objects after they have been returned with a getter do not map with the real world violating bijection.
On this page there is an excellent example of disrespected anthropomorphism when using getters.
When we start to model our objects forgetting about their accidental representation, we will be able to avoid anemic classes (which only fulfill the function of saving data, resulting in a well-known anti-pattern).
As with data structures, there is no way for an anemic class to guarantee the integrity of your data and relationships.
Since operations on anemic classes are outside of anemic classes boundaries there is no single point of control. Therefore, we will generate both repeated code and access points to these attributes that exist in our model.
We will always pursue to emulate the behavior of objects like black boxes, getting much more realistic and declarative bijections.
Not all is bad news. Converting a bad model into a good one is possible through a correct responsibilities reassignment, and with the help of the appropriate refactors.
If we have the opportunity of improving a system with good coverage, we can gradually encapsulate the objects, restricting their access in an incremental iterative way.
In case of not having enough coverage we will be in front of a legacy code system according to the excellent definition of Michael Feathers:
A legacy code system is one that has no coverage.
Should this be the case, we must first cover the existing functionality, and then we can carry out the necessary transformations.
There's an example addressing this problem here:
Photo by
Greg Nunes
on
Unsplash
The well established practice of using setters and getters generates coupling and prevents the incremental evolution of our computer systems.
According to the arguments stated in this article, we should restrict their use as much as possible.
Part of the objective of this series of articles is to generate spaces for debate and discussion on software design.
We look forward to comments and suggestions on this article.