I know some of you are on holidays or coming back from some paradise... ok, that's not my case! Some people like me are keeping the world turning :P
Anyway... I would like to continue with more posts and today I bring you some more theoretical but really interesting if you never face those principles before. Listen closely:
SOLID is an acronym made up by Robert C.Martin to stablish the fundamentals in object-oriented programming. The main purpose is to get "low coupling" and "high cohesion" in our applications.All of this is mainly for the maintenance phase: to keep the code readable, maintainable and be able to create new functionalities.
Sometimes we just want to add a new method to a class that has no relation with the main purpose of that class. We just one to add that reusable method to avoid extra work creating a proper class where contain it.
The problem appears when we need to access that method from another class, if now we don't re-factorize that class and create a new one to contain our method, in the future we will be facing classes which contains functionalities not defined for them. In the end, only the developer who code the class will be able to find easily that pieces of code but for the rest of the team could be a big problem.
In this example we have a "Customer" class and one method to add new customers into our database.
The problem is we are loading the class with more responsibility by logging the errors in the catch section. If we keep this programming methodology maybe in the future we are facing huge monstrous classes impossible to maintain like this swiss knife:
This class is built just to make customer validations, but if we move that logging functionality into another class, built just for that kind of process then we will get the single responsibility principle.
In this example we have a "Customer" class and one method to add new customers into our database.
class Customer { public void Add() { try { // Database code goes here } catch (Exception ex) { System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString()); } } }
The problem is we are loading the class with more responsibility by logging the errors in the catch section. If we keep this programming methodology maybe in the future we are facing huge monstrous classes impossible to maintain like this swiss knife:
This class is built just to make customer validations, but if we move that logging functionality into another class, built just for that kind of process then we will get the single responsibility principle.
O: Open/Closed
This principle was created by Bertrand Meyer and is about creation of extensible classes. It tries to avoid go into the class to modify the code, the idea is to provide open code the developer can extend but closed making those changes. To get this principle it needs to be clear where the application could be extended. The most common way to use this principle is by inheritance but also with an interface we can extend some methods without changing the internal code. At some point, we can face requirements that we are not able to cover by this method, in that case it's recommended to re-factorize our projects.
Let's see an example to get a clear understanding of this principle. Continue with the customer class example we want to calculate the discount according with the type of customer we have (Silver or gold customers).
If you take a look at the code above and see inside of the getDiscount method. We are just handling two kind of customers, but what happen when appears a new kind of customer? Then, we will need to change the customer class, but not just this, we will need to check the functionality of the previous code every time we change this class. Why not, instead of modify, extend the class? Check this out:
With inheritance, we are able to create a shared class with a getDiscount method that we can implement in every customer we want to create (SilverCustomer or GoldCustomer by now) and this way we will be able to create new Customers easily and maintainable. Putting in simple words the “Customer” class is now closed for any new modification but it’s open for extensions when new customer types are added to the project.
L: Liskov substitution
Let's see an example to get a clear understanding of this principle. Continue with the customer class example we want to calculate the discount according with the type of customer we have (Silver or gold customers).
class Customer { private int _CustType; public int CustType { get { return _CustType; } set { _CustType = value; } } public double getDiscount(double TotalSales) { if (_CustType == 1) { return TotalSales - 100; } else { return TotalSales - 50; } } }
If you take a look at the code above and see inside of the getDiscount method. We are just handling two kind of customers, but what happen when appears a new kind of customer? Then, we will need to change the customer class, but not just this, we will need to check the functionality of the previous code every time we change this class. Why not, instead of modify, extend the class? Check this out:
class Customer { public virtual double getDiscount(double TotalSales) { return TotalSales; } } class SilverCustomer : Customer { public override double getDiscount(double TotalSales) { return base.getDiscount(TotalSales) - 50; } } class goldCustomer : SilverCustomer { public override double getDiscount(double TotalSales) { return base.getDiscount(TotalSales) - 100; } }
With inheritance, we are able to create a shared class with a getDiscount method that we can implement in every customer we want to create (SilverCustomer or GoldCustomer by now) and this way we will be able to create new Customers easily and maintainable. Putting in simple words the “Customer” class is now closed for any new modification but it’s open for extensions when new customer types are added to the project.
L: Liskov substitution
Created by Barbara Liskov, it tries to create all of our classes derived to be able to use them like the base class. When we are creating derived classes is important not to overwrite methods from the base class making this base methods useless.
Let's see an example using our Customer class. If we would like to calculate discounts for enquiries we would use our Customer interface to implement the getDiscount method but we are not going to save this in the database because our Enquiry class is not a Customer.
class Enquiry : Customer { public override double getDiscount(double TotalSales) { return base.getDiscount(TotalSales) - 5; } public override void Add() { throw new Exception("Not allowed"); } }
We are overriding the interface method to provide a discount and we will throw an exception to avoid save this Enquiries in the database. Meanwhile, another developer could use our interface to loop over several inherited classes using the Customer class by polymorphism: SilverCustomer, GoldCustomer and Enquiry.
List< customer> Customers = new List< customer>(); Customers.Add(new SilverCustomer()); Customers.Add(new goldCustomer()); Customers.Add(new Enquiry()); foreach (Customer o in Customers) { o.Add(); } }
This will produce a runtime exception whe the foreach loop gets the Enquiry object. The point is, the Enquiry class implements a getDiscount method, but in reality it is not a real customer so the parent cannot replace the child object seamlessly. Customer is not the real father of Enquiry, and Liskov says the parent should easily replace the children. So we need to create two more interfaces, one is for Discount and other for the database. With those interfaces the Enquiry class just need to implement the IDiscount and Customer class will implement IDiscount and IDatabase. With this implementation we never face that error again, actually visual studio will provide us a compilation error. See line 4 below
List< IDatabase> Customers = new List< IDatabase>(); Customers.Add(new SilverCustomer()); Customers.Add(new goldCustomer()); Customers.Add(new Enquiry()); foreach (IDatabase in Customers) { o.Add(); } }
I 'ISP': Interface segregation principle
Made up by Robert C. Martin is common with the first principle. When we are creating interfaces, they need to be associated with a particular functionality. Is better to create several interfaces with some abstract methods inside, instead of create on big interface which contains those methods. The main purpose is to reuse the interfaces in other classes. It's clear, if we have an interface which compares and clones, it will be harder to use it in other classes instead of have several interfaces for those two purposes.
An example of this principle again with our fancy Customer class. Imagin this class had a great success and is used by 1000 clients that are really happy using our Add method. And a few customers ask to get also a Read method. We can think about changing our interface by adding a new method for read data. In that case we will be disturbing all the happy clients that probably don't need to use this new method. So the best solution will be create a new interface called IDatabaseV1 rather than update the current one. This interface inherits from the original IDatabase and those customer who wants to use the read method just need to implement our new IDatabaseV1 class.
An example of this principle again with our fancy Customer class. Imagin this class had a great success and is used by 1000 clients that are really happy using our Add method. And a few customers ask to get also a Read method. We can think about changing our interface by adding a new method for read data. In that case we will be disturbing all the happy clients that probably don't need to use this new method. So the best solution will be create a new interface called IDatabaseV1 rather than update the current one. This interface inherits from the original IDatabase and those customer who wants to use the read method just need to implement our new IDatabaseV1 class.
interface IDatabaseV1 : IDatabase // Gets the Add method { Void Read(); } class CustomerwithRead : IDatabase, IDatabaseV1 { public void Add() { Customer obj = new Customer(); Obj.Add(); } public void Read() { // Implements logic for read } }
D: Dependency inversion
Defined by Robert C. Martin. The objective is just to decouple the classes. A system low coupled has no special problems but with high coupling it will be really hard to maintain. The idea is use abstractions to communicate two classes but without meeting one of the other. It's not needed to know the "details" of the other class. There are several patterns like Dependency injection or service locator to allow us to invert the control.
For our last example, if you remember we created a Logger class to commit our first principle (SRP).
Just to control things we create a common interface and using this common interface new logger classes have been created.
Below are three logger classes and more can be added down the line.
This is breaking the SRP, Customer class should be focused on related funtions. We need to INVERT / DELEGATE this responsability to someone. Let's modify the code to get an INVERSION of dependency. To achieve this, we will delegate the responsability into the client who is consuming the customer object allowing him to INJECT the logger that he wants to use.
class Customer { private FileLogger obj = new FileLogger(); public virtual void Add() { try { // Database code goes here } catch (Exception ex) { obj.Handle(ex.ToString()); } } }
Just to control things we create a common interface and using this common interface new logger classes have been created.
interface ILogger { void Handle(string error); }
Below are three logger classes and more can be added down the line.
class FileLogger : ILogger { public void Handle(string error) { System.IO.File.WriteAllText(@"c:\Error.txt", error); } } class EverViewerLogger : ILogger { public void Handle(string error) { // log errors to event viewer } } class EmailLogger : ILogger { public void Handle(string error) { // send errors in email } }
This is breaking the SRP, Customer class should be focused on related funtions. We need to INVERT / DELEGATE this responsability to someone. Let's modify the code to get an INVERSION of dependency. To achieve this, we will delegate the responsability into the client who is consuming the customer object allowing him to INJECT the logger that he wants to use.
class Customer : IDiscount, IDatabase { private Ilogger obj; public Customer(ILogger i) { obj = i; } }
IDatabase i = new Customer(new EmailLogger());
0 comments:
Post a Comment