Introducing SRP Example0:00
Today, we will discuss what is probably the most well-known of the SOLID design principles, and that is the S in SOLID, the Single Responsibility Principle. Now chances are, even if you don't exactly understand what this refers to, you are probably implementing it in your code, at least to some degree. So let's dig in and view an example. Here you can see that I have a SalesReporter class. Let's quickly review the structure. So within my app directory, we have our vendor name. That can be anything you want, your company name. And then we have Reporting, SalesReporter.
That can be anything you want, your company name. And then we have Reporting, SalesReporter. Next, if I switch over to my composer.json file, you can see I have registered this for PSR-4 autoloading. So that's fairly simple stuff. Alright, let's dig back into SalesReporter. So we begin with this between method, and it looks like it accepts a start and end date. And then on line 10, looks like we are checking for authorization, may not be a good idea in here. Next, it looks like we call a query DB for sales between.
in here. Next, it looks like we call a query DB for sales between. Now I've made that method name especially explicit just to make it very clear that this method is querying the database. So notice we grab the sales table, and we grab all rows where the created_at field is between the start and end date, and then we grab the sum of all of those charges and divide by 100 just because you're likely storing your money in cents. So we divide by 100 to turn it back into dollars. So already, if you are paying attention, we've violated the SRP a couple times. But now we have one more thing, return the results.
So already, if you are paying attention, we've violated the SRP a couple times. But now we have one more thing, return the results. So it returns this format, and let's see, alright, well that looks like it just returns some kind of HTML representation for the sales. So it takes the sum, and it places it within an H1 tag. So if we were to try this out, maybe from our routes file, we could instantiate the SalesReporter, set our begin and end date, and then call report between. Let's see what we would get in the browser. However, because this is a sample app, I don't have any kind of user system in here, so I will temporarily comment that out.
Defining Single Responsibility2:37
And that's really what the single responsibility is about. And there's a number of ways to phrase this. For example, an object should only have a single reason to change. Another way to put it is a class should have exactly one job. But ultimately, all of these various explanations are just different ways of saying a class should have a single responsibility. If it has too many responsibilities, it's doing too much, at which point you should extract some of these various actions into their own classes and their own responsibilities. So to illustrate that, let's go back to SalesReporter and describe what exactly is wrong here.
So to illustrate that, let's go back to SalesReporter and describe what exactly is wrong here. Now for me, the first thing that pokes its head out is on line 10. Why should SalesReporter have any interest at all in the authenticated user? That's application logic. It does not belong in here. Or another way to think of it is, is it possible that we could consume this SalesReporter class without having an authenticated user? Will it always come from the web? Or could you maybe have some kind of service or API?
Extracting Authorization Logic3:34
Will it always come from the web? Or could you maybe have some kind of service or API? These are things you need to think about. So in this case, checking for the authenticated User doesn't belong here. It's not the responsibility of SalesReporter to care about that. So let's remove that entirely, and instead you could extract that to your controller. Next on line 10, we have this database query. So we are grabbing a table, fetching all rows that match a start and end date, grabbing the sum of all of those rows based on the charge, and then dividing by 100 to compensate for cents to dollars.
the sum of all of those rows based on the charge, and then dividing by 100 to compensate for cents to dollars. Now once again, we come back to this Idea that SalesReporter has too many responsibilities. It has too many reasons to change. Or a third way to think of it is, there are too many consumers of this class. For example, if our persistence layer were to change in the future, we would have to update this class. Alternatively though, if we were to change the way that we wanted to format the output, well, once again, this class would have to change. So those two alone violate the single responsibility principle.
Creating Sales Repository4:35
well, once again, this class would have to change. So those two alone violate the single responsibility principle. It is not necessarily this class's responsibility to understand what our persistence layer is or how to fetch that information. So instead, why don't we inject that through our constructor. Perhaps we have something like a SalesRepository. Now ideally, we would actually type in an interface, however, I'm going to save that for a different lesson in this solid series. So for now, this is okay. We will inject SalesRepository.
So for now, this is okay. We will inject SalesRepository. We will call it repo, and then we will initialize these fields, like so. Now we need to create the class. So why don't we call it AcmeRepositoriesSalesRepository? Next we can create that. So Acme, create a new folder called repositories. And now within there, SalesRepository. Alright, let's go ahead and populate this, namespace AcmeRepositories. And now for the class, SalesRepository.
Alright, let's go ahead and populate this, namespace Acme\Repositories. And now for the class, SalesRepository. Now this class will be responsible for our database-specific interaction. So if we go back, that means we can extract all of this into its own responsibility. And we will update this to public. And we can't forget to use DB here. And finally, let's change this method to something a little bit more friendly. How about salesRepository between startDate and endDate. Now we can go back here and update this a bit. We could say this repo between startDate and endDate.
Now we can go back here and update this a bit. We could say this repo between startDate and endDate. So now this class has one less responsibility. It's as a result easier to test. It's just more maintainable. Finally on line 23, we see this call to an oddFormat method. So that's just returning the output. But once again, why should this class care? Or why should it be this class's responsibility to output or format or print the results? Or here's another way to think of it.
Or why should it be this class's responsibility to output or format or print the results? Or here's another way to think of it. Is it possible that you will want your output in a different format? Right now we are just assuming HTML. But what about maybe we want it in JSON instead? Maybe we want it in some different format altogether. Well now, anytime you need to change that, this class would have to be updated. So we have a couple different ways to tackle this. One option might simply be to say, you know what, we're going to leave the formatting to the consumer of this class.
One option might simply be to say, you know what, we're going to leave the formatting to the consumer of this class. We're not going to worry about it here, in which case we would simply return this data. And remember, in a real life app, you might have some other things like some validation, maybe some kind of filter. I'm not really sure, but you get the idea. Anyhow, that would be one option. Just leave it up to the consumer to format it. However, another option, let's say you do want to have some kind of class-based formatting. How could we allow for that while still giving the flexibility to maybe do HTML formatting?
Extracting Output Formatting7:27
However, another option, let's say you do want to have some kind of class-based formatting. How could we allow for that while still giving the flexibility to maybe do HTML formatting or do some JSON formatting or whatever it is you need to do? How could we allow for that when we don't really know which format is going to be desired up front? What do we do in those situations? Well, you might think of it like this. If we can establish that it's not the sales reporter's job to format or print the output, then we immediately know that this should be extracted, so we can remove that right away.
then we immediately know that this should be extracted, so we can remove that right away. Next, if we open up the sidebar again within reporting, and this is a little contrived, but hopefully it'll get the basic idea across. Let's create an interface here, and this will describe how something can be formatted or printed for the user. Why don't we call it SalesOutputInterface.php? Now within here, of course, we can set up our interface, SalesOutputInterface, and now let's define what the contract is. Well, we just want to have a simple function here.
let's define what the contract is. Well, we just want to have a simple function here. Why don't we call it output? And that's it. Just a very simple contract that any implementation has to adhere to. Now if I quickly set up my namespace here, reporting, we could create our first implementation. So this one might be HTMLOutput.php. And then you could create another one if you need a different implementation. So class HTMLOutput implements SalesOutputInterface. And let's go ahead and import all of that and set up our namespace.
So class HTMLOutput implements SalesOutputInterface. And let's go ahead and import all of that and set up our namespace. Namespace is AcmeReporting. Now notice how we still have this red line here? Well, it's letting us know that we have to implement one of those methods. So that's what I mean when I say every implementation has to adhere to that contract. So now within here, we can print it however it is we need to. And again, remember, this is a little contrived, but it's the basic concept that's important here. And that will accept the sales.
here. And that will accept the sales. Now if you do need to format it in a different way, simply create a new implementation. Now in this case, notice how we're getting a red squiggly? We just need to make sure that it does match up with what we specified in the interface. Like so. And that should fix it. So if I hide the sidebar, let's go back to SalesReporter. Now we could do this in a number of ways. One might be to accept a third argument that implements that interface.
Now we could do this in a number of ways. One might be to accept a third argument that implements that interface. Or you could add another method, maybe like a helper method called format, that will accept an implementation of the output or the SalesOutputInterface. In this case, let's keep it simple and do the third argument. So SalesOutputInterface. And we will call it Formatter. Now we could simply say Formatter, and now because we know that any object provided as that third argument will adhere to this contract, we can, with confidence, call the output method and pass through the sales.
that third argument will adhere to this contract, we can, with confidence, call the output method and pass through the sales. So now notice in this case, SalesReporter doesn't care about how we format the data. All it cares is that whatever implementation we decide to use, it just wants to make sure that we adhere to that contract so that it can successfully call an output method. Now notice how much cleaner this class is. It has a single responsibility. So if we were to go back to our routes file, we need to pass through that repository. So new SalesRepository. And remember, this could also be handled through the IOC container, but I don't want to overflow
Wiring and Testing Output10:53
So new SalesRepository. And remember, this could also be handled through the IOC container, but I don't want to overflow you right now. The only remaining thing to do is right here. Notice we need to pass that third argument. So how do we want our results? In this case, we want it as HTML. So we could say new AcmeReporting HTML output. And that's it. So now we call this between method.
And that's it. So now we call this between method. We grab whatever it is we need to from the repository, and then we return the output. So now if we switch back to the browser, sure enough, it is formatted as HTML. However, a different consumer, if we need some different format, create a new implementation, inject that into the between method, and then once again, SalesReporter doesn't care. It doesn't even know the difference. It just knows there will be an output method that I can call. So give me whatever kind of object you want, and I will call an output method on it. So hopefully this helps.
So give me whatever kind of object you want, and I will call an output method on it. So hopefully this helps. When you're reading about these design principles, and you see page after page after page of theory, sometimes it can be really difficult to break it down to its core. And the core is each class should have its own responsibility. Or in other words, it should have exactly one reason to change. If you start identifying multiple consumers and multiple reasons for a class to change, well, chances are you need to extract some of that logic into their own dedicated classes.
