DEV Community

Alexandr
Alexandr

Posted on

1

Getters and Setters in Objects

In programming, there are many different approaches to structuring code. For example, the procedural style—familiar from school or university—where a program is described as a series of sequential instructions:

step1($data);
step2($data);
step3($data);
Enter fullscreen mode Exit fullscreen mode

This programming style is easy to grasp and was the main approach for many years in languages such as C, Pascal, and others.

However, in modern PHP and frameworks like Laravel, the object-oriented approach is commonly used. In this paradigm, we create classes, instantiate them, and use methods to manage the behavior of objects. OOP is based on the idea that data and behavior should be encapsulated within a single “living” object. For example, consider the App class:

$app = new App();

$app->start();
Enter fullscreen mode Exit fullscreen mode

In this case, we don't instruct the program step by step; instead, we delegate the complete initialization and execution to the object. This approach hides implementation details and provides a clean interface for interacting with the object.

Yet, there is a vast amount of code and practices in OOP languages that remain procedural in nature. Let’s explore a few examples.

Dumb Objects Lead to Procedural Code

There is a common practice where an object is used purely as a container for data—it contains no business logic, only data storage. Consider the following example:

class User
{
    private string $name;
    private string $email;

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): void
    {
        $this->email = $email;
    }
}
Enter fullscreen mode Exit fullscreen mode

Such a class is often mistakenly called a "Data Transfer Object" (DTO), but that's not accurate. A DTO should be immutable, and having setters violates that principle.

Here, the User object is used solely for storing data. Later, this data is processed in various parts of the application. For instance:

// In one part of the code, an object is created and populated with data
$user = new User();
$user->setName('John Doe');
$user->setEmail('john.doe@example.com');

// In another part of the code, the data is validated
$validator = new Validator();
$validator->validate($user);

// In yet another part of the code, an email is sent to the user
$notification = new WelcomeNotification();
$notification->send($user->getEmail());
Enter fullscreen mode Exit fullscreen mode

In this example, the User object doesn't possess its own logic—external classes like Validator and Mailer handle data processing. Essentially, rather than using an array to store information, we use an object; however, the business logic is moved outside it. This results in an architecture that is closer to procedural programming, where the program consists of a set of functions acting upon data.

A similar situation is observed in ORMs that do not use the Active Record pattern, such as Doctrine:

$entityManager = EntityManager::create($connection, $config);

// Create a user object
$user = new User();
$user->setName("John Doe");
$user->setEmail("john.doe@example.com");

// Save to the database
$entityManager->persist($user);
$entityManager->flush();
Enter fullscreen mode Exit fullscreen mode

This contradicts the principles of object-oriented programming because objects should be responsible for their own data and behavior.

As a result, we end up with a system where logic is distributed between “dumb” objects and procedural sections of code. This separation of responsibilities makes the application harder to understand, test, and further develop.

Tell, Don’t Ask

Tell, Don’t Ask is a well-articulated principle of quality object-oriented design that helps in creating a clean and maintainable architecture.

"Don't ask an object for data to make a decision—tell the object what to do."

At first glance, this principle might seem to contradict the Single Responsibility Principle (SRP), but in real-world development, you often have to choose which principle is more important in a given context. Still, the more principles you manage to adhere to simultaneously, the better.

Instead of creating a “dumb” object, you can encapsulate business logic directly within the object or in a related service. For example, if you need to validate data and send a welcome email, you can delegate this responsibility to an object or service that knows how to handle it. Consider the following two approaches:

Rather than extracting data and validating it externally, let the object perform the necessary action itself:

class User
{
    protected string $name;
    protected string $email;

    private function validate(): bool
    {
        $validator = Validator::make([
            'name'  => $this->name,
            'email' => $this->email,
        ], [
            'name'  => 'required',
            'email' => 'required|email',
        ]);

        return $validator->passes();
    }

    public function register(): void
    {
        $this->validate();

        // Save the user to the database...

        $this->notify(WelcomeNotification::class);
    }
}

$user = new User(
    name: "John Doe",
    email: "john.doe@example.com"
);

$user->register();
Enter fullscreen mode Exit fullscreen mode

Here, the object itself performs validation and sends notifications rather than simply exposing data for external manipulation.

Additionally, the Tell, Don’t Ask principle works well with the Law of Demeter (LoD), which also promotes the creation of more resilient and independent objects.

For further reading, you can explore works by Martin Fowler, Allen Holub, and Yegor Bugayenko.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (1)

Collapse
 
xwero profile image
david duymelinck • Edited

Separation of concerns is not a bad thing.

You write objects, but you mean entities. Not all objects have the same task that it why they have different names.

The validation in the entity only makes sure that it is good for storage. This doesn't exclude data validations in other parts of the application.

There is a reason in Domain Driven Design infrastructure is separated from application and domains.
And that is why the Doctrine example is bad to demonstrate your point. It separates the execution of the database queries, infrastructure, from the data structure that is used by the application.

Entities should control their own lifecycle; creation, manipulation and deletion. How the application handles that lifecycle is not in the scope of the entity.
And that is what again what the Doctrine example shows. The EntityManager is responsible for the lifecycle with the persist and remove methods.

There are all kinds of architectures, from hybrids to pure. But the last example is not what I want to see in a codebase.

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

If this post resonated with you, feel free to hit ❤️ or leave a quick comment to share your thoughts!

Okay