Who ever as a developer did not come across a code full of IF's and started having to debug the code mentally, wondering, what could happen when the break-point went into this or that if or else, or simply wrote a method full of IF's which in that moment made total sense to you, but after a few days or weeks without touching in that code, had to return to make a change ... and wondered what I was doing here? Took a longer time to understand your own code?
Often for the simplicity of writing codes in a procedural way, we faced with these situations, but concepts of OOP (Object-oriented programming) have emerged to facilitate the development of software. And we must make use of these concepts to write software that can be updated without the needs to change something that already is working, beyond it, we should to make extensible code and as important as develop, we must create testable code.
In many cases we can use OOP concepts to abstract procedural logic into an architecture of stable classes that will account for directing execution by our business rules. Having said it, today's post I will introduce an example of Strategy Pattern, a way to develop a code strategy based on some business rule and this strategy can be changed at runtime. Without more length, let's understand the problem and how we can solve it.
Imagine that we would like to provide different discount rates for each type of user of an application, in the following example three types of user will be treated. Being the first Standard that will not have any discount on purchases made, the second type will be Premium having 10% discount and lastly, the Diamond user who will own 20%. So we have different discount strategies by each type of user, being Standard = 0%, Premium = 10% and Diamond = 20%. To represent the types of users will be creating an Enum type class that going to represent each one of them.
UserType.cs
Let's create the user class.
User.cs
To represent discounts strategies, we'll create an abstract class that will have a method and a property.
DiscountRate.cs
From this abstraction will be creating three classes, representing each type of discount.
Standard.cs Premium.cs Diamond.cs
As an implementation detail, the PercentOfDiscountRate property was created as protected, so only classes that implement DiscountRate.cs will have access to it, in this case the Standard.cs, Premium.cs and Diamond.cs. Another interesting detail is that the classes have been marked as sealed , ensuring that they will be final classes and can not be inherited by other classes.
With these classes we can now proceed with the creation and implementation of purchasing logic. Let's create a PurchaseOrder class that basically will have a property with the value of the purchase, a user and a method to calculate the purchase price that can be discounted depending on the user type who is making it.
PurchaseOrder.cs
When we execute from this implementation, purchases in the amount of 1000.00 for each kind of user was obtained the follow result, that's right.
However, this code has some points of improvement. First, the first principle of SOLID Single Responsibility Principle, which says, a class must have only one reason to be changed and must have only responsibilities inherent to it, and analyzing the implementation created we can see that it has more responsibilities than it should, such as validating the user type to return the correct instance of DiscountRate, so if would be necessary to change or create another type of DiscountRate the implementation of PurchaseOrder will be affected, if would necessary to change or create new types of users it will also be affected and these are examples of modifications that should not affect the PurchaseOrder class, after all it's only goal is to return the updated purchase price and make the purchase.
How can we improve this code? First step would be to place the responsibility of returning the discount rate type to the user class.
Correcting SRP
With this change, the responsibility of returning the discount rate instance was transferred to the user class, since it has the information of which type it belongs, we also improved the Encapsulation of the User class, reaching another principle known in object orientation that is the principle of Tell, Do not ask, where objects should describe their actions and not be asked for an action to be performed, for example, when the PurchaseOrder were questioning the User, about what information was contained in yours UserType to make the decision for what flow of execution it should to follow, we have not refactored the user class to follow this principle but we will get there.
Although the improved taken in PurchaseOrder, we still hurting another principle of the SOLID, Open Closed Principle in the User class. The OCP says, a class must be open for extension, but closed for modification, why the principle is being hurt? The answer to this question is, when we want to know about the user type, we're doing it through IFs and in the future if there's a need to create a new type. The GetDiscountRate method will be affected, and we'll need to add a new IF to address this situation. If we stop to think the SRP principle also wouldn't be agree, because that would be an external modification, that's affecting the user class.
Now, another question, could this problem be solved? And how could we remove the IFs? YES, it is possible to work around this problem, but the question is how ?!
There is more than one way to solve it, we could use Dictionaries or static variables, that would return the instance of an object, but I will not try to solve using these approaches, another way that I want to share with you is solution using Attributes that can be used in classes, properties and why not in Enum types.
There is more than one way to solve it, we could use Dictionaries or static variables, that would return the instance of an object, but I will not try to solve using these approaches, another way that I want to share with you is solution using Attributes that can be used in classes, properties and why not in Enum types.
For it, we'll be creating a class that inherits from System.Attribute, thus allowing assignment custom attributes in classes or properties, if you've worked with MVC (Model View Controller), you should remember about DataAnnotations and the well-known tag [Required] in properties of ours view models, basically what will be creating is a tag that has a similar origin. At runtime it will be possible to know which class has the discount strategy for a given Enum (user type).
So let's create the UserTypeAttribute class that will give us this possibility.
UserTypeAttribute.cs
Having created UserTypeAttribute.cs, now it will be possible to place Custom Attributes on the user types and inform with typeof the class responsible for calculating the discount rate for a given type.
UserTypes.cs
Now, another question will arise, how can we return the instance of the this class which implements DiscountRate (Standard, Premium, and Diamond) to the User class and then return that instance to PurchaseOrder class?
The answer to this doubt in this example will be given through ExtensionMethods for the enum UserTypes that will used to return the instance of DiscountRate implementation, from an Activator that will be a property that going to be defined in UserTypeAttribute class.
UserTypeAttribute.cs
Extension methods are a special kind of static method, but are called as if they were instance methods in extended type.
To declare an extension method, it is necessary to create a static class with a static method and in this first parameter inform the type preceded by this modifier, thus giving the context for its execution.
UserTypesExtensionMethods.cs
Once the extension method was defined, let's simply develop the logic to return the correct implementation based on the parameter passed in userType parameter.
The implemented logic requires the use of System.Linq. From the typeof of UserTypes it is possible to access all fields declared by using GetFields method. Using the method Where with Linq you can perform a filter comparing by name Field with enum userType informed as a parameter.
UserTypesExtensionMethods.cs
The implemented logic requires the use of System.Linq. From the typeof of UserTypes it is possible to access all fields declared by using GetFields method. Using the method Where with Linq you can perform a filter comparing by name Field with enum userType informed as a parameter.
The return of Where method will be captured by the method Linq FirsOrDefault and if not null, you can retrieve the custom attributes which will also be a list, for this reason it's necessary to use the method First of Linq.
When converting the return with AS modifier, we perform a cast for UserTypeAttribute class allowing access to its properties, so we use the CreateInstance property that is populated from the custom markup above the enum field, eg: UserTypeAttribute (typeof (Standard))], so is returned the correct DiscountRate implementation to the calling object.
Success! When executing we have the same result.
Conclusion
In a simple example it was possible to approach several concepts of object orientation and to go into the detail of two SOLID principles, SRP and OCP, from their violation until their correction, the Stragy Pattern was approached in the abstract class DiscountRate, on which based on the user type the implementation of it, is changed and we enable such behavior at runtime. I hope to have added to the knowledge of you readers.
The example was based on a C# class application using .NET Core 2.1 and is in my GitHub already integrated with NUnit which is a unit testing framework. Feel free to take the fork of the repository and test the implementation.
This article has a Portuguese version on Medium, so if you had any problems understanding some concepts and Portuguese would be better for you, try rereading the Portuguese version.
References
- Programming in the Real World Design Patterns Vol. 1 - Fabio Silva Lima
- Object Orientation and SOLID for Ninjas - MaurĂcio Aniche
- Microsoft Extension Methods
- Microsoft Activator
Comments
Post a Comment