مرور جایگزینی لیسکوف (Liskov Substitution)0:00
At this point, we are halfway through our discussion of the solid principles. Now like I noted in the earlier videos, what you'll find as you learn about this stuff is that all of these various techniques and principles, they're all intertwined. So you'll hear the same phrases pop up over and over again. And one of those phrases is code to an interface. Hopefully by now we've established that this is a good practice. It allows us to ensure inputs. So for example, when you have a function that accepts an interface, some interface, we can rest assured that any implementation that is passed to this function will adhere to that contract.
Defining Liskov Substitution0:35
we can rest assured that any implementation that is passed to this function will adhere to that contract. But now we come to the L in solid, and this stands for the Liskov Substitution Principle, Liskov being the name of the creator, Barbara Liskov. Now if you were to take a look at the mathematical definition of this principle, it would be really confusing and likely just fly right over your head, just as it did mine. Let's instead break all of that down to something a lot more consumable. Think of it like this. Every time you prepare a subclass, that subclass should be substitutable in every place where the original class was accepted.
Basic Subclass Substitution1:09
Every time you prepare a subclass, that subclass should be substitutable in every place where the original class was accepted. Or to tweak that definition just a bit, any implementation of an abstraction or an interface should be substitutable anywhere that the abstraction is accepted. So to illustrate that, let me show you three different examples. First, we will begin with the absolute basics. So this principle states that if I have ClassA, and let's imagine that it has a function, maybe something like fire, then if we were to create a subclass, ClassB extends ClassA, according to this principle, we should be able to use ClassB anywhere that ClassA is accepted. So if I were to copy this right here, imagine that you have some function doSomething,
to this principle, we should be able to use B anywhere that A is accepted. So if I were to copy this right here, imagine that you have some function doSomething, and that should accept an instance of A, and then we are to do something with that. The specifics don't really matter, but the principle states that if this accepts A, then we should also be able to swap in or substitute B, and everything should continue to work perfectly. Now you may look at this and think, wait a minute, this is common sense, right? Well let's dig a little bit deeper. Here's another example. Imagine that you have some kind of video player.
Video Player Violation2:25
Here's another example. Imagine that you have some kind of video player. Maybe this class should have a play method, and that should do whatever is necessary to play the video. But now let's imagine that if we are playing, for example, AVI videos, that's a little bit different. So maybe you do something like this. AVIVideoPlayer extends VideoPlayer. Now we override the play method, however, this time we do something a little bit different. Maybe we check the extension of the file.
Now we override the play method, however, this time we do something a little bit different. Maybe we check the extension of the file. So for example, when you call the play method, you provide the name of the file to play. So maybe when we override this, we say something like this. If, and let's do pathinfo to grab the extension. So let's say pathinfo(file), and we want to grab pathinfo(file, PATHINFO_EXTENSION). So we'll say if the extension is not AVI, then in that case, maybe we will throw some kind of Exception. Now at first glance, you might think, all right, well, this is probably acceptable. We subclass it.
Now at first glance, you might think, all right, well, this is probably acceptable. We subclass it. We override the play method just to do a quick check to see if it is an AVI file. And if it's not, in that case, we will throw an exception. However, this code right here violates the Liskov substitution principle. Now why is that though? What is the violation here? Well, one of the rules is that the preconditions for the subclass can't be greater. So in this case, no longer can we substitute it anywhere else because the output could potentially be different.
So in this case, no longer can we substitute it anywhere else because the output could potentially be different. If we use this class and we call play, then we should expect some kind of response. However, the LSP states that we should be able to substitute this anywhere that this is accepted. Unfortunately, that's not the case because if we were to use this and the extension does not match, then we throw an exception and that's what creates the violation. So as you can hopefully see, this principle helps protect us against those situations where some kind of descendant exposes a behavior that's quite different from the original parent class or the original abstraction or interface.
Repository Return-Type Mismatch5:00
However, there's just one problem. That contract validates the input, but it says nothing about the output. Let me give you a Laravel-friendly example of this. If you watched the lesson at Laracasts on repositories, then you know that they can provide significant benefits. Imagine that we set up an interface here, maybe something at Laracasts, like LessonRepositoryInterface. This is the contract. It's a binding contract. So maybe we need to provide some way to get all records.
It's a binding contract. So maybe we need to provide some way to get all records. So we could say getAll, like so. Now we have our contract set up. So we have our first implementation. Maybe originally we just store everything within a file, and we're going to have that implement LessonRepositoryInterface. Next, like I've noted many times, because we implement the interface, we have to adhere to the contract. So now within here, we do whatever it is necessary to query the file system and return the results.
to the contract. So now within here, we do whatever it is necessary to query the file system and return the results. For now, we will represent that as an array. So at this point, everything is great. But now let's do another one, and this will be our database-specific repository. Well now, maybe you have an Eloquent model. You probably do. So you just return something like Lesson::all(). Now although this looks okay, we are coding to an interface, the problem is that the return values are different, and that too breaks the Liskov substitution principle.
Now although this looks okay, we are coding to an interface, the problem is that the return values are different, and that too breaks the Liskov substitution principle. In this case, we have an array, and in this case, we have a collection. That means that the consumer of either of these implementations won't work identically. In one case, we have an object that has a number of methods, and in another case, we simply get an array. This truth breaks the LSP. Now I will be the first to tell you that specifically for this example, where we are using repositories, sometimes I break this rule. But nonetheless, it's really important that you understand that we actually are breaking
sometimes I break this rule. But nonetheless, it's really important that you understand that we actually are breaking a rule, and there could be ramifications for that. Now really, if we want to adhere to it, then you might use something like dog blocks. So you could say, fetch all records, and then we can comment that it should return an array. Just remember that php isn't really going to do anything with this, it's simply a hint. However, sometimes that can be enough, if you're careful. Unfortunately, we don't have many other choices in php. Just remember though, if you see an interface that says return an array, make sure that all of your implementations adhere to that contract, even if the language doesn't force
Just remember though, if you see an interface that says return an array, make sure that all of your implementations adhere to that contract, even if the language doesn't force you to. So in our case, we might want to do something like lessonAll to array. That way, no matter what implementation we use, both offer a getAll method, and both will return an array of results. Now though, imagine that we didn't do this. Where is the pitfall here? Well, if we had some other code that consumed this repository, we will use pseudocode here. We'll say we have some kind of function foo that accepts some implementation of lesson
Avoiding Consumer Type Checks7:56
Well, if we had some other code that consumed this repository, we will use pseudocode here. We'll say we have some kind of function foo that accepts some implementation of LessonRepository interface. Because we've type hinted an interface rather than a concrete class, we've freed ourselves in many ways. Now, as long as whatever we pass to this function conforms to that contract, or behaves according to the terms of that contract, we can use it. Or like you might have heard around the web, then it must be a duck. Now though, continuing on, imagine that we have our lessons, and we call lesson->getAll(). We have access to that method.
Now though, continuing on, imagine that we have our lessons, and we call getAll. We have access to that method. Now, if the returned types are different, which they are in this case, you might find scenarios where you have to do something like if isA, and then compare the value against the type. And like we learned with the Open/Closed Principle, whenever you find yourself doing type checking, that's a dead ringer that you are breaking one of these principles. Or in fact, like I noted, because the teachings from these principles are linked in many ways, if you are breaking one principle, then chances are, in effect, you are also breaking the other principle.
if you are breaking one principle, then chances are, in effect, you are also breaking the other principle. So as a result, if you ever find yourself doing checks like instanceof, then not only are you likely breaking the Open/Closed principle, but you are also breaking the Liskov Substitution principle. Before learning about this principle though, you might have done something very much like this. Well, we don't exactly know what we get, so let's do a quick if statement. And if we get a Collection, then we want to respond in this way. But if we instead get an array, then we should respond in that way.
And if we get a collection, then we want to respond in this way. But if we instead get an array, then we should respond in that way. That breaks this principle. Instead, always make sure that the output of your implementations match what is specified in the contract. So, in closing, I will let you review a quick list of ways to adhere to the Liskov substitution principle.
