How to remove getters and setters
Join the DZone community and get the full member experience.
Join For FreeGetters and setters are one of the first abstraction step that is thought over public fields in object-oriented programming. However, the paradigm was never about encapsulating properties by providing a special reading/writing mechanism via methods, but about objects responding to messages.
In other words, encapsulation is (not only, but also) about being capable of changing private fields. If you write getters and setters, you introduce a leaky abstraction over private fields, since the names and number of your public methods are influenced coupled to them. They aren't really private anymore: for example in service classes I pass dependencies for private methods in the constructor, and the client code is never affected when these dependencies change. With getters and setters, a field addition, removal or renaming affects the contract of the class.
In this article, we're going to take this User Entity class and explore the many ways we have for removing getters and setters. The choice between them depends mainly on what you need them for: it's the client code that should decide what contracts wants from these classes.
<?php
class User
{
private $nickname;
private $password;
private $location;
private $favoriteFood;
private $active;
private $activationKey;
public function setNickname($nickname)
{
$this->nickname = $nickname;
}
public function getNickname()
{
return $nickname;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getPassword()
{
return $this->password;
}
// ... you get the picture: other 8 getters or setters
}
The various techniques are ordered by complexity. As always, I express use cases for the User class via a test case. All the code is on Github.
Constructor
First, we can pass some fields in the constructor. If we do not expose the field then, the value will be immutable and the client code will never even know that this value exists.
For example, our User has an immutable nickname, which serves also as a primary key:
public function testPassWriteOnlyDataInTheConstructor()
{
$user = new User('giorgiosironi');
// should not explode
//removed the setter as it cannot be changed
}
class User
{
public function __construct($nickname, $activationKey = '')
{
$this->nickname = $nickname;
$this->activationKey = $activationKey;
}
}
Information Expert
Next, we have the Information Expert pattern: assign an operation to the object with the greatest knowledge for accomplishing it. If you do so, you won't need to expose private fields via getters and setters, since you will model some behavior as code in that class, which can see private fields.
For example, when we register an User we want to activate it via email verification, so we send an activation key via mail. But to check it, we don't need to extract the value from the User object.
// Information Expert
/**
* @expectedException InvalidArgumentException
*/
public function testAnUserIsActivated()
{
$user = new User('giorgiosironi', 'ABC');
$user->activate('AB');
}
class User
{
public function activate($key)
{
if ($this->activationKey === $key) {
$this->active = true;
return;
}
throw new InvalidArgumentException('Key for activation is incorrect.');
}
}
This style is an example of the Tell, Don't Ask principle: we tell our User to do something instead of asking it for information and doing it ourselves.
Double Dispatch
Putting behavior inside an Entity class is nice, but sometimes the operation needs some external dependency to work, like a login mechanism needing a storage for the identity of the user (usually the session). We have two types of coupling to solve here:
- static: the User class should not depend on any other infrastructure class, which in turn refers the database or the session storage.
- runtime: the User class cannot hold a reference to an infrastructure object, for instance because we want to serialize it or to create it simply with a new operator, or our ORM does not support injection of collaborators.
The first issues is solved by introducing an interface implemented by the infrastructure class; the second by passing in the dependency via Double Dispatch instead of via the constructor like we do with service classes.
public function testUsersLogin()
{
$user = new User('giorgiosironi');
$user->setPassword('gs'); // will be removed in next tests
// in reality, we would use a SessionLoginAdapter or something like that
$loginAdapterMock = $this->getMock('LoginAdapter');
$loginAdapterMock->expects($this->once())
->method('storeIdentity')
->with('giorgiosironi');
$user->login('gs', $loginAdapterMock);
}
class User
{
public function login($password, LoginAdapter $loginAdapter)
{
if ($this->password == $password) {
$loginAdapter->storeIdentity($this->nickname);
return true;
} else {
return false;
}
}
}
Still an example of Tell, Don't Ask, but more real world now.
Command and changesets
You really have to put data inside this object: the user has just compiled a form and you have to get thos input values in. So how do we define that operation? As an atomic method call, by passing in what I call a Changeset but it's a specialization of a Command (not Command pattern but CommandQueryResponsibilitySegregation). In the simplest cases, it's just a Value Object or a Data Transfer Object with no behavior.
public function testCommandForChangingPassword()
{
$user = new User('giorgiosironi');
$passwordChange = new ChangeUserPassword('', 'gs');
$user->handle($passwordChange);
$this->assertEquals('gs', $user->getPassword()); //deprecated, will be removed in next tests
}
class User
{
public function handle($command)
{
if ($command instanceof ChangeUserPassword) {
$this->handleChangeUserPassword($command);
}
if ($command instanceof SetUserDetails) {
$this->handleSetUserDetails($command);
}
// support other commands here...
}
private function handleChangeUserPassword(ChangeUserPassword $command)
{
if ($command->getOldPassword() == $this->password) {
$this->password = $command->getNewPassword();
} else {
throw new Exception('The old password is not correct.');
}
}
}
Think about it: you will have to put these getters and setters somewhere; it's best to put them on an object which is a data structure than that on your Entity. This way:
- you will be coupled to the current fields only on this particular operation and not when you pass an User around.
- you will make it clear that you support only a full update operation, and it's not ok to call an isolate setter.
Actually in PHP you could just use an array as a Changeset, but a class provides a stricter contract. Also public fields are not viable for a contract as no error will be raised by PHP if you assign a non-existent field on a Changeset object.
Rendering on a canvas
In the Growing Object-Oriented Software mailing list, there has been recently a discussion on how to emulate getters via callbacks. This solution is the specular of our Changeset argument used for extracting data instead of updating it.
public function testCanvasForRenderingAnObject()
{
$user = new User('giorgiosironi');
$detailsSet = new SetUserDetails('Italy', 'Pizza'); //THIS may have set/get
$user->handle($detailsSet);
// canvas can also be a form, or xml, or json...
$canvas = new HtmlCanvas('<p>{{location}}</p><p>{{favoriteFood}}</p>');
$user->render($canvas);
$this->assertEquals('<p>Italy</p><p>Pizza</p>', (string) $canvas);
}
class User
{
public function render(Canvas $canvas)
{
$canvas->nickname = $this->nickname;
$canvas->location = $this->location;
$canvas->favoriteFood = $this->favoriteFood;
}
}
Again, the canvas is hidden behind an interface and can be anything: an HTML view, a form, a JSON or RSS feed generator...
CQRS
In Command-Query Responsibility Segregation, you use the ORM for mapping your objects in the database, and fill your report screens by querying it directly, or even by querying another store which is continuously rebuilt from your master database.
I don't know of any implementations of CQRS in PHP, but this mechanism promises to at least eliminate getters, as your domain objects will be write-only.
Conclusion
The full code is in this Github repository, as always.
You have no excuses now: go and ditch one of your getters and setters immediately. Your code will breath a bit of fresh air.
Opinions expressed by DZone contributors are their own.
Comments