Advanced Object-Oriented Programming (OOP)
Core concepts and SOLID Principles
To achieve a successful OOP, we should comply with SOLID principles.
I assume you are here because you want to improve your OOP skills, prepare for an interview, or just stumbled on this article. So, before I get too technical and bore you with buzzwords, I should confess that I also struggled with the OOP and SOLID principles, which was a grind at first.
“Just keep swimming, just keep swimming” — Finding Nemo.
In this article, we cover the following subjects:
I) OOP Core Concepts.
II) SOLID principles.
Everything clicked when I paid attention to how to write more maintainable code. I didn't write based on a particular OOP pillar or SOLID principle. Instead, I focused on implementing the most appropriate coding technic and best practices first and improving upon them later.
“Practice makes perfect” — Vince Lombardi.
What Is the Problem?
We all have designed some Classes, used Inheritance, proudly employed Encapsulation, taken advantage of Polymorphism, and even utilized Abstraction. We also grasped that our code isn’t perfect in the first iteration and requires change or refactoring later. The problem is that sometimes, ‘later’ is too late, and we are so far from ‘practical’ that “making it better” is a project of its own. Most of us have successfully deployed our code to production, but something somehow somewhere reminds us that our job isn’t over just because the code is deployed. New Requirements, Bugs, or Changes will come, and when they come, they reveal our shortcomings or short-sightedness. We even have a default reaction when that happens.
“How could I possibly have known about it? There was no such discussion before, there was no time…,” and my favorite, “To finish the job, we must redesign the code and spend more time than anticipated.”
Can we be more prepared when that happens? Can’t we extend our code and not change it?
I - What is Object-oriented Programming (OOP)
OOP is a programming model that arranges objects rather than functions. Functional programming is about writing methods that perform operations on the data, while OOP is about assembling objects that contain both data and methods. A class is a template for objects, and an object is an instance of a class.
For example, Class: Fruit | Object: Banana, Grape, Orange
Pillars of OOP
There are four basic principles of object-oriented programming. They are:
1- Abstraction: Modeling the relationships, attributes, and interactions amongst entities as classes to create an abstraction of a system. If correctly applied, abstraction can isolate the changes made to the code so that if something goes wrong, the change will only impact the implementation details of a class and not the outside calling code. As a rule of thumb, it’s preferred to separate the interface of a class from its implementation.
2- Encapsulation: Hiding the internal state of an object and allowing interaction with them via public properties or functions. Encapsulation means enclosing something. It forms a protective layer around the information within an object from the rest of the code. When an object prevents code from accessing its private data directly, it is well encapsulated.
An example can be making the social security number of a Person a private member of the class. By encapsulating this member as a private variable, the outside code can’t access it directly and will remain safe within that Person’s object. A method inside the class can access this private member if necessary.
3- Polymorphism: Ability to implement properties or methods in the derived objects differently from their parent abstractions. Polymorphism allows for the unvarying treatment of classes in a hierarchy. As mentioned earlier, derived objects share the same interface as their parents. Using Polymorphism and at run-time, we can expect appropriate behavior from the same method in the derived classes vs. the parent classes.
One more valuable point about Polymorphism is that you can replace Conditionals with Polymorphism! Your code will correspond to the right section of the conditional using Polymorphism. For example, if you write an If-statement to perform different actions based on an Object Type, you can instead write subclasses that match the “If” or the “else” sections.
4- Inheritance: Ability to create derived abstractions based on existing parent abstractions. Inheritance acts like hierarchies or trees where a class might have one or more child classes. If a class inherits from another parent class, we say it is derived from the parent class, and it represents an “IS-A” relationship. ‘Inheritance’ leads to good code reuse as parent functions don’t need to be redefined in child classes. For example, ‘Espresso’ is a ‘drink.’ The parent class is the ‘drink,’ and ‘Espresso’ is the child/derived class; therefore, ‘Espresso’ obtains much of the same functionality and properties of a ‘drink.’ It can also extend new functionalities of its own as well.
Consider the following: We have an Animal parent/base class and two derived/child classes called Cat and Dog. If the Animal class has a “MakeSomeNoise” method, we can override the function so that the same method makes “meow” for Cats but “barks” for Dogs.
Inheritance vs. Composition
“Program to interfaces, not implementations” — Gang of Four
In the previous section, I said that Inheritance leads to code reuse. I need to give more information about it. Although Inheritance is a fundamental principle for OOP, there’s something to be said about using “Composition.” Inheritance offers a way to reuse code by extending the class, but if you need to use an object as a field within another class, you should use Composition.
The main difference between Inheritance and Composition is the relationship between objects. As you saw, Inheritance uses the “IS-A.” relationship, but Composition uses the “Has-A” or “Uses A” relationship. A Car is a Vehicle, but a Car has /uses a steering capability.
Classes and objects in inheritance code are tightly coupled, which is sometimes exactly what you need. But with Composition, Classes and objects are loosely coupled, meaning you can switch up these components more quickly. Sometimes Inheritance can break Encapsulation; to solve that, you should use Composition. Composition lets you create complex types by combining “Behaviors.”
Composition is my preferred method in cases where one object “has” or is “part of another object.” For example, a house has a bedroom (a bedroom is part of a house). My personal preference has dictated me to conclude that if I am not sure which one to use, I go with Composition over Inheritance initially unless I need to switch, which has been rarer than you think.
Here is another example of providing a problem starting with Inheritance: Birds fly and eat. An Eagle is a “bird.” therefore, they ‘must’ fly and eat. Inheritance!
Later, a new requirement comes along, and a new bird is introduced, an Ostrich. Ostriches can’t fly!
What are you going to do? “Composition” fits better in this case. Birds have different behaviors as Base classes, such as fly behaviors (iFly Interface). Eagles Inherit common behaviors from birds, but they also implement their specific Interfaces. Do you think Ostriches can implement a “Not-flying” behavior as the solution? I will explain the validity of such claims when we discuss Design Patterns.
OOP advantages
Here are some of the advantages of OOP over functional or procedural programming:
- Faster and easier to execute
- Clarity of design and structure
- Provides better code reusability, which shortens development time
- Adheres to the DRY principle (Don’t Repeat Yourself), which makes maintenance easier
II — SOLID Principles
It’s time to get a little more practical and tell you how to comply with the pillars of OOP in our code using SOLID principles. SOLID is an acronym for five design principles intended to make OOP more intelligible.
- SOLID reduces coupling so that changes in one part of your application have the most negligible impact on the other parts of your application.
- SOLID makes it easier to understand, maintain, and extend your code.
- The Single Responsibility Principle (SRP): A class should have only one reason to change.
- The Open–Closed Principle (OCP): Software should be open for extension but closed for modification.
- The Liskov Substitution Principle (LSP): Child class should be perfectly substitutable for their parent class.
- The Interface Segregation Principle (ISP): Clients should not be forced to implement methods they don’t use. Contracts should be broken down into thin ones.
- The Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Let’s talk a little more technical about each one of them
Single Responsibility Principle — SRP:
Following SRP means trying to accomplish one behavior or responsibility in a single class. By doing so, it will be straightforward to:
- Verify if it's working as planned or not.
- Change code since the risk of unintended breaks is minimized.
- Explain, implement or understand.
- Follow OOP’s Encapsulation. Hiding data from the user would be easier.
You can still accomplish more extensive and complex work by letting smaller classes work together.
Consider this example: You want to shop online but don’t have an account. You click on the “Create Account” button, provide your email, password, and some additional information, and a few seconds later, you have an account. You also receive an email from the site welcoming you to their system and asking you to verify your email.
How did their developer do that? Let’s assume they did something like the code below:
You may say the above code is not following SRP, and you will be correct. That code performs several tasks: creating a user, establishing a database connection, creating the email client, and sending the welcome email!
One suggestion will be to separate the responsibilities and create smaller classes where each takes care of a single task. Take a look at this.
Isn’t the above code better? It creates two smaller classes, UserManagement and EmailManagement, where each is responsible for their specialized tasks.
Open-Closed Principle — OCP:
If you follow the OCP successfully, you will be one of the exquisite group of people who can claim that they can “extend” functionalities in their application without “modifying the existing” implementation. Sound too good to be true? It is possible.
If we consider that the application is written to carry out specific actions or functionalities, then we can design our classes to “behave” in particular manners by implementing specific “interfaces.” Check out this sample pseudo code below:
It is easy to spot the problem. The above code is not following the OCP principle. Every time any new employment type is added, you have to change this code. Every time the conditions for each type change, the code must change too. If only we could replace the conditionals with Polymorphism and code to interfaces, not implementations. Let’s try to change the code by improving it just a bit:
The above code is better than the original. The easiest way to describe it is that it kicks the problem down the road. You can easily extend the code in the above example if a new employment type is added. Also, if the tax rules of a particular employment type change, you can limit code modification to that exact type.
Broadening this principle allows you to decouple the calculations from other Employee behaviors, and consequently, a change in the payment calculation will not affect the Employee class.
OCP is one of those principles that allow code-maintainability. Following it is consequential in the success of that application and future-proofing it.
Liskov Substitution Principle — LSP:
This is the official definition: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. LSP requires Polymorphism to create class-level behavior for the same methods. The following image provides the best example of not complying with LSP:
I will provide two different examples to help you learn about LSP. In the first example, I use Inheritance, and in the second one, I use Subtyping.
Example 1) Using Inheritance:
Example 2) Using Subtyping:
Piggybacking off of the previous example, the following sample is not complying with LSP.
Ostrich is a bird, but it can’t fly. The Ostrich class can’t implement Fly behavior even though it is a bird.
Of course, based on the two examples, you can easily deduce that you can also use Interfaces to create another LSP-Complying example. Just think of Flying() as a behavior; therefore, you can make an iFly interface &…
Interface Segregation Principle — ISP:
You shouldn’t be forced to implement an interface when your object doesn’t share that purpose. In other words, Fat is Bad! Classes with too many contracts should be broken down into smaller classes or interfaces.
Consider the example below:
In this example, we do not comply with ISP because we are forcing a class (ShoppingOffline) to implement a method (ProcessCreditCard) it doesn’t need. To fix the issue, we break the general interface into two thinner interfaces. AddToCart stays in the common interface, and ProcessCreditCard moves to another interface.
As you may have guessed by now, the Single Responsibility Principle and the Interface Segregation Principle are very much related.
Dependency Inversion Principle —DIP:
DIP expects us to depend on abstractions instead of concrete types. A prominent place this principle comes in handy is when we use Services, and usually, services depend on other services. In those cases, wherever you need to access those services, you access them via the interfaces.
By now, you may have asked yourself this question: Isn’t he talking about Dependency Injection (DI)? DI is a technique to implement the DIP. To achieve the DIP, you can either use abstract classes or interfaces. One advantage of using DIP is that by having a more decoupled code, you will spend less time in the future changing it when new requirements come along. Consider this hypothetical example; you are working on an application that needs a database, and your manager has chosen SQL Server. Will you write your data access layer based on the SQL Server, or will you write it so you can switch the database engine from SQL Server to something else later on if needed? If you write it that way, you can Add the new data access layer and ‘pass’ it along based on the ‘type’ you need.
As mentioned earlier, one of the ways to perform the DIP is to use Dependency Injection. You can pass your dependencies via the class constructor. If you pass the dependency down the chain to the derived class, you will not need to instantiate / “NEW up” the dependency in the child/derived class, and instead, you send the proper dependency at the right time. Just remember the famous mantra, New is Glue!
Conclusion
If there is one sentence to summarize the benefits of following the SOLID principles based on what we have learned so far is that “SOLID enables loose coupling in your code, which results in having less rigid, easier to maintain, and less fragile code.”
I hope you enjoyed reading this article and looking forward to seeing you in my next article, where I discuss more advanced topics:
III) we will connect the OOP Design Patterns
IV) we go over Refactoring and Code smells