We Need to Talk About Encapsulation
and a little about constructors vs setters while we're here
Written by Anthony Chambers , and read8,685 times
Have you ever seen code that looks like this?
<?php $fullname = $user->firstName . ' ' . $user->lastName;
Of course you have! What about this?
<?php $taxRate = 0.2; $tax = $item->price * $taxRate; $total = $item->price + $tax;
Yeah, you know what I'm talking about, right? You have these $user
and $item
objects, but all that they're doing is storing public properties. If we're lucky, these properties may be declared in the respective classes. Something like this, maybe:
<?php class User { public $firstName; public $lastName; public $email; }
What is happening here is that the User
class is simply being used as a store for user related information. This is good, right? We can pass this User
object around and it'll always be the same and every piece of code that reads it will have the same properties available to read from. Excellent!
Only, soon you notice that you're now writing this everywhere:
<?php echo 'Logged in as ' . $user->firstName . ' ' . $user->lastName;
And let's be honest, this is starting to be a pain, right? What if you want to change it? What if you don't always have a last name? What if sometimes you have a middle name? You want to ensure that it's consistent everywhere that you use this, right? And you don't want to have to keep searching your entire code-base so that you can update EVERY SINGLE usage, do you?
No, you don't!
So why don't we put this functionality somewhere... I dunno... centralised? Ah, now we come on to the next thing that you've probably seen a hundred times or more:
<?php class Common { public static function userFullName(User $user) { return $user->firstName . ' ' . $user->lastName; } }>
Oh yes, we've all had a Common
class, haven't we? The place where all of your miscellaneous functions go that you will call from wherever you might need them. So now we will go back over all of our code where we've echoed out the user's full name, and do something like this:
<?php echo Common::userFullName($user);
Now we can call this handy helper from anywhere, and we can be sure that if we ever need to update it that we can just head off to our Common
class and edit it once, and everywhere will be updated immediately.
This seems like a great idea on the face of it, but there are a couple of issues with this approach:
Firstly, we will need to port this Common
class to any other site that we may build where we want to reuse our handy User
class, because we're really proud of this class and we want to take it everywhere and use it again, right? But without the Common
class we will need to resort back to concatenating the firstName
and lastName
properties again. That's no good!
Secondly, we're now far too concerned with the inner workings of this class, and we're exposing far too much about how it works to the outside world.
What we want to do is to hide this sort of thing inside the object so we never have to worry about it on the outside. We want to provide a simple interface to the object that you can use no matter where you are using the User
class and always get the same result.
So let's do this:
<?php class User { public $firstName; public $lastName; public $email; public function fullName() { return $this->firstname . ' ' . $this->lastname; } }
So now I can simply call the fullName()
method from anywhere, and it will just work:
<?php echo $user->fullName();
Now isn't that much nicer?
Do you need your Common::userFullName
function to continue to work? Just do this then:
<?php class Common { public static function userFullName(User $user) { return $user->fullName(); } }
Ah, this is much easier to use now, right? In fact, I might want to protect my properties, as they're publicly visible so they could be inadvertently set to something completely inappropriate:
<?php $user->firstName = new PDO($dsn, $username, $passwd, $options); echo $user->fullName();
We're going to have problems when fullName()
tried to concatenate a PDO object with a string, right? So maybe it's not such a good idea to use the public visibility on properties?
Visibility in PHP comes in three flavours:
-
Public: This is the default and if you don't declare your visibility then PHP will assume that you meant public. Your data can be overwritten by any code that has access to your
User
object. - Private: This means that ONLY this class can access your value. If you declare your property as private then you can't access it from outside of the object, or from an object that is extending your class.
- Protected: This is a sort of middle ground between Public and Private. You still can't access the property from outside of the object, but if you have extended the class then the extending class can still access this property.
Personally, I'm a fan of protected properties for most use cases because I can prevent outside tampering, but I can add additional functionality by extending my class if I need to. So let's change the visibility to protected:
<?php class User { protected $firstName; protected $lastName; protected $email; public function fullName() { return $this->firstname . ' ' . $this->lastname; } }
So we've left the fullName()
method public because we still need to access this from outside of the object, but the properties are now protected. So now we can't set the values:
<?php $user = new User; $user->firstname = 'Anthony'; // FATAL ERROR Cannot access protected property User::$firstname ...
So now we need to think about how to actually SET these protected properties since we can no longer address them directly.
There are two main choices here:
- Class Constructor: If these values are REQUIRED for the object to function then you should provide them when you are instantiating your object, and you should properly validate immediately.
- Setter Methods: If you do not require that these values are set for the object to function at all then you may set them using methods that exist purely to control access to a property.
Let's look at both now:
<?php class User { protected $firstName; protected $lastName; protected $email; public function __construct($firstName, $lastName, $email) { if (!is_string($firstName)){ throw new InvalidArgumentException('firstName must be a string'); } if (!is_string($lastName)){ throw new InvalidArgumentException('lastName must be a string'); } if (!is_string($email)){ throw new InvalidArgumentException('email must be a string'); } $this->firstName = $firstName; $this->lastName = $lastName; $this->email = $email; } }
I could make this much nicer, but the point is to illustrate that we can provide all required properties to the constructor and then be sure that we've got the correct types. We now know that whenever we use this User
object that we will always have a firstName
, lastName
and email
value, though we haven't validated that they're anything other than strings at this point (dhsjdhak
is a valid string, but it sure isn't a valid email address!)
OK, so what about using setter methods?
<?php class User { protected $firstName; protected $lastName; protected $email; public function setFirstName($firstName) { if (!is_string($firstName)){ throw new InvalidArgumentException('firstName must be a string'); } $this->firstName = $firstName; } public function setLastName($lastName) { if (!is_string($lastName)){ throw new InvalidArgumentException('lastName must be a string'); } $this->lastName = $lastName; } public function setEmail($email) { if (!is_string($email)){ throw new InvalidArgumentException('email must be a string'); } $this->email = $email; } }
So now we have created methods that can be called to set each property and they can be coded to provide adequate validation... more than I included. But the point is there.
Both of these solutions allow me to put all of the logic that this object needs into it's own class. I can add any validation here. I can ensure that I have the information that I need. I can handle those values if I need to as well. You no longer need to concern yourself if you're implementing this class with the inner workings.
Imagine a car: You know that you turn the key and it starts, idles but doesn't go anywhere. You know that you can put your foot on the accelerator and the revs go up, but you don't necessarily know how this happens. Nor do you notice the difference between an old car with carburetors and distributors, rotor arms and caps etc, or a new car with a digital system powered by an ECU. You don't need to care, because your interface is the same; the key, the steering, the pedals and the gear stick (if you drive a manual, of course).
Why don't you need to care?
Because the internal workings of the car are encapsulated behind the interface. In our code our interface is our public methods, and if I'm implementing your code then that should be all that I need to concern myself with.
I give my car fuel, a key and someone who knows how to use the interface and I can drive it. The same goes for any other car.
Now do it with your PHP.
BOOTNOTES
A word on class constructors vs setter methods
I've discussed in the past how a class constructor is your gateway to the class and unless you properly define your class dependencies in your constructor then it will not be obvious that you need to call a setter later in order for you to use your object. In our user example, imagine that I do this:
<?php $user = new User; echo $user->fullName();
What will this echo? It would be nothing but a single space character. Well that's not a full name at all, is it? And if I was calling a fullName()
method wouldn't I be a bit surprised when I only get a blank space? What's happened to the name? Wait, now I'm confused, how does the object even know what name I want it to have?
This is a relatively harmless example, but imagine if you needed to provide a database connection and you didn't do it?
<?php $user = new User; $user->setFirstName('Anthony'); $user->save(); // Whoops, I didn't call $user->setDbConnection() first!
So if we're using setters we need to ensure that our object can operate without any of them having been set. If you NEED something, make it an argument in the constructor. Otherwise you're throwing one of a god-knows-how-many exceptions to let the developer know that they didn't follow a step. And that's frustrating!
While you're on this subject, check out my post on mutable vs immutable objects for another insight into why setters and public properties can sometimes be a bad thing.