➤ Design Pattern Interview Questions**
➤ Design Patterns in Java
➤ Types of Design Patterns
➤ Singleton Design Pattern
➤ Factory Pattern in Java
➤ Factory Method Design Pattern
➤ Abstract Factory Design Pattern
➤ Factory, Factory Method vs Abstract Factory
➤ Template Method Design Pattern
➤ Builder Design Pattern in Java
➤ Decorator Design Pattern in Java
➤ Adapter Design Pattern in Java
➤ Flyweight Design Pattern
➤ Strategy Design Pattern in Java
Design Pattern Interview Questions | Also see:- Design Patterns in Java, Types of Design Patterns.
Table of Contents
1. Categories of Java Design Patterns?
Java design patterns are categorized into 4 main types:-
- Creational Design Patterns:- These patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. These patterns define and describe how objects are created at class instantiation time.
- Structural Design Patterns:- These patterns deal with object composition or the structure of classes and objects. They help ensure that if one part of a system changes, the entire system doesn’t need to change.
- Behavioral Design Patterns:- These patterns are concerned with algorithms and assigning responsibilities between objects. They help in defining how objects interact and communicate with each other.
- J2EE Design Patterns:- J2EE (Java 2 Platform, Enterprise Edition) design patterns are specialized patterns used in enterprise-level Java applications. These patterns solve common problems encountered in enterprise application development, promoting best practices for scalability, maintainability, and efficiency.
2. Why do we need the design pattern?
- As design patterns are well-documented and understood by software architects, designers, and developers, their application within a specific solution will likewise be well-understood.
- Patterns give a software developer an array of tried and tested solutions to common problems, thus reducing the technical risk to the project by not having to employ a new and untested design, thus saving time and effort during the implementation stage of the software development lifecycle.
- They are language-neutral and so can be applied to any language that supports object orientation.
- By using well-understood and documented solutions, the final product will have a much higher degree of comprehension. If the solution is easier to comprehend, then by extension, it will also be easier to maintain.
Creational Design Patterns
3. What are the Creational Patterns?
- Creational design patterns are related to the way of creating objects.
- These patterns are used to define and describe how objects are created at class instantiation time.
Example:- Employee emp = new Employee();
Here we are creating the instance using the new keyword.
Creation Design Patterns are categorized as follows:-
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Singleton
Factory Method Pattern
- Factory Method Pattern abstracts the object creational process of the classes.
- In the Factory pattern, we don’t expose the creation logic to the client and refer to the created object using a standard interface.
- The factory pattern is also known as Virtual Constructor.
Steps:
- Create the main class which call the factory class.
- Factory class returns the required class instance.
In factory class:-
- The method’s return type should be a common type (interface).
- The method should be static.
public interface Profession {
void print();
}
public class Engineer implements Profession {
@Override
public void print() {
System.out.println("Engineer.print()");
}
}
public class Doctor implements Profession {
@Override
public void print() {
System.out.println("Doctor.print()");
}
}
public class Teacher implements Profession {
@Override
public void print() {
System.out.println("Teacher.print()");
}
}
public class ProfessionFactory {
public static Profession getProfession(String typeOfProfession) {
if (typeOfProfession == null || typeOfProfession.isBlank()) {
return null;
}
if (typeOfProfession.equalsIgnoreCase("teacher")) {
return new Teacher();
} else if (typeOfProfession.equalsIgnoreCase("doctor")) {
return new Doctor();
} else if (typeOfProfession.equalsIgnoreCase("engineer")) {
return new Engineer();
}
return null;
}
}
public class Test {
public static void main(String[] args) {
Profession teacher = ProfessionFactory.getProfession("teacher");
teacher.print(); // Teacher.print()
Profession doctor = ProfessionFactory.getProfession("doctor");
doctor.print(); // Doctor.print()
}
}
Abstract Factory Design Pattern
- Abstract Factory Design Pattern is also called a factory of factories.
- Abstract Factory lets a class return a factory of classes.
- So, this is the reason that the Abstract Factory Pattern is one level higher than the Factory Pattern.
- An Abstract Factory Pattern is also known as a Kit.
Steps:-
- Create the main class which calls the factory of the factory class.
- Factory of factory/factory Producer creates an instance of the factory class.
- Factory class returns the required class instance.

public interface Profession {
void print();
}
public class Engineer implements Profession {
@Override
public void print() {
System.out.println("Engineer.print()");
}
}
public class Teacher implements Profession {
@Override
public void print() {
System.out.println("Teacher.print()");
}
}
public class TraineeEngineer implements Profession {
@Override
public void print() {
System.out.println("TraineeEngineer.print()");
}
}
public class TraineeTeacher implements Profession {
@Override
public void print() {
System.out.println("TraineeTeacher.print()");
}
}
public abstract class AbstractFactory {
abstract Profession getProfession(String typeOfProfession);
}
public class TraineeProfessionAbstractFactory extends AbstractFactory {
@Override
public Profession getProfession(String typeOfProfession) {
if (typeOfProfession == null || typeOfProfession.isBlank()) {
return null;
}
return typeOfProfession.equalsIgnoreCase("Teacher") ?
new TraineeTeacher()
: typeOfProfession.equalsIgnoreCase("Engineer") ?
new TraineeEngineer() : null;
}
}
public class ProfessionAbstractFactory extends AbstractFactory {
@Override
public Profession getProfession(String typeOfProfession) {
if (typeOfProfession == null || typeOfProfession.isBlank()) {
return null;
}
return typeOfProfession.equalsIgnoreCase("Teacher") ? new Teacher()
: typeOfProfession.equalsIgnoreCase("Engineer") ? new Engineer() : null;
}
}
public class AbstractFactoryProducer {
public static AbstractFactory getProfessionFactory(boolean isTrainee) {
return isTrainee ? new TraineeProfessionAbstractFactory() :
new ProfessionAbstractFactory();
}
}
public class Test {
public static void main(String[] args) {
AbstractFactory traineeAbstractFactory =
AbstractFactoryProducer.getProfessionFactory(true);
Profession traineeTeacher = traineeAbstractFactory.getProfession("teacher");
traineeTeacher.print(); // TraineeTeacher.print()
Profession traineeEngineer = traineeAbstractFactory.getProfession("engineer");
traineeEngineer.print(); // TraineeEngineer.print()
AbstractFactory expAbstractFactory =
AbstractFactoryProducer.getProfessionFactory(false);
Profession teacher = expAbstractFactory.getProfession("teacher");
teacher.print(); // Teacher.print()
Profession engineer = expAbstractFactory.getProfession("engineer");
engineer.print(); // Engineer.print()
}
}
Singleton Design Pattern
The Singleton Design Pattern is one of the simplest design patterns in Java. This pattern involves a single class that is responsible for creating an object while making sure that only a single object gets created in one JVM.
This class provides a way to access its only object which can be accessed directly without the need to instantiate the object of the class.
Singleton class is a class whose only one instance can be created at any given time, in one JVM. Simple steps:-
- Create a private static variable.
- Create a private constructor so that an object can’t be created using the new keyword.
- Create a static method that returns the instance.
public class Singleton {
private static volatile Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
// double check
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
public class Test {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
System.out.println(singleton1.hashCode()); // 114935352
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton2.hashCode()); // 114935352
}
}
In the Singleton Design Pattern, we have discussed in detail why we have used volatile keywords, synchronization concept with block scope, double checking.
Prototype Design Pattern
A Prototype pattern refers to creating duplicate objects while keeping performance in mind. It involves implementing a prototype interface that tells to create a clone of the current object. This pattern is used when the creation of an object directly is costly. For example, it requires database calls or requires too much processing that will take a lot of memory.
What can be done? We can cache the object, and return its clone on the next request.
Steps:-
- Create the main class which calls CacheImpl Class.
- CacheImpl class has 2 methods: 1st to load the key value in the map and create the cache. 2nd to return the required cloned object.
- The main class, the parent of all required concrete classes contains the cloning technique. Rest concrete classes are normal POJOs, nothing special.
public abstract class Profession implements Cloneable {
public int id;
public String name;
abstract void print();
public Object getClone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException cnse) {
cnse.printStackTrace();
}
return clone;
}
}
public class Engineer extends Profession {
@Override
void print() {
System.out.println("Engineer.print()");
}
}
public class Doctor extends Profession {
@Override
void print() {
System.out.println("Doctor.print()");
}
}
import java.util.Hashtable;
public class ProfessionCache {
private static Hashtable<Integer, Profession> professionMap = new Hashtable<>();
public static Profession getCloneNewProfession(int id) {
Profession cachedProfessionInstance = professionMap.get(id);
return (Profession) cachedProfessionInstance.getClone();
}
public static void loadProfessionCache() {
Doctor doc = new Doctor();
doc.id = 1;
professionMap.put(doc.id, doc);
Engineer engineer = new Engineer();
engineer.id = 2;
professionMap.put(engineer.id, engineer);
}
}
public class Test {
public static void main(String[] args) {
ProfessionCache.loadProfessionCache();
Profession docProfession = ProfessionCache.getCloneNewProfession(1);
System.out.println(docProfession);
Profession engProfession = ProfessionCache.getCloneNewProfession(2);
System.out.println(engProfession);
Profession docProfession2 = ProfessionCache.getCloneNewProfession(1);
System.out.println(docProfession2);
}
}
Builder Design Pattern
Builder Pattern refers to the approach that focuses on constructing a complex object from simple objects using a step-by-step approach.
The major roles used in this design pattern are:-
- Complex Object / Final Product:- e.g. house – complex object which we will generate with builder design pattern
- Builder:- abstract class/interface that defines all ways to create the product. It also has the getFinalProduct method that will finally return the complex object.
- ConcreteBuilder:- multiple Builder Impls that will give different final objects which are complex to design,
- Director:- Controls complex object creation. It has 2 main goals: 1st to call the appropriate concrete builder class to create the correct complex object. 2nd to return that complex object.
Steps:-
1. Create complex class POJO (Plain Old Java Object) – Home.
public class Home {
public String floor;
public String walls;
public String terrace;
}
- Create a Builder interface/abstract class that has definitions.
interface Builder {
public void buildFloor();
public void buildWalls();
public void buildTerrace();
public Home getComplexHomeObject();
}
- Create a concrete builder class that has implementations.
public class EarthQuakeResistantBuilder implements Builder {
private Home earthquakeResistanceHome = new Home();
@Override
public void buildFloor() {
this.earthquakeResistanceHome.floor = "Wooden floor";
}
@Override
public void buildWalls() {
this.earthquakeResistanceHome.walls = "Wooden walls";
}
@Override
public void buildTerrace() {
this.earthquakeResistanceHome.terrace = "Wooden terrace";
}
@Override
public Home getComplexHomeObject() {
return this.earthquakeResistanceHome;
}
}
public class FloodResistantBuilder implements Builder {
private Home floodResistanceHome = new Home();
@Override
public void buildFloor() {
this.floodResistanceHome.floor = "10 Feets Above The Ground Level";
}
@Override
public void buildWalls() {
this.floodResistanceHome.walls = "Water Resistant walls";
}
@Override
public void buildTerrace() {
this.floodResistanceHome.terrace = "Water leakage resistant terrace";
}
@Override
public Home getComplexHomeObject() {
return this.floodResistanceHome;
}
}
- Create a Director who will have the responsibility to call the correct concrete builder and return the final object.
public class Director {
private Builder builder;
public Director(Builder builderType) {
this.builder = builderType;
}
public Home getComplexObjectOfHome() {
return this.builder.getComplexHomeObject();
}
public void manageRequiredHomeConstruction() {
this.builder.buildFloor();
this.builder.buildWalls();
this.builder.buildTerrace();
}
}
- Create a main class that will initialize Director, and call the method of a director finally which will in turn return the complex object instance required.
public class Test {
public static void main(String[] args) {
// create object of required home builder
EarthQuakeResistantBuilder earthQuakeResistantBuilder =
new EarthQuakeResistantBuilder();
// create object of director that will keep an eye on your builder
Director director = new Director(earthQuakeResistantBuilder);
director.manageRequiredHomeConstruction();
Home home = director.getComplexObjectOfHome();
System.out.println(home); // Home@5ca881b5
System.out.println(home.floor + " - " + home.walls + " - " + home.terrace);
// Wooden floor - Wooden walls - Wooden terrace
}
}
Structural Design Pattern
1. What is a Structural Design Pattern?
- Structural design patterns show you how to assemble different pieces of a system together in a flexible and extensible fashion.
- They help you guarantee that when one of the parts changes, the entire structure does not need to change.
According to GOF, the structural pattern can be realized in the following patterns:-
- Adapter Pattern
- Bridge Pattern
- Composite Pattern
- Decorator Pattern
- Facade Pattern
- Flyweight Pattern
- Proxy Pattern
Proxy Design Pattern
1. What are Proxy Design Patterns?
It’s a Structural design pattern. It lets you provide a substitute or placeholder for another object.
A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.
2. The architecture of the Proxy Design Pattern.
The client should always call the Proxy class.

public interface Subject {
public void method();
}
public class RealSubjectClass implements Subject {
@Override
public void method() {
System.out.println("I am an actual impl of Subject, rest all are proxies");
}
}
public class ProxyClass extends RealSubjectClass {
@Override
public void method() {
System.out.println("Hi, I am proxy, I will perform authentication and security checks");
// Logic to check if user is authentic or not. If Yes then call real object
// method else don't call below method
System.out.println("Calling real method of real Subject class impl"
+" class after the call is autheticated");
super.method();
}
}
public class Test {
public static void main(String[] args) {
Subject proxy = new ProxyClass();
proxy.method();
}
}
3. Roles in Proxy Design Pattern?
The major roles used in this design pattern are:-
- Subject – is an interface that exposes the functionality available to be used by the clients.
- Real Subject – is a class implementing Subject and it is the concrete implementation that needs to be hidden behind a proxy.
- Proxy – hides the real object by extending it and clients communicate to the real object via this proxy object. Usually, frameworks create this proxy object when the client requests for the real object.
4. Advantages of Proxy Design Pattern?***
- Control Access: Proxies can control access to the real object, which is useful for implementing access control mechanisms. This can enhance security by ensuring that only authorized users or processes can interact with the object.
- Lazy Initialization: Proxies can delay the creation and initialization of expensive objects until they are actually needed. This can improve performance and reduce resource consumption.
- Logging and Monitoring: Proxies can be used to add logging and monitoring functionality. By intercepting requests to the real object, proxies can log usage statistics, monitor performance, and track access patterns.
- Remote Proxy: In distributed systems, proxies can represent objects located in different address spaces. This allows for communication with remote objects as if they were local, simplifying the development of distributed applications.
- Caching: Proxies can cache the results of expensive operations to improve performance. If the same request is made multiple times, the proxy can return the cached result instead of invoking the real object again.
- Data Validation: Proxies can validate input data before passing it to the real object. This ensures that only valid data is processed, reducing the risk of errors and improving the robustness of the application.
- Flexibility and Customization: Proxies provide a flexible way to customize the behavior of objects. They can add, modify, or override functionality without changing the real object’s code.
Flyweight Design Pattern
1. What is the Flyweight Design Pattern?
- It’s a Structural design pattern.
- A flyweight is a shared object that can be used in multiple contexts simultaneously. The flyweight acts as an independent object in each context.
2. Where to use the Flyweight design pattern?
Your application needs many objects that share most of the common attributes (called intrinsic attributes/properties) and only a few unique attributes (extrinsic attributes/properties) then we can use the Flyweight design pattern.
We need to control the memory consumption by a large number of objects – by creating fewer objects and sharing them across.
Intrinsic attribute:- common attribute
Extrinsic attribute:- unique attribute
3. When to use the Flyweight design pattern?
- The number of objects to be created by the application should be huge.
- The object creation is heavy on memory and it can be time-consuming too.
- The object properties can be divided into intrinsic and extrinsic properties, extrinsic properties of an object should be defined by the client program.

public interface Animal {
void setName(String name);
public void printAnimalAttributes();
}
public class CommonSharableClass {
public static int eyes = 2;
public static int nose = 1;
public static int legs = 4;
public static int tail = 1;
}
public class Cat implements Animal {
private String name = null;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void printAnimalAttributes() {
System.out.println("Name: " + this.name);
System.out.println("Number of legs: " + CommonSharableClass.legs);
System.out.println("Number of eyes: " + CommonSharableClass.eyes);
System.out.println("Number of nose: " + CommonSharableClass.nose);
System.out.println("Number of tail: " + CommonSharableClass.tail);
}
}
public class Dog implements Animal {
private String name = null;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void printAnimalAttributes() {
System.out.println("Name: " + this.name);
System.out.println("Number of legs: " + CommonSharableClass.legs);
System.out.println("Number of eyes: " + CommonSharableClass.eyes);
System.out.println("Number of nose: " + CommonSharableClass.nose);
System.out.println("Number of tail: " + CommonSharableClass.tail);
}
}
public class Cow implements Animal {
private String name = null;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void printAnimalAttributes() {
System.out.println("Name: " + this.name);
System.out.println("Number of legs: " + CommonSharableClass.legs);
System.out.println("Number of eyes: " + CommonSharableClass.eyes);
System.out.println("Number of nose: " + CommonSharableClass.nose);
System.out.println("Number of tail: " + CommonSharableClass.tail);
}
}
import java.util.HashMap;
public class AnimalFactory {
private static final HashMap<String, Animal> animalMap = new HashMap<>();
public static Animal getCat(String name) {
String key = name + "-CAT";
Animal animal = animalMap.get(key);
if (animal == null) {
animal = new Cat();
animal.setName(name);
animalMap.put(key, animal);
}
return animal;
}
public static Animal getDog(String name) {
String key = name + "-DOG";
Animal animal = animalMap.get(key);
if (animal == null) {
animal = new Dog();
animal.setName(name);
animalMap.put(key, animal);
}
return animal;
}
public static Animal getCow(String name) {
String key = name + "-COW";
Animal animal = animalMap.get(key);
if (animal == null) {
animal = new Cow();
animal.setName(name);
animalMap.put(key, animal);
}
return animal;
}
}
public class Test {
public static void main(String[] args) {
Animal catAnimal = AnimalFactory.getCat("CAT");
catAnimal.printAnimalAttributes();
Animal catAnimal2 = AnimalFactory.getCat("CAT");
catAnimal2.printAnimalAttributes();
System.out.println(catAnimal.hashCode());
System.out.println(catAnimal2.hashCode());
Animal cowAnimal = AnimalFactory.getCat("COW");
cowAnimal.printAnimalAttributes();
Animal dogAnimal = AnimalFactory.getCat("DOG");
dogAnimal.printAnimalAttributes();
System.out.println(cowAnimal.hashCode());
System.out.println(dogAnimal.hashCode());
}
}
Behavioral Design Pattern
1. What is a Behavioral Design Pattern?
Behavioral design patterns are design patterns that identify common communication patterns between objects and realize these patterns.
The 11 behavioral design patterns are:
- Chain of Responsibility Pattern
- Command Pattern
- Interpreter Pattern
- Iterator Pattern
- Mediator Pattern
- Momento Pattern
- Observer Pattern
- State Pattern
- Strategy Pattern
- Template Pattern
- Visitor Pattern
Strategy Design Pattern
1. What is the Strategy Design Pattern in Java?
The strategy design pattern is a behavioral design pattern that enables selecting an algorithm at run-time. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
Example:- There are multiple encoding algorithms and based on our requirement we can use one of them while encoding.

2. Why is need for the Strategy design pattern?
Typically, we tend to bundle all the algorithm logic in the host class, resulting in a monolithic class with multiple switch cases or conditional statements. The following example shows the structure of one such class that supports multiple algorithms to encrypt data. Family of algorithms to use:-
public class Encryptor {
public void encrypt() {
if (algorithmName.equals("Aes")){
System.out.println("Encrypting data using AES algorithm");
} else if (algorithmName.equals("Blowfish")){
System.out.println("Encrypting data using Blowfish algorithm");
}
/*More else if statements for other encryption algorithms*/
}
}
- At runtime, the code loops through the statements to perform encryption based on the client-specified algorithm. The result is tightly coupled and rigid software that is difficult to change.
- You can imagine the consequences if you try to implement a new encryption algorithm, say TripleDES or a custom encryption algorithm.
- You’d have to open and modify the Encryptor class. Also, if an existing algorithm needs to be changed, the Encryptor class will again require modification.
- As you can see, our Encryptor class is a clear violation of the Open Closed principle – one of the SOLID design principles. As per the principle, new functionality should be added by writing new code, rather than modifying existing code.
- We can avoid them by using the Strategy pattern.
3. How does the Strategy Design Pattern Work?
We define one abstracting Interface and multiple implementing classes.
Clients can choose the algorithm to use at run time. Like adding it in the properties file and reading at runtime, thereby deciding which also to pick at run time only.
Each of the algorithm classes adheres to the Single Responsibility principle, another SOLID principle as they will only be concerned with encrypting data with a specific algorithm, which is currently lacking.
4. Participants of Strategy Design Pattern?
- Common interface.
- Multiple implementing standalone classes.
- Create Enum with Multiple encryption types.
- Strategy Factory -> having a Map<StrategyName, Interface>
Let us create a spring starter project to demonstrate this.
package com.knowprogram.dp.enums;
public enum EncodingPatternEnum {
MD5, SHA1, SHA2
}
package com.knowprogram.dp.encrypt;
public interface Encryption {
public void encrypt(String toBeEncrypted);
public EncodingPatternEnum getEncryptionType();
}
package com.knowprogram.dp.encrypt;
@Service
public class MD5Encryption implements Encryption {
@Override
public void encrypt(String toBeEncrypted) {
System.out.println("MD5Encryption.encrypt()");
}
@Override
public EncodingPatternEnum getEncryptionType() {
return EncodingPatternEnum.MD5;
}
}
package com.knowprogram.dp.encrypt;
@Service
public class SHA1Encryption implements Encryption {
@Override
public void encrypt(String toBeEncrypted) {
System.out.println("SHA1Encryption.encrypt()");
}
@Override
public EncodingPatternEnum getEncryptionType() {
return EncodingPatternEnum.SHA1;
}
}
package com.knowprogram.dp.encrypt;
@Service
public class SHA2Encryption implements Encryption {
@Override
public void encrypt(String toBeEncrypted) {
System.out.println("SHA2Encryption.encrypt()");
}
@Override
public EncodingPatternEnum getEncryptionType() {
return EncodingPatternEnum.SHA2;
}
}
package com.knowprogram.dp.factory;
@Component
public class EncryptionFactory {
Map<EncodingPatternEnum, Encryption> map;
public EncryptionFactory(Set<Encryption> encryptionTypeSet) {
System.out.println("EncryptionFactory.EncryptionFactory()");
createStrategy(encryptionTypeSet);
}
private void createStrategy(Set<Encryption> encryptionTypeSet) {
map = new HashMap<>();
encryptionTypeSet.stream()
.forEach(encryptionTypes ->
map.put(encryptionTypes.getEncryptionType(), encryptionTypes));
}
public Encryption findEncryptionType(EncodingPatternEnum encodingPatternEnum) {
return map.get(encodingPatternEnum);
}
}
package com.knowprogram.dp.restcontroller;
@RestController
public class DemoController {
@Autowired
EncryptionFactory factory;
@GetMapping("/encrypt")
public void encyptWithEncode(@RequestParam
EncodingPatternEnum encodingPatternEnum) {
factory.findEncryptionType(encodingPatternEnum).encrypt("code decode");
}
}
5. Real-time use of Strategy Design Pattern?
- Sorting class that supports multiple sorting algorithms, such as bubble sort, merge sort, and quick sort.
- A file compression class can support different compression algorithms, such as ZIP, GZIP, and LZ4, or even a custom compression algorithm.
- Data encryption class that encrypts data using different encryption algorithms, such as SHA1 SHA2 MD5, etc.
SOLID
1. Why do we need solid Principles?
The broad goal of the SOLID principles is to reduce dependencies so that engineers can change one area of software without impacting others. Additionally, they’re intended to make designs easier to understand, maintain, and extend. Ultimately, using these design principles makes it easier for software engineers to avoid issues and to build adaptive, effective, and agile software. They lead to better code for readability, maintainability, design patterns, and testability.
2. What are solid Principles?
The following five concepts make up our SOLID principles:-
- S:- Single Responsibility
- O:- Open/Closed
- L:- Liskov Substitution
- I:- Interface Segregation
- D:- Dependency Inversion
3. What is the Single Responsibility Principle in solid principles?
- It states that “One class should have one and only one responsibility”.
- Which specifically means – we should write, change, and maintain a class only for one purpose.
- Change Class only when you need to change the state of one particular object or instance
- Example: POJOS follow SRP.
- Suppose we have Employee and Address Class, If we want to change the state of Employee then we do not need to modify the class Account and vice-versa.
- If you would have merged both as a single POJO, then modification in one field for address (like state ) needs to be modified, and the whole POJO including Employee.
- Worst Design – Merging Entity class with service code(CRUD)/business logic. That’s why we have the service layer, DAP layer, and Entities separated.
4. Why is the Single Responsibility Principle important?
- In the Real world, requirements change and so does our code implementation to cater to the changing requirements. The more responsibilities our class has, the more often we need to change it. To prevent frequent changes to the same class SRP is important.
- Testing is easier – With a single responsibility, the class will have fewer test cases.
- Easier to Understand.
- Less functionality also means fewer dependencies on other classes.
- So best practice says: Use layers in your application and break God classes into smaller classes or modules.
5. What is the Open/Closed Principle in solid principles?
- It states that “Software components should be open for extension, but closed for modification”.
- The term “Open for extension” means that we can extend and include extra functionalities in our code without altering or affecting our existing implementation.
- The term “Closed for modification” means that after we add the extra functionality, we should not modify the existing implementation.
- In the real world, You must have noticed that you change something to cater to a new requirement and some other functionality breaks because of your change. To prevent that we have this principle in hand. It’s one of the most important concepts in SOLID principles.
6. How to implement the Open/Closed Principles in solid principles?
The application classes should be designed in such a way that whenever fellow developers want to change the flow of control in specific conditions in the application, all they need to extend the class and override some functions, and that’s it.
Example – created a POJO employee with ID and name. Now new functionality comes which says add Training location. Your constructor will fail for employees who didn’t do training. Better extend employee class, name it TrainedEmployee then add constructor.
public class Employee {
private int id;
private String name;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
// getter, setter
}
public class TrainedEmployee extends Employee {
private String trainingAreas;
public TrainedEmployee(int id, String name, String trainingAreas) {
super(id, name);
this.trainingAreas = trainingAreas;
}
}
7. Why is the Open/Close principle important?
Using this principle separates the existing code from the modified code so it provides better stability, and maintainability and minimizes changes as in your code.
8. What is the Liskov Substitution Principle?
- LSP states that “the software should not alter the desirable results when we replace a parent type with any of the subtypes” or “Derived types must be completely substitutable for their base types“.
- LSP means that the classes, fellow developers created by extending our class, should be able to fit into the application without failure. This is important when we resort to polymorphic behavior through inheritance.
- Take the previous example, print employee from child or parent reference prints all required details
- This requires the objects of the subclasses to behave in the same way as the objects of the superclass. This is mostly seen in places where we do runtime type identification and then cast it to the appropriate reference type.
9. Why is the Liskov Substitution Principle important?
- This avoids misusing inheritance.
- It helps us conform to the “IS-A” relationship.
- We can also say that subclasses must fulfill a contract defined by the base class.
Wrongly Implementing it can prove real-world objects wrong like:- “Square is a Rectangle”, it is not true in the real world but it’s easy to implement through code. To prevent this, better use this principle.
10. What is the Interface Segregation Principle?
It states that “Clients should not be forced to implement unnecessary methods which they will not use“.
ISP is applicable to interfaces as a single responsibility principle holds to classes. ISP states that we should split our interfaces into smaller and more specific ones.
Assume we have an interface that accepts different types of payments and has different modes of order.
public interface PizzaApp {
public void acceptOrderOnline();
public void acceptWalkInOrders();
public void acceptTelephonicOrder();
public void acceptPaymentsOnline();
public void acceptCash();
}
Now whenever we create an implementation class then they must accept online payment and cash. Similarly, they have to accept online orders, walk-ins, and telephonic orders. So, the interface is forcing all implementation classes to implement all these. If it is a cloud kitchen then it won’t accept walk-in orders and telephonic orders, and will only accept online payment.
Let us split the interface into smaller and specific ones.
public interface PizzaAppOnline {
public void acceptOrderOnline();
public void acceptPaymentsOnline();
}
public interface PizzaAppOffline {
public void acceptWalkInOrders();
public void acceptTelephonic();
public void acceptCash();
}
11. What is the Dependency Inversion Principle?
- It states that it “Depends on abstractions, not on concretions“.
- We should design our software in such a way that various modules can be separated from each other using an abstract layer to bind them together.
public class Student {
private Address address;
public Student() {
address = new Address();
}
}
12. Why is Dependency inversion Important?
- It allows a programmer to remove hardcoded dependencies so that the application becomes loosely coupled and extendable.
- In the above example, Student class requires an Address object and it is responsible for initializing and using the Address object. If the Address class is changed in the future then we have to make changes in the Student class also. This makes the tight coupling between Students and Address objects. We can resolve this problem using the dependency inversion design pattern. i.e. Address object will be implemented independently and will be provided to the Student when the Student is instantiated by using constructor-based or setter-based dependency inversion.
If you enjoyed this post, share it with your friends. Do you want to share more information about the topic discussed above or do you find anything incorrect? Let us know in the comments. Thank you!