[00:01] about one of the most popular design patterns in software engineering, the strategy pattern. I'll give you an example use case of where you could apply the strategy pattern and what problems it solves and also what [00:15] benefits it gives you moving forward. So, let's dive into the example. Here's the example that I will use to show you what problem the strategy pattern solves. So, what we have here is an order processor class and this is a very [00:28] common example when you've got a central piece of code that contains the logic for several different code paths. And what I mean by this is we have this method called calculate shipping cost on your order processor class and based on [00:42] the current shipping provider that's passed in as a parameter, we execute a completely distinct piece of code to calculate the shipping cost for that specific provider. We have a switch statement here and each case represents [00:56] one of the supported shipping providers. So, we've got FedEx, UPS, USPS, DHL. And future, we would need to add more case statements. And this type of code construct introduces multiple problems that we have to deal with. First, the [01:11] order processor class itself violates the open-closed principle. The open-closed principle states that a class should be open for extension but closed for modification. And in this case, if we want to extend the order [01:24] processor to support an additional shipping provider, we have to modify the it violates this principle. Another problem is that as our shipping calculation logic becomes more complex, [01:38] so does the order processor. In a real-world scenario, we can imagine this class growing to multiple hundreds and possibly thousands of lines of code depending on what we need to do to calculate the shipping cost and the more [01:51] shipping providers we support, the more complex this becomes. Another issue with this is that when we have multiple developers working on different shipping providers, it increases the chances for a merge conflict showing up. And what if [02:03] the calculations for a specific provider requires a specific dependency, like an external service that we have to call, but the other providers depend on something else. All of this would be forced upon the order processor even [02:16] doesn't require it. So, we are increasing the number of dependencies on the order processor to facilitate what's required to calculate the shipping costs for a single provider. And this is another thing that we want to avoid. So, [02:29] just to show you what this looks like, I'll place a quick breakpoint here and then start our application, which is going to trigger the calculation for a support. And based on the current shipping provider, we're going to run [02:43] the logic to calculate the required shipping cost. So, you can see what continue and in the console, we can see the output of our calculation. Now, how can the strategy pattern help us solve this? So, we first have to figure out [02:57] what is the common denominator between the different calculate methods. And what you can see here is that they need access to the order in order to calculate the shipping cost, plus any dependencies that are required, which we [03:10] can provide using dependency injection, mind that, and they have to return the shipping cost as a decimal value. So, our base building block is going to be a new interface that I will call an I shipping strategy. And what we want to [03:23] define here is a string representing the current shipping provider. And we're also going to have a decimal value, which is going to be our shipping cost, and this will be returned by the calculate cost method. So, this is our [03:36] first step. We defined our strategy abstraction, and then we need to implement our individual strategies. So, let's start with the first one, which is going to be for our FedEx shipping provider. So, with the strategy pattern, [03:49] you pull out the common behavior representing the strategy itself, and for each implementation, we need a dedicated class. So, this is going to be our FedEx shipping strategy, and it's going to implement our [04:01] abstraction interface. For the provider name, I'm going to define it as a read-only property, and it's just going to return FedEx. And then, in the calculate cost method, we just want to copy over what we already had inside of [04:15] our order processor. So, now we've moved the logic from the order processor into a strategy class. And we go ahead and repeat this for the remaining types that we have. So, I'm going to paste this three times, and then let's move them [04:28] one by one. So, the next one is going to be our UPS provider, and I'm going to update our class name to be called the UPS shipping strategy. We have to update the provider name here, and I'll just drop in the logic for UPS that was [04:42] previously in the order processor. Then, we've got USPS, so let's go ahead and define a class for that. So, this is going to be our USPS shipping strategy. We have to make sure to update the provider name, and you will see how this [04:56] fits later when we update the order processor. And our last shipping provider is DHL, so let me define a corresponding strategy to that. So, this is the DHL shipping strategy. I'll just have to match the same naming convention [05:10] as we had before. So, this should be capital case. And lastly, I'll drop over the calculation for our DHL shipping strategy. So, what does this approach give us? Well, firstly, now our strategies are defined in a smaller [05:23] scope within individual classes. And what we can do is move these into their own classes. I'll actually make a folder for our strategy implementations, and let's go ahead and move them inside. I will also update the namespaces, and let [05:37] me do that for my remaining strategies. I'll extract each one into its own file, and then I'm going to move them one by one into the strategies folder. So, let me go ahead and quickly do that. Here goes the UPS strategy and lastly, I'm [05:52] going to move the USPS strategy. And now the benefits should start being apparent. So, as I said, we've already got our reduced mental load because we're only dealing with a single shipping strategy within each class. If [06:05] for whatever reason these strategies need an external abstraction, so let me define an interface. I'm going to call this the USPS API that we need to call. Now, I can go ahead and just inject that inside of the strategy object. And let's [06:20] say this returns some sort of fee that we can add on top of the shipping cost and we can easily get this by calling our API abstraction. And you can imagine external service. All of the other shipping strategies do not need to know [06:36] about this abstraction. Whereas with our previous implementation, if we needed to introduce this, then it would have to be on the order processor, which means we with concerns that don't really matter to some of our shipping provider [06:50] benefit. I'm just going to revert this to make our testing simpler, but I hope you get the idea. The second benefit is that we can now test individual strategies, which means our test cases are also going to be simpler. Again, [07:03] imagine having to set up the entire order processor abstraction in order to test just one shipping provider. Whereas individual shipping providers in isolation. And I'm going to mention last [07:16] benefit a bit later. Let me show you what we have to do inside of the order processor to make it work with the strategy pattern. So, because we've got our strategies in the individual classes and also behind a common abstraction, I [07:29] can inject this as a dependency. So, let's say we have an IEnumerable of I shipping strategies. Let's call this the shipping strategies. And what we want to do here is map these from an IEnumerable, which we can easily provide [07:43] dictionary. So, I'll say private is going to be a string and it's going to return a strategy object. So, let's call this the shipping strategies as well. And in the constructor, we can [07:57] just select our shipping strategies, say to dictionary, and for the key, we want we're going to just return the entire strategy as an object. Of course, I have to assign this to our dictionary. Now, watch what happens with our [08:11] implementation of the calculate shipping cost method. We can replace all of this with something like this. I'll say if shipping strategies, try get value, and provider identifier, and we're going to get an out var shipping strategy. So, [08:27] we're going to capture the value in the dictionary if it's there, otherwise, we can throw a new argument exception, the same as we did before. And then, I can just say return shipping strategy, calculate cost, and pass in the order. [08:41] Now, I have to make sure that my condition says if not try get value, which is going to evaluate to true if we don't find the strategy with the line, then the shipping strategy is sure to be within the dictionary, and it [08:54] won't be null. So, now, I can remove this code. I'm just going to comment it comparison, but just compare the number of lines of code here to what we had here. Of course, we paid the cost in having to maintain multiple classes, but [09:08] we can also get rid of the individual implementations, and we would be left with something like this. I'm going to drop in a separate class for our previous implementation, which is going to stay there just for reference, but [09:20] compare this implementation here to this one here. Now, we still have to do a few more things in order to support this. So, this is going to actually be our previous implementation, and here, we're going to register our order processor [09:32] with DI, but we also need to register the individual strategies. So, I'll say at transient and then we're going to add our shipping strategy. So, let's start with FedEx. I'm going to copy this a couple more times and then let's add [09:45] UPS, USPS, and also the DHL shipping strategy. And now that we've wired this up with dependency injection, we can resolve it as an I numerable, which is what we're doing here, populate the [09:57] implementation for calculating the shipping cost to the concrete strategy object. Now, let me show you what our code looks like with the new example. I'll drop in a demo here. Let's place a breakpoint in the order processor and [10:12] let's start the application. You'll see when we hit this breakpoint that our populated. The dictionary contains our four strategy objects with each provider remember that the provider key is defined within the strategy itself. So, [10:28] then what we do is we check if the strategy is in the dictionary and if that's true, we're going to get back back a concrete instance that we can call to calculate the shipping cost. And with this, our order processor is now [10:42] I'll press continue and I'll have to do that a couple times for this to complete, but now think about what happens when we need to introduce a new shipping strategy. Let's say that we get a deal with Amazon and we want to [10:56] support them as a shipping provider. All we would have to do is to define a new strategy implementation. So, we implement the I shipping strategy interface. The provider name is going to be Amazon and then the calculation could [11:09] look something like this. So, then how do we wire this up with our order processor? We want to extend it, but not have to modify it. Otherwise, we would be violating the open-closed principle. Well, all we have to do is go to our [11:22] program file and register a new shipping strategy implementation. So, what we do is add the Amazon shipping strategy. And if I just wanted to test out this example, I'm going to pass in Amazon as the sole provider. Let's keep a [11:35] application. And now, I can just go ahead and check if this is in the dictionary. You can see that it is because it's automatically provided with dependency injection and I can just step into the implementation and calculate [11:48] the shipping cost for our new shipping provider. And we didn't have to change anything within our order processor implementation. It remained consistent while we were able to extend it with new behavior. So, now we are respecting the [12:00] open-close principle. It's easy to add new shipping strategies by just implementing the respective abstraction and registering it with dependency strategies in isolation and the dependencies for each strategy are [12:15] encapsulated within the respective classes for those shipping strategies. However, the downside is that we now have to maintain multiple classes for our shipping strategies. In this case, it's five additional classes inside of [12:27] our code base as opposed to having a switch statement with five methods below. Now, in this example, it might not seem like it was all that worth it, but in a real-world scenario, these methods would be a lot more complex. [12:39] They would have a lot more dependencies and it would be difficult to figure out what is going on inside as opposed to having each shipping strategy implemented within its own class where it's much easier to reason about what is [12:51] happening inside. So, there you have it. This is the strategy pattern implemented using an order processor and shipping provider example. And it's also a good example of how we can refactor our code to respect the open-close principle. If [13:03] this video, you can do so from the pinned comment right below. And if you want to learn more about design patterns, I recommend checking out this video here. I know I haven't talked about design patterns a lot on this [13:17] change, let me know in the comments. Thanks a lot for watching and until next Thanks a lot for watching and until next time, stay awesome.