DEV Community

Albert Bennett
Albert Bennett

Posted on • Edited on

SOLID Principles for OOP

If you learned something new feel free to connect with me on linkedin or follow me on dev.to :)


I feel like this is a topic that I don't see many people talking about so much anymore, so this is my explanation of it.
SOLID is an acronym for 5 core principles. These are a set of principles designed to make your code more maintainable and robust when, coding in an object orientated language. They stand for:

  • Single Responsibility
  • Open/ Close
  • Liskov Substitution
  • Interface Segregation
  • Dependency Inversion (injection)

Brace yourself! There is a lot to go through...



Single Responsibility:

Essentially your method/ class should have one thing to do and do it well. For example, this is bad coding:

image

It's bad because, it doesn't make much sense to be writing an arbitrary piece of text to the console inside of a method called "Count". It's unexpected and unnecessary behavior. This on the other hand, is good. It's good because, each method now has a specific job.

image

Having an object only do one thing makes the code much easier to read and understand. Also, if it needs to be extended this can be done much more quickly.



Open/ Close:

Simply put, software entities (methods, classes, etc) should be open to extension and closed to modification. Hmmmm... time for another example, I think! In the code below we are trying to give the method too much functionality. In doing so we are modifying the expected behavior of the method. As you can see to modify something like this could become very trouble some.

image

Now consider this. We split up the class, add an interface and have each object specify how to handle the processing of an order. Now, we have something that is far more open to extension.

image

As you can see by adding a layer of inheritance we have not changed the underlying behavior of our interface and yet we have opened it up for extension. Now to follow the above example you'd need to add an if statement in the calling class for both object and switch between them. But you get the point, by abstracting the functionality out we have made the code much more flexible, including derived classes being able to implement the function in the interface in whichever way that they want to.



Liskov Substitution:

Basically, what this principle means is that a software entity should be replaceable with a sub-type without creating any new errors. Sub-type in this context means something that inherits from something else. Such as Order to IOrder. I feel another example coming up:

image

In the above sample we are violating the rule because, we should be able to use the BigOrder object in the same way as the Order object and well... right now we can't. Both objects are concreate implementations, of separate objects ie, they don't have a common sub-type. This can be problematic in the real world, especially when testing components of your software. A better way of doing the exact same thing is either, create a new method to take in the BigOrder object (this is bad don't do it) or require an interface instead.

image

In the above sample you can clearly see that the two implementations of IOrder (both BigOrder and Order inherit form IOrder) can be substituted with one another without effecting the functionality of the program.



Interface Segregation:

What this means is don't tack on new methods to an interface if they are unnecessary. Example, this is bad:

image

Now clearly a car shouldn't have any need for a fly function at least not in 2021 maybe 2030 though... Likewise a plane shouldn't be able to drive. As such we have introduced redundant functionality to both objects. Which violates this principle. We can fix this by introducing new interfaces to better manage the new functionality.

image

Simple when you see it in action.



Dependency Inversion (injection):

Finally, the great DI (Dependency Inversion (Injection)). This can be a massive topic in itself. And there are many ways of achieving this. The way I like to think of it is don't use the "new" keyword. except or at the composition route of your application. This principle states that:

  1. High/ low level modules should depend on abstractions
  2. Abstractions should not depend on details

Essentially don't depend on concrete implementations in your code. A concrete implementation is an instance or an object that was created using the 'new' keyword. If you have read down as far as here. First of well done, secondly you would have seen DI being used in some of the samples. From the sample below you can see the rule being violated because, in BuildHouse method we are using the new keyword to create an instance of the BrickLayer object. This added dependency reduces the test-ability of our method.

image

Now first off what we should do is to inject the BrickLayer into the BuildHouse method. However we can go one step further by abstracting the BrickLayer.

image

Great we did it! Now for the proof.

image

Super. Now if we needed to put in a dummy IConstructionWorker object into the method to test it. We can and without any issues.


I hope my explanation helps with your understanding of SOLID principles. See you next time.

Top comments (16)

Collapse
 
promise_sheggsmann profile image
Promise Sheggsmann

very cool and informative, I am starting out my career as a software engineer and I admire people who write code like this, but I don't seem to be able to design a program as efficient, extensible and following the principles, can anyone please suggest to me how to learn these skills or at least practice them.

Collapse
 
albertbennett profile image
Albert Bennett

I'd second looking at sources surrounding the gang of four as well. In my opinion viewing code samples is always your best bet when learning about code. On top of that it would be worth it to read more into common software design patterns. It helped me out a lot when I first started out. However, if you'd like to learn more about the other aspects of software development I'd suggest reading 'The Pragmatic Programmer' by David Thomas and Andrew Hunt. It's a great book, and one that I find myself going back to every few years.

Collapse
 
promise_sheggsmann profile image
Promise Sheggsmann

Thanks for your advice Albert, really appreciate it.

Collapse
 
tsalman1980 profile image
taha salman

I would suggest look at gang of four, open source code, other frameworks and…
write code, take an existing implementation and try to do it again. there is no replacement for hands on.

Collapse
 
promise_sheggsmann profile image
Promise Sheggsmann

Thanks soo much, I would check out the book and practice.

Collapse
 
mdjorov profile image
Miro

I do not agree about the example for Liskov Substitution principle. Order and LargeOrder do not inherit IOrder, they implement it. Big difference. For me a right example is something like:
public class Parent {
void DoSomeWork() {};
}
public class Child : Parent {
void DoSomeOtherWork(){};
}

public void Main()
{
Parent p = new Parent();
Child c = new Child();

SomeFunction(p);
SomeFunction(c);
}

private void SomeFunction(Parent p)
{
p.DOSomeWork();
}
This way no matter what child class of the Parent class is used, the SomeFunction will always work as intended and won't know that the parameter is not a Parent class object, but its child.

Collapse
 
bpkinez profile image
Branislav Petrović

Thanks for this correction! Just what I spotted and I totally agree with your explanation of LSP.

Collapse
 
junipa profile image
Junipa

Hello. I am not sure to understand why you wrote "Now to follow the above example you'd need to add an if statement in the calling class for both object and switch between them" in the Open/Close Principle section: since both objects implement the IOrder interface, from my understanding, one could call the MakeOrder() method regardless of the concrete type of the object. Am I missing something ?

Collapse
 
mdjorov profile image
Miro

Yes, for me something is not right either. The if statement should be in a Factory class, that knows what object to return. Something like:
We have somewhere a class factory
IOrder OrderFactory(int size)
{
if (size < 100)
return new Order;

return new LargeOrder();
}

and a class using it:
class Foo ()
{
void Bar(int size)
{
IOrder order = OrderFactopy(size);
order.MakeOrder();
}
}
This way the Foo class won't change if somewhere in the future an ExtraLargeOrder class appear. The only place that will change is the OrderFactory class, but all other code won't.

Collapse
 
ginogrecor profile image
gino greco

Excellent, thank you for sharing your knowledge.

Collapse
 
hanokhaloni profile image
Hanokh Aloni

These are some nice and fresh examples right there! :)

Collapse
 
kapilpatel profile image
Kapil Patel

Wow this is super easy

Collapse
 
evrtrabajo profile image
Emmanuel Valverde Ramos

dev.to/evrtrabajo/solid-in-php-d8e

I've wrote one in PHP

Collapse
 
nwaokoron profile image
Nzechi Nwaokoro

For DI you mentioned adding the new keyword reduces testability, can you elaborate a little bit more on this? Maybe an Example?

Collapse
 
zankyr profile image
zankyr

Let's say that you want to test that, by calling BrickLayer.ConstructHouse(), all the exceptions are handled correctly. Using DI allows you to inject a mocked object, configured to throw an exception when required:

// given
BrickLayer mockedBrickLayer = mock(BrickLayer.class);
when(mockedBrickLayer.ConstructHouse()).then(throw new Exception());

// then
assertThrow(new HouseBuilder().BuildHouse(mockedBrickLayer));
Enter fullscreen mode Exit fullscreen mode

In this test case you don't care how or why the exception is thrown (incorrect or missing data etc), you just care about how you're handling that exception. DI allows you to build tests without worrying about the logic within the dependencies (in our case, the BrickLayer).

Another example: what if the BrickLayer.ConstructHouse() bases its logic on external data? For example, data from a database or external API.

public class BrickLayer {

    public void ConstructHouse() {
        // retrieve data from the external
        var externalData = callTheExternalService();

        // do your magic
        if (extarnalData.something()) {
            Console.Writing(...);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Without a DI, in your tests you are strictly bound to the response provided by the external system. So, you really have to call the system, which means moving to integration or end-to-end testing.
Using the DI, you don't need a working external system, but you can easily simulate its behavior:

// given
BrickLayer mockedBrickLayer = mock(BrickLayer.class);
when(mockedBrickLayer.ConstructHouse()).then(Console.writing(...));

// when
new HouseBuilder().BuildHouse(mockedBrickLayer));

// then
assertEquals("The expected message", Console.ReadKey());
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ninacoelhodr profile image
Janaina Coelho

I realy like this article medium.com/backticks-tildes/the-s-...