Wrapping Primitives Overview0:00
Our next rule instructs us to always wrap primitives and strings. And this is one where I'm going to give you a pretty strong word of caution. You need to be careful about this one. But first things first, what exactly is a primitive? Just think of it as the basic building blocks. So things like strings or integers or boolean. We're not dealing with anything overly sophisticated with its own set of behavior. They're a little more, well, primitive. So when this rule tells us to always wrap primitives, what exactly does that mean? Let me give you an example. Imagine that you have some kind of method in your system called cache, and that will accept some data as well as the number of seconds to cache that data. Now, this rule would suggest that rather than accepting an integer here, it should actually be its own type. So in that case, you would instead have a class Second here that would accept the seconds. And then
suggest that rather than accepting an integer here, it should actually be its own type. So in that case, you would instead have a class Second here that would accept the seconds. And then finally, you could have a getter to capture the number of seconds here. Now, it's true. The advantage is we are able to type into this now, which you could argue gives us a little more clarity. We also have the ability to, if it's relevant, attach behavior to this class. And third but not least, when we are using this method, for example, imagine in some other file you want to cache some array. Well, before you would do something like this, 50. But when we come back to that six months from now, we don't really know what that refers to. Is that 50 hours? Is it 50 minutes? Is it 50 seconds? You don't exactly know without clicking through and looking over the method definition. So again, many would argue that it's better to do something like this. You're adding
Criteria for Wrapping1:35
Is it 50 seconds? You don't exactly know without clicking through and looking over the method definition. So again, many would argue that it's better to do something like this. You're adding more clarity this way, and it makes it easier for your future self. Now, here's my word of caution. When using PHP, I wouldn't recommend that you blindly follow this rule that anything that's a primitive should immediately be wrapped within its own class. Instead, we should add some criteria to determine when it's appropriate and when it's not. For example, one, does it bring clarity? Things like this are always at the top of my list. Am I making my application more or less clear by making this change? So consider that. Two, is there behavior associated with this concept? And if the answer is yes, it probably means you should wrap this primitive in its own object. Here's another thing to consider. Should there be validation associated with this?
Value Object Validation2:26
And if the answer is yes, it probably means you should wrap this primitive in its own object. Here's another thing to consider. Should there be validation associated with this? So for example, if some function accepts an email address, is this a place where you would perform some kind of validation to make sure that the email address given is in fact a valid email address? And again, if the answer is yes, well, maybe you should instead create a value object in this case. Just think of a value object as a simple immutable object that represents some concept in your domain. So going with this example, we would have a class EmailAddress. Now we would accept that, and here is where you could do some kind of validation. For example, if not, filter_var, pass in our email, and compare that against FILTER_VALIDATE_EMAIL, then if that's the case, they didn't give us a correct email address, so we should throw
if not, filter_var, pass in our email, and compare that against FILTER_VALIDATE_EMAIL, then if that's the case, they didn't give us a correct email address, so we should throw an InvalidArgumentException. And we refer to this as the object maintaining and being responsible for its own consistency. Finally, we could just assign it like so. So again, it's up to you to consider your project and the complexity of your project and determine if this is something that makes sense. So we will say consistency here. And now four, does this represent an important concept in your domain? If the answer is yes, then let's make sure that we make it a first-class citizen. So let me give you an example. But first, we'll say important domain concept. Okay, so imagine that I'm building something like Google Maps. Well, in that case, things like latitude and longitude are very important concepts. So with that in mind,
Modeling Domain Concepts4:04
important domain concept. Okay, so imagine that I'm building something like Google Maps. Well, in that case, things like latitude and longitude are very important concepts. So with that in mind, I don't want to reduce them to simple latitude and longitude variables. They are important concepts in my domain. So instead, I want a Location class that will accept the latitude and longitude coordinates. The benefit is this now better reflects the actual terminology that humans would use. If we're talking about a person's location within the context of a Maps app, then it makes sense to have a Location object. Here's another example. Maybe you run some kind of workout place where people can sign up for membership and use all of your various equipment. Now, if you want to offer some tools to measure and monitor a person's weight, well, for that project, the person's weight is an important concept in your domain. So let's make sure that
Now, if you want to offer some tools to measure and monitor a person's weight, well, for that project, the person's weight is an important concept in your domain. So let's make sure that what was implicit is now explicit. So for example, we will accept the weight and then assign it. Next, we talked about behavior. Could there be behavior associated with this Weight class? Even if it's something as simple as, well, a person can gain or lose weight. So maybe we could represent that here. gain and then another method called lose. So at this point, you could either make it mutable, which means you would do something like this. this.weight++. Or if we want to pass in pounds here, we could say this.weight += the pounds. So if we instantiate this class and we set it to 150, if we gain five pounds, the weight property is now set to 155. However, if we're thinking of this as a value object, then it's generally recommended that we
instantiate this class and we set it to 150, if we gain five pounds, the weight property is now set to 155. However, if we're thinking of this as a value object, then it's generally recommended that we make these immutable, meaning they don't change. In the same way that you don't change the color red to green, you just have a new color. So from that point of view, we would just return a new Weight object that contains the new weight. And we could represent that with this.weight + pounds. Now, this is immutable. And the same thing will be true here. So pounds, but this time we will subtract it. Now, if we had some main class and this will be a bad name, something like WorkoutPlaceMember. And when you sign up, you'll give your name. Again, you need to decide, all right, name is a primitive here. Does that need to be wrapped within its own object? And in this case, I don't think it's necessary. So I don't want to add any more complexity than is absolutely necessary.
is a primitive here. Does that need to be wrapped within its own object? And in this case, I don't think it's necessary. So I don't want to add any more complexity than is absolutely necessary. So next we have weight, but that is an important concept. So we will type in that. We expect a Weight object here. Now, once again, when we instantiate this, I am a new WorkoutPlaceMember, John Doe. Well, before you would just have something like this, but the problem is we can think of this as a magic number. We don't immediately understand what that refers to. But if we instead change it to new Weight(160), it's that much more clear. And even better, like I said earlier, because weight is its own class, we can attach behavior here. For example, in any method where it's relevant, we can now say weight, and we're going to gain five pounds. Things like this would not be possible if weight was just equal to some integer. Maybe this workout
in any method where it's relevant, we can now say weight, and we're going to gain five pounds. Things like this would not be possible if weight was just equal to some integer. Maybe this WorkoutMember can work out for some period of time. So what if we had some method called workoutFor, and that will accept the length. And maybe what you want to do here is say for every hour that the person works out, their weight will be decreased using some kind of algorithm. Well, because weight is its own object, that part is easy. But what about the length? Is it hours or minutes or seconds, or does it assume that you're working out for weeks at a time? We don't really know. And it's true we could change this to hours, and that makes the source more readable. But again, when we are consuming this method, we still have the same burden. So your three options are one, just deal with it and click through to the definition to remember. Two, you could extend
TimeLength and Named Constructors8:21
when we are consuming this method, we still have the same burden. So your three options are one, just deal with it and click through to the definition to remember. Two, you could extend your API. So you could add workoutForHours, and then another method, workoutForMinutes. And in many cases, that's a really good way to go about it. You take these things that are a little vague, and you make them more explicit by extending the API. So that's an option. But third, if you don't want to do that, maybe we could represent this as its own concept. Maybe something like a TimeLength. Now, when we work out, we'll say John, we'll say John is going to work out for four. And again, in the past, we might have passed three, but it's at three minutes or three hours or three days. Now, we're going to say new TimeLength. But still, if we do this, it's still a little vague, right? So maybe we could use a named constructor, something like timeLengthFromHours and pass in three. Now,
going to say new TimeLength. But still, if we do this, it's still a little vague, right? So maybe we could use a named constructor, something like timeLengthFromHours and pass in three. Now, yes, a little more verbose, but also a little more readable, and also a bit more flexible. It's very clear at this point that John is working out for a time length of three hours. And even cooler, because this is its own concept, if we want that length in seconds, then part of our API for this TimeLength class could be in seconds. Or if we want that in hours, then we could extend that. And further, when John is specifying how long he worked out, he's not constrained by any measurement. So if he wants to report that in hours, he can. Or if he only worked out for 45 minutes, then we can represent that in a more readable fashion. So to show you what this could look like, just to code this up on the fly, maybe we have a constructor here that will expect
minutes, then we can represent that in a more readable fashion. So to show you what this could look like, just to code this up on the fly, maybe we have a constructor here that will expect seconds. So let's initialize that. However, we're going to make this private. If you want to create a new instance of this class, you have to use our named constructor methods here. So now, we could have public static function. If you want to create a new TimeLength span from minutes, then you pass in your minutes, and all we do is instantiate the object for you. So that's why we refer to it as a named constructor. return new static($minutes * 60), and that will give us the number of seconds. So if you worked out for two minutes, that's actually 120 seconds. Now, we can do the exact same thing for anything else. So if you want a fromHours, then we pass that in, and we instantiate the class once again. hours * minutes in an hour * seconds in a minute. Finally, we need
same thing for anything else. So if you want a fromHours, then we pass that in, and we instantiate the class once again. Hours times minutes in an hour times seconds in a minute. Finally, we need methods like inSeconds. If we want to represent a time length in seconds, then we could call this method, and that will just return the seconds in this case. And if we want in hours, then we would return the seconds, and then I guess that would be divided by 60 divided by 60, the inverse. Anyways, just a quick demo. But again, you see that it gives us just a little bit more flexibility, again, if this concept in our domain is important enough to be its own first-class citizen. So yeah, that about does it for this lesson. Remember my warning, though. Don't just immediately try to change every single string and integer into its own type. That's very likely going to be the wrong way to go about it. Instead, here's what I'd prefer you to do. See if you can argue for why
Final Caution and Recap11:50
change every single string and integer into its own type. That's very likely going to be the wrong way to go about it. Instead, here's what I'd prefer you to do. See if you can argue for why some particular primitive needs to be its own object. And go back to those reasons we discussed. Does it represent an important concept? Is there behavior associated with it? Like, is there some kind of validation that this needs to be responsible for? Let all of those things sink in, and then only on the condition that you've convinced yourself should you wrap it.
