15 posts
The Object-Oriented Design Principles are the core of OOP programming.They will help you to create a clean and modular design, which would be easy to test, debug, and maintain.
Our first object-oriented design principle is DRY, as the name suggests DRY (don't repeat yourself) means don't write duplicate code, instead use Abstraction to abstract common things in one place. If you have a block of code in more than two places consider making it a separate method, or if you use a hard-coded value more than one time make them public final constant.
The benefit of this Object oriented design principle is in maintenance. It's important not to abuse it, duplication is not for code, but for functionality. It means if you used common code to validate OrderID and SSN it doesn’t mean they are the same or they will remain the same in future.
By using common code for two different functionality or thing you closely couple them forever and when your OrderId changes its format, your SSN validation code will break. So beware of such coupling and don’t combine anything which uses similar code.
Don’t repeat yourself design principle is about abstracting out common code and putting it in a single location.
Consider a Mechanic class which services cars and bikes.
package com.dry;
public class Mechanic {
public void serviceCar() {
System.out.println("servicing car now");
}
public void serviceBike() {
System.out.println("servicing bike now");
}
}
Here, we have two methods serviceCar() and serviceBike(). The mechanic services bikes according to their own method. Nothing strange about it.
Now consider the workshop is offering you other services, the mechanic will wash your bike and then service, he is also offering to polish vehicles in the service itself.
Now we have to update the code for serviceCar() and serviceBike() also.
public void serviceCar() {
// washing vehicle here
System.out.println("servicing car now");
// polishing vehicle here
}
public void serviceBike() {
// washing vehicle here
System.out.println("servicing bike now");
// polishing vehicle here
}
Now what is the problem here? Whenever some procedure changes, methods serviceCar and serviceBike also change. There is code duplication, one piece of code is repeating for same purpose. Here comes the application of don’t repeat yourself principle. It states to abstract out the code that is being duplicated. So we can write a separate method that performs the tasks which mechanic offers other than servicing.
package com.dry;
public class Mechanic {
public void serviceCar() {
System.out.println("servicing car now");
performOtherTasks();
}
public void serviceBike() {
System.out.println("servicing bike now");
performOtherTasks();
}
public void performOtherTasks() {
// do washing here
// or do something else
System.out.println("performing tasks other than servicing");
// do whatever you want to do in the servicing package
}
}
Now we have created a method performOtherTasks() by applying don’t repeat yourself principle. serviceCar() and serviceBike() simply call it. Now whatever changes the workshop offers in service package, they can be included in the same method. That code need not replicate in serviceCar() and serviceBike(). Thus it makes code more cohesive and maintainable.
Single Responsibility Principle is another SOLID design principle, and represent "S" on the SOLID acronym. As per SRP, there should not be more than one reason for a class to change, or a class should always handle single functionality.
If you put more than one functionality in one Class in Java, it introduces coupling between the functionalities and even if you are trying to change one functionality, there is chance of you breaking the coupled functionality, which requires another round of testing to avoid any surprise in production environment.
public class TicketReservation
{
public void createTicketReservation(ReservationDetails reservation){
// Create a reservation
new OnlineReservationDAO.createReservation(reservation);
}
}
This class has clearly just one responsibility and it doesn’t violate the SRP as its only responsibility is to call a data access object to create a ticket reservation. Since it only has one responsibility, it also has only one reason to change.
If we add some code to the same class so that the user receives an email once the reservation is saved, it would look like this :
public class TicketReservation
{
public void createTicketReservation(ReservationDetails reservation){
// Create a reservation
new OnlineReservationDAO.createReservation(reservation);
}
public void sendEmailToCustomer(){
// Code to send email
}
}
This looks simple, but if we just introduce a new reason for this class to change.Every time we need to change how the class does the Ticket reservation or how it sends email, we’ll need to change it.We don’t want a class to be impacted by these two completely different reasons. The new code above violates the SRP.
So let’s see how to avoid the SRP violation:
One way to satisfy the SRP in the example above will be to create a new class with the code that takes care of emailing reservation details.Code that generates emails should not be part of this TicketReservation class.
In short, Open Closed Principle tells us “You should not modify existing behavior but if you want, you can extend it according to your needs.” i.e. Code is open for extension but closed for modification.
Consider we have a Vehicle class and a Mechanic class. A mechanic can service all vehicles. You are the owner of Vehicle class and your code does vehicle servicing well. The mechanic simply calls service method in Vehicle class to get it done. Here is how a mechanic services a vehicle.
Vehicle class :
package com.openclosed;
public class Vehicle {
public void service() {
System.out.println("General procedures being done on your vehicle");
}
}
Mechanic class :
package com.openclosed;
public class Mechanic {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.service();
}
}
Nothing fancy about it, a simple call to method on object. Now there is a customer who owns a specific bike of which servicing needs more attention and more tasks than a general vehicle servicing. The mechanic wants to service the bike and he asks you to change code in service() method to make it bike specific.
Now you have a problem, you cannot change service method in Vehicle class to make it bike specific.So here comes the open closed principle in action. You can ask the mechanic to extend existing Vehicle class and override service method to write bike specific code in it. In this manner Vehicle class code is not modified and mechanic creates Bike class extending Vehicle to add bike servicing specific code.
Bike class :
package com.openclosed;
public class Bike extends Vehicle {
@Override
public void service() {
System.out.println("Now doing bike specific servicing");
}
}
Mechanic class :
package com.openclosed;
public class Mechanic {
public static void main(String[] args) {
Bike cbr = new Bike();
cbr.service();
}
}
Now the problem is solved, we did not have to modify Vehicle class and mechanic can extend Vehicle class functionality by inheriting it.
According to the Liskov Substitution Principle, Subtypes must be substitutable for supertype i.e. methods or functions which uses superclass type must be able to work with the object of subclass without any issue.
LSP is closely related to the Single responsibility principle and Interface Segregation Principle. If a class has more functionality than subclass might not support some of the functionality and does violate LSP.
Example :
The LSP is popularly explained using the square and rectangle example. Let’s assume we try to establish an ISA relationship between Square and Rectangle. Thus, we call “Square is a Rectangle.”
Rectangle class :
public class Rectangle {
private int length;
private int breadth;
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getBreadth() {
return breadth;
}
public void setBreadth(int breadth) {
this.breadth = breadth;
}
public int getArea() {
return this.length * this.breadth;
}
}
Square class :
// Represents ISA relationship - Square is a Rectangle
public class Square extends Rectangle {
@Override
public void setBreadth(int breadth) {
super.setBreadth(breadth);
super.setLength(breadth);
}
@Override
public void setLength(int length) {
super.setLength(length);
super.setBreadth(length);
}
}
As per the principle, the functions that use references to the base classes must be able to use objects of derived class without knowing it. Thus, in the example shown below, the function calculateArea, which uses the reference of “Rectangle,” should be able to use the objects of derived class, such as Square, and fulfill the requirement posed by Rectangle definition.
Look at the calculateArea method in the code below. One should note that, as per the definition of Rectangle, the following must always hold true given the data below:
In this case, we try to establish an ISA relationship between Square and Rectangle such that calling “Square is a Rectangle” in the below code would start behaving unexpectedly if an instance of Square is passed. An assertion error will be thrown in the case of checking for "Area" and checking for "Breadth," although the program will terminate as the assertion error is thrown due to the failure of the Area check.
LSP class :
/**
* The class demonstrates the Liskov Substitution Principle (LSP)
*
* As per the principle, the functions that use references to the base classes must be able to use objects of derived class without knowing it.
* Thus, in the example shown below, the function calculateArea which uses the reference of "Rectangle" should be able to use the objects of
* derived class such as Square and fulfill the requirement posed by Rectangle definition.
*/
public class LSPDemo {
/**
* In case, we try to establish ISA relationship between Square and Rectangle such that we call "Square is a Rectangle",
* below code would start behaving unexpectedly if an instance of Square is passed
* Assertion error will be thrown in case of check for area and check for breadth, although the program will terminate as
* the assertion error is thrown due to failure of Area check.
*
* @param r Instance of Rectangle
*/
public void calculateArea(Rectangle r) {
r.setBreadth(2);
r.setLength(3);
//
// Assert Area
//
// From the code, the expected behavior is that
// the area of the rectangle is equal to 6
//
assert r.getArea() == 6 : printError("area", r);
//
// Assert Length & Breadth
//
// From the code, the expected behavior is that
// the length should always be equal to 3 and
// the breadth should always be equal to 2
//
assert r.getLength() == 3 : printError("length", r);
assert r.getBreadth() == 2 : printError("breadth", r);
}
private String printError(String errorIdentifier, Rectangle r) {
return "Unexpected value of " + errorIdentifier + " for instance of " + r.getClass().getName();
}
public static void main(String[] args) {
LSPDemo lsp = new LSPDemo();
//
// An instance of Rectangle is passed
//
lsp.calculateArea(new Rectangle());
//
// An instance of Square is passed
//
lsp.calculateArea(new Square());
}
}
what is the problem with Square-Rectangle ISA relationship?
This principle states that:
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.
Now we will try with some code that violates that principle.
Say you are working as part of a software team. We need to implement a project. For now, the software team consists of:
A BackEnd Developer :
package com.dependencyinversion;
public class BackEndDeveloper {
public void writeJava() {
}
}
And a FrontEnd developer :
package com.dependencyinversion;
public class FrontEndDeveloper {
public void writeJavascript() {
}
}
And our project uses both throughout the development process :
package com.dependencyinversion;
public class Project {
private BackEndDeveloper backEndDeveloper = new BackEndDeveloper();
private FrontEndDeveloper frontEndDeveloper = new FrontEndDeveloper();
public void implement() {
backEndDeveloper.writeJava();
frontEndDeveloper.writeJavascript();
}
}
So as we can see, the Project class is a high-level module, and it depends on low-level modules such as BackEndDeveloper and FrontEndDeveloper. We are actually violating the first part of the dependency inversion principle.
Also, by inspecting the implement function of Project.class, we realize that the methods writeJava and writeJavascript are methods bound to the corresponding classes. Regarding the project scope, those are details since, in both cases, they are forms of development. Thus, the second part of the dependency inversion principle is violated.
In order to tackle this problem, we shall implement an interface called the Developer interface :
package com.dependencyinversion;
public interface Developer {
void develop();
}
Therefore, we introduce an abstraction.
The BackEndDeveloper should be refactored to:
package com.dependencyinversion;
public class BackEndDeveloper implements Developer {
@Override
public void develop() {
writeJava();
}
private void writeJava() {
}
}
And the FrontEndDeveloper shall be refactored to :
package com.dependencyinversion;
public class FrontEndDeveloper implements Developer {
@Override
public void develop() {
writeJavascript();
}
public void writeJavascript() {
}
}
The next step is, in order to tackle the violation of the first part, would be to refactor the Project class so that it will not depend on the FrontEndDeveloper and the BackendDeveloper classes.
package com.dependencyinversion;
import java.util.List;
public class Project {
private List<Developer> developers;
public Project(List<Developer> developers) {
this.developers = developers;
}
public void implement() {
developers.forEach(d->d.develop());
}
}
The outcome is that the Project class does not depend on lower level modules, but rather abstractions. Also, low-level modules and their details depend on abstractions.
6. Interface Segregation principle :
The interface segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
Imagine an interface with many methods in our codebase and that many of our classes implement this interface, although only some of its methods are implemented.
In our case, the Athlete interface is an interface with some actions of an athlete:
package com.interfacesegregation;
public interface Athlete {
void compete();
void swim();
void highJump();
void longJump();
}
We have added the method compete, but also there some extra methods like swim,
highJump
, and longJump
.
Suppose that John Doe is a swimming athlete. By implementing the Athlete interface, we have to implement methods like highJump
and longJump
, which JohnDoe will never use.
package com.interfacesegregation;
public class JohnDoe implements Athlete {
@Override
public void compete() {
System.out.println("John Doe started competing");
}
@Override
public void swim() {
System.out.println("John Doe started swimming");
}
@Override
public void highJump() {
}
@Override
public void longJump() {
}
}
The same problem will occur for another athlete who might be a field Athlete competing in the high jump and long jump.
We will follow the interface segregation principle and refactor the original interface :
package com.interfacesegregation;
public interface Athlete {
void compete();
}
Then we will create two other interfaces — one for Jumping athletes and one for Swimming athletes.
package com.interfacesegregation;
public interface SwimmingAthlete extends Athlete {
void swim();
}
package com.interfacesegregation;
public interface JumpingAthlete extends Athlete {
void highJump();
void longJump();
}
And therefore John Doe will not have to implement actions that he is not capable of performing :
package com.interfacesegragation;
public class JohnDoe implements SwimmingAthlete {
@Override
public void compete() {
System.out.println("John Doe started competing");
}
@Override
public void swim() {
System.out.println("John Doe started swimming");
}
}
Please log in to leave a comment.