Basic service example0:00
Let's talk about decorators and PHP. So imagine that you run some kind of car repair or car maintenance shop. So as you can imagine, you would offer a number of services. One of them, maybe your base service, would be the inspection. So if you were to represent that as code, you might have something like this. class BasicInspection, and the cost for that will be, how about, $19. So we'll keep it simple and just return 19. All right, so now if a customer wants to know how much a basic inspection costs, we can call a getCost method and echo that out. So if we run this file, sure enough, we get 19.
Stacking via inheritance0:35
call a getCost method and echo that out. So if we run this file, sure enough, we get $19. All right, pretty basic. But now what about a basic inspection as well as an oil change? How might we go about doing that? Well, maybe your first instinct is to do something like this. So now you have basic inspection and oil change. And that will return $19 for the inspection, and how about another $19 for the oil change? So now we can check that out, save it, and if I run it again, now it's $38. So you might think, all right, this is fine, no problem here.
Introduce decorator interface2:11
And that's specifically what a decorator allows us to do. So if we were to rewrite this, it might take the form of this. We could begin with a basic contract. The contract will be called CarService, and the only thing that's required is some way to determine the cost of the service. Public function getCost. So now, if we get rid of this, we have our basic inspection, and that will adhere to our contract. Okay, great, we have our base, so anytime somebody needs work done to their car, there will always be $25 to perform the normal inspection.
Inject and wrap services3:13
Well, with our current implementation, that wouldn't work. What we can do instead is for our decorator, we could inject it. So I could say protected CarService, and we will add that through our constructor, and I will type in that. Okay, so now for oilChange to work, it needs some kind of instance of a CarService. Now we can extend and append to the existing behavior of getCost. So now I can say return $29 plus this CarService getCost. Okay, let's take a look at this. So now we have a basic inspection. If we run this, we get $25.
So now we have a basic inspection. If we run this, we get $25. That's correct. But now we want to decorate this. We want our inspection, but also an OilChange. So we will essentially wrap our core service here with any number of decorators. So let's try it out. new OilChange, and if we now run it, notice we get 54, which is $25 plus $29. And really, though there's a couple inversions to this pattern, this is the basic syntax that you would use in php.
And really, though there's a couple inversions to this pattern, this is the basic syntax that you would use in php. So you have your core service, and then you have your decorator that you forced to implement the same contract, and that contract in this case is a CarService. Next, the decorator must accept within the constructor some instance or implementation of that same contract here. And this is what specifically allows us to build up these objects at runtime rather than falling back to something like inheritance, which clearly isn't the most flexible thing in the world. So now imagine that we have another one here.
Add descriptions to decorators5:16
and have them be as dynamic as they need to be. So for example, maybe we just want a tireRotation and the basic inspection. Well, we can do that and get the correct price here. Why don't we append to this? Why don't we also say a CarService needs some kind of details or description? So we'll say any implementation of a CarService needs to offer a description. So let's add that. The basic inspection will simply return basicInspection. Now for an oilChange, let's add to this. And like before, we can return this CarService, get the original description that this class
Now for an oil change, let's add to this. And like before, we can return this CarService, get the original description that this class is wrapping, and append to it, and oil change. And then we can do the exact same thing down here. Return this CarService, get description, and append to it, and a tire rotation. OK, so let's build this up again. This time I will save it to a variable. service equals new BasicInspection. And now we will echo service get description. OK, if we run it, we get basic inspection.
And now we will echo service getDescription. OK, if we run it, we get basic inspection. Simple. But now we also want to add a tireRotation to that. So let's wrap it. Now if we run it, we get basic inspection and a tireRotation. But also later the customer decides, oh, I also want a oilChange. Well now we don't have to build up another class or refer to inheritance to find a way to make this work. We just decorate it as needed at runtime.
Why not modify base class7:38
Well, to give you an example, imagine that we just had a simple CarService class here that wasn't part of a contract. OK. So we have getCost. We have getDescription. And then maybe for each of these, you could have public function with oilChange. And you could have these sorts of methods. And then you could have another one with tireRotation. And each of these would dynamically update the cost. So I could set a setter here, setCost, and then say $this->cost equals $cost.
And each of these would dynamically update the cost. So I could set a setter here, set cost, and then say this cost equals cost. All right. So now with oilChange, you might say this cost plus equals $29. And then on that note, maybe the base cost will be $25. So we can return this cost. All right. So now for tireRotation, we could do the same thing here. And if tireRotation is $15, we can return that. Now this is a quick and sloppy demonstration, but think to yourself, what are the downsides
And the answer is no. So every time we add a new service, you would have to return to this class and add additional methods and possibly change existing code here. So when you return to these classes and you modify them, you introduce the possibility of breakage and more bugs. However, if we instead use interfaces to extend functionality and behavior when we need to, we never have to worry about any of this stuff. We can instead get rid of all of this and defer to decorators to add behavior when we need to. And luckily, as long as each of these related decorators implement a common interface, doing
need to. And luckily, as long as each of these related decorators implement a common interface, doing this stuff is trivial. Let's make sure that whatever you extend will eventually make a call to that class that it's wrapping. Because if we scroll down, think of TireRotation as wrapping a basic inspection. And OilChange, too, is wrapping the TireRotation, which is wrapping the basic inspection. So that means when we call OilChange here, it is returning its cost plus the TireRotation's cost. Subsequently, the TireRotation's cost will return 15 plus the cost of the class that
cost. Subsequently, the TireRotation's cost will return 15 plus the cost of the class that it is wrapping, which in this case would be the BasicInspection class that we've already removed. But you get the basic idea. All right, so that'll do it for this lesson. I think you'll find a number of situations when this would be appropriate in your code. Specifically, it's appropriate in those instances where you want to change or adjust behavior in some way, and you're feeling pressured into simply inheriting from another class. But always ask yourself, if I inherit from that class, do I really need to pull in the
in some way, and you're feeling pressured into simply inheriting from another class. But always ask yourself, if I inherit from that class, do I really need to pull in the entirety of that functionality? Or do I instead need to simply adjust the behavior of one or two methods? If that's the case, then maybe you can refer to the decorator pattern.
