Design patterns are reusable and proven solutions to common software design problems. They provide a structured approach for building flexible, maintainable, and scalable applications while helping developers write cleaner code and make better architectural decisions.
- Design patterns represent best practices derived from experienced developers.
- They promote code reusability, maintainability, and loose coupling.
- Commonly categorized into Creational, Structural, and Behavioral patterns.
1. Why are design patterns used in software development?
Design patterns provide proven and reusable solutions to common software design problems. They help developers write clean, maintainable, and well-structured code while promoting best practices. They also improve flexibility, scalability, and communication among developers.
- Promote loose coupling and modular design
- Enhance code reusability, maintainability, and scalability
- Provide a common language for developers to communicate design ideas
2. How Are Design Patterns Different from Algorithms?
While algorithms and design patterns both offer solutions for recurring problems, their difference lies in their purpose:
- Algorithms give a step-by-step solution to perform a specific task, often focusing on solving computational problems.
- Design Patterns, give general guidelines or blueprints on how to organize software to address repeated design problems. Patterns are more concerned with the architecture and object interactions, whereas algorithms are concerned with an exact computational step.
3. How Are Design Principles Different from Design Patterns?
Design Principles are guidelines followed during software design, such as SOLID, which focus on making software more scalable, extensible, and maintainable. These principles apply to the entire development process and programming practices.
Design Patterns, however, are predefined solutions to specific design problems. They are ready-to-use solutions that can be customized based on specific needs. For example, Factory, Singleton, and Strategy patterns are solutions for specific issues that arise during software development.
4. What are the Types of Design Patterns?
Three main types of Design Patterns are as follows
- Creational Patterns: Deal with object creation mechanisms (e.g., Singleton, Factory).
- Structural Patterns: Deal with object composition and inheritance (e.g., Adapter, Facade).
- Behavioral Patterns: Deal with object interactions and communication (e.g., Observer, Strategy).
5. What are the Advantages of Using Design Patterns?
There are many advantages of using Design Patterns
- Proven Solutions: They offer tested solutions to common problems, saving time and effort.
- Reusability: They are reusable and can be used in multiple projects.
- Better Communication: They provide a common language for developers to discuss designs clearly.
- Improved Architecture: They help in creating a well-organized and easy to understand system.
- Clarity: Design patterns make the design process transparent and easier to follow.
6. What are the types of creational Patterns?
The following are five types of Creational Patterns:
- Factory method
- Abstract Factory
- Builder
- Prototype
- Singleton
7. What are the types of Structural patterns?
The types of Structural patterns are as follow :
- Adapter
- Bridge
- Filter
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
8. What are the types of Behavioral patterns?
The types of Behavioral Patterns are as follow:
- Interpreter Pattern
- Template Method Pattern
- Chain of responsibility Pattern
- Command Pattern
- Iterator Pattern
- Strategy Pattern
- Visitor Pattern
9. What Are Some of the Design Patterns Used in Java’s JDK Library?
Some of the design patterns used in Java's JDK library include:
- Decorator Pattern: Used by wrapper classes like
BufferedReaderandBufferedWriter. - Singleton Pattern: Used by
RuntimeandCalendarclasses to ensure only one instance exists. - Factory Pattern: Used in methods like
Integer.valueOf()for converting strings to integers. - Observer Pattern: Used in event-handling frameworks like
AWTandSwingfor UI event management.
Examples: Decorator Pattern: BufferedReader wraps a FileReader to add buffering functionality without modifying FileReader.
10. What Are the SOLID Principles?
The SOLID Principles are five design principles developers use to write clean, maintainable, and scalable code:
- Single Responsibility Principle (SRP): A class should have a single reason to change.
- Open-Closed Principle (OCP): Software entities must be open for extension but closed for modification.
- Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of its subclasses without changing the correctness of the program.
- Interface Segregation Principle (ISP): Clients shouldn't be made to depend on interfaces they don't use.
- Dependency Inversion Principle (DIP): High-level modules must not be dependent on low-level modules. Both of them must be dependent upon abstractions.
11. What is Known as Gang of Four?
The four authors who published the book Design Patterns Elements of Reusable Object-Oriented Software are known as Gang of Four. The name of four authors are Erich Gamma, Ralph Johnson, Richard Helm, and John Vlissides.
12. What is the Singleton pattern, and when would you use it?
The Singleton Pattern ensures that a class has only one instances and provides a global point of access to that instance. It is used when we want to limit creation of an object to only one instance. It ensures controlled access to a resource.
Example:
#include <iostream>
class Singleton {
private:
static Singleton* instance;
Singleton() {} // Private constructor
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
public class Singleton {
private static Singleton instance;
private Singleton() {} // Private constructor
public static Singleton getInstance()
{
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
# Private constructor equivalent
def __init__(self):
if not hasattr(self, 'initialized'):
self.initialized = True
class Singleton {
static instance = null;
constructor() {} // Private constructor
static getInstance() {
if (Singleton.instance === null) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
Use:
Use the Singleton pattern when:
- You need to control access to a shared resource like a database connection or a configuration manager.
- You want to ensure that there is only one instance of a class preventing redundant copies.
13. Explain the Factory Method Pattern and provide an Example of its use.
The Factory Method Pattern defines an interface for creating objects because it allows subclasses to alter the type of objects that will be created. This pattern helps in assigning the object creation to subclasses that enables flexibility and extensibility.
Use the Factory Method Pattern when:
- You need to create objects, but the exact type of the object should be determined by subclasses.
- You want to transfer the responsibility of object creation to subclasses without altering the code that uses the objects.
Example:
#include <iostream>
#include <memory>
#include <string>
// Product Interface
class Product {
public:
virtual std::string operation() const = 0;
};
// Concrete Products
class ConcreteProduct1 : public Product {
public:
std::string operation() const override {
return "Product 1";
}
};
class ConcreteProduct2 : public Product {
public:
std::string operation() const override {
return "Product 2";
}
};
// Creator Interface
class Creator {
public:
virtual std::unique_ptr<Product> factory_method() const = 0;
std::string some_operation() const {
std::unique_ptr<Product> product = factory_method();
return "Creator: " + product->operation();
}
};
// Concrete Creators
class ConcreteCreator1 : public Creator {
public:
std::unique_ptr<Product> factory_method() const override {
return std::make_unique<ConcreteProduct1>();
}
};
class ConcreteCreator2 : public Creator {
public:
std::unique_ptr<Product> factory_method() const override {
return std::make_unique<ConcreteProduct2>();
}
};
// Product Interface
interface Product {
String operation();
}
// Concrete Products
class ConcreteProduct1 implements Product {
@Override
public String operation() {
return "Product 1";
}
}
class ConcreteProduct2 implements Product {
@Override
public String operation() {
return "Product 2";
}
}
// Creator Interface
abstract class Creator {
public abstract Product factoryMethod();
public String someOperation() {
Product product = factoryMethod();
return "Creator: " + product.operation();
}
}
// Concrete Creators
class ConcreteCreator1 extends Creator {
@Override
public Product factoryMethod() {
return new ConcreteProduct1();
}
}
class ConcreteCreator2 extends Creator {
@Override
public Product factoryMethod() {
return new ConcreteProduct2();
}
}
from abc import ABC, abstractmethod
class Creator(ABC):
@abstractmethod
def factory_method(self):
pass
def some_operation(self):
product = self.factory_method()
return f"Creator: {product.operation()}"
class ConcreteCreator1(Creator):
def factory_method(self):
return ConcreteProduct1()
class ConcreteCreator2(Creator):
def factory_method(self):
return ConcreteProduct2()
class Product(ABC):
@abstractmethod
def operation(self):
pass
class ConcreteProduct1(Product):
def operation(self):
return "Product 1"
class ConcreteProduct2(Product):
def operation(self):
return "Product 2"
// Product Interface
class Product {
constructor() {}
operation() {
throw new Error('operation method must be overridden.');
}
}
// Concrete Products
class ConcreteProduct1 extends Product {
operation() {
return 'Product 1';
}
}
class ConcreteProduct2 extends Product {
operation() {
return 'Product 2';
}
}
// Creator Interface
class Creator {
constructor() {}
factoryMethod() {
throw new Error('factoryMethod must be overridden.');
}
someOperation() {
const product = this.factoryMethod();
return `Creator: ${product.operation()}`;
}
}
// Concrete Creators
class ConcreteCreator1 extends Creator {
factoryMethod() {
return new ConcreteProduct1();
}
}
class ConcreteCreator2 extends Creator {
factoryMethod() {
return new ConcreteProduct2();
}
}
14. Describe the Adapter Pattern and provide an Example of where it can be applied.
The Adapter pattern allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.
Use the Adapter Pattern when:
- We need to integrate classes that have incompatible interfaces.
- We cannot modify the existing class but need it to work with new code or libraries.
Example:
#include <iostream>
using namespace std;
// Target Interface
class ITarget {
public:
virtual void Request() = 0; // pure virtual function
virtual ~ITarget() {} // virtual destructor
};
// Adaptee Class
class Adaptee {
public:
void SpecificRequest() {
cout << "Adaptee's method called" << endl;
}
};
// Adapter Class
class Adapter : public ITarget {
private:
Adaptee* adaptee;
public:
Adapter() {
adaptee = new Adaptee();
}
~Adapter() {
delete adaptee;
}
void Request() override {
adaptee->SpecificRequest();
}
};
// Client Code
int main() {
ITarget* target = new Adapter();
target->Request(); // Calls adaptee's method through adapter
delete target;
return 0;
}
import java.util.logging.Logger;
// Target Interface
interface ITarget {
void Request();
}
// Adaptee Class
class Adaptee {
public void SpecificRequest() {
System.out.println("Adaptee's method called");
}
}
// Adapter Class
class Adapter implements ITarget {
private Adaptee adaptee;
public Adapter() {
adaptee = new Adaptee();
}
@Override
public void Request() {
adaptee.SpecificRequest();
}
}
// Client Code
public class Main {
public static void main(String[] args) {
ITarget target = new Adapter();
target.Request(); // Calls adaptee's method through adapter
}
}
import logging
# Target Interface
class ITarget:
def Request(self):
pass
# Adaptee Class
class Adaptee:
def SpecificRequest(self):
print("Adaptee's method called")
# Adapter Class
class Adapter(ITarget):
def __init__(self):
self.adaptee = Adaptee()
def Request(self):
self.adaptee.SpecificRequest()
# Client Code
if __name__ == '__main__':
target = Adapter()
target.Request() # Calls adaptee's method through adapter
15. Provide a scenario where the Command pattern would be preferable to the Strategy pattern.
The Command pattern is preferable you want to encapsulate a request as an object with additional metadata such as the request's originator or ability to queue commands for later execution. It allows you to undo/redo operations, queue them for execution or log them.
Imagine a remote control for multiple devices (TV, lights, fan). Each button on the remote represents a command. The Command Pattern allows you to treat each button's action as a command object, which can be queued, executed, or undone. This is more complex than simply switching algorithms and involves additional metadata (such as the device's state or the command's origin).
The Strategy Pattern focuses on encapsulating interchangeable algorithms where there is no need for metadata about the request itself.
16. Explain the Single Responsibility Principle and its significance in Software Design.
The SRP defines that one class should have just a single reason to change i.e it should have only one responsibility or task. This principle ensures that one class is dedicated to a single concern and is not overloaded with several unrelated responsibilities.
Significance:
- Modularity: By implementing SRP the system is more modular and easier to maintain and extend.
- Maintainability: Changes in one area of the system are less likely to impact other areas that are unrelated, limiting the risk of bugs when changing the code.
- Readability: Code becomes more readable and easier to comprehend since each class has a defined, singular responsibility.
17. What is the Observer Pattern, and how does it allow objects to inform other objects about changes in State?
The Observer pattern establishes a one-to-many relationship between objects such that one object (the subject) keeps a list of its dependents (observers) and informs them of changes to the state. The observers are updated automatically whenever the subject's state is changed.
Example:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// Observer Interface
class Observer {
public:
virtual void update(const string& message) = 0;
virtual ~Observer() {} // Virtual destructor for proper cleanup
};
// Concrete Observer
class ConcreteObserver : public Observer {
private:
string name;
public:
ConcreteObserver(const string& observerName) : name(observerName) {}
void update(const string& message) override {
cout << name << " received message: " << message << endl;
}
};
// Subject Class
class Subject {
private:
vector<Observer*> observers;
public:
void attach(Observer* observer) {
observers.push_back(observer);
}
void detach(Observer* observer) {
observers.erase(
remove(observers.begin(), observers.end(), observer),
observers.end()
);
}
void notifyObservers(const string& message) {
for (Observer* observer : observers) {
observer->update(message);
}
}
};
// Client Code
int main() {
Subject subject;
ConcreteObserver observer1("Observer 1");
ConcreteObserver observer2("Observer 2");
subject.attach(&observer1);
subject.attach(&observer2);
subject.notifyObservers("Hello, Observers!");
subject.detach(&observer1);
subject.notifyObservers("Second message!");
return 0;
}
import java.util.ArrayList;
import java.util.List;
// Observer Interface
interface Observer {
void update(String message);
}
// Concrete Observer
class ConcreteObserver implements Observer {
private String name;
ConcreteObserver(String observerName) {
this.name = observerName;
}
@Override
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
// Subject Class
class Subject {
private List<Observer> observers = new ArrayList<>();
void attach(Observer observer) {
observers.add(observer);
}
void detach(Observer observer) {
observers.remove(observer);
}
void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
// Client Code
public class Main {
public static void main(String[] args) {
Subject subject = new Subject();
ConcreteObserver observer1 = new ConcreteObserver("Observer 1");
ConcreteObserver observer2 = new ConcreteObserver("Observer 2");
subject.attach(observer1);
subject.attach(observer2);
subject.notifyObservers("Hello, Observers!");
subject.detach(observer1);
subject.notifyObservers("Second message!");
}
}
from abc import ABC, abstractmethod
from typing import List
# Observer Interface
class Observer(ABC):
@abstractmethod
def update(self, message: str):
pass
# Concrete Observer
class ConcreteObserver(Observer):
def __init__(self, observer_name: str):
self.name = observer_name
def update(self, message: str):
print(f"{self.name} received message: {message}")
# Subject Class
class Subject:
def __init__(self):
self.observers: List[Observer] = []
def attach(self, observer: Observer):
self.observers.append(observer)
def detach(self, observer: Observer):
self.observers.remove(observer)
def notify_observers(self, message: str):
for observer in self.observers:
observer.update(message)
# Client Code
if __name__ == "__main__":
subject = Subject()
observer1 = ConcreteObserver("Observer 1")
observer2 = ConcreteObserver("Observer 2")
subject.attach(observer1)
subject.attach(observer2)
subject.notify_observers("Hello, Observers!")
subject.detach(observer1)
subject.notify_observers("Second message!")
/* Observer Interface */
class Observer {
update(message) {
throw new Error('Method update() must be implemented.');
}
}
/* Concrete Observer */
class ConcreteObserver extends Observer {
constructor(observerName) {
super();
this.name = observerName;
}
update(message) {
console.log(`${this.name} received message: ${message}`);
}
}
/* Subject Class */
class Subject {
constructor() {
this.observers = [];
}
attach(observer) {
this.observers.push(observer);
}
detach(observer) {
this.observers = this.observers.filter(obs => obs!== observer);
}
notifyObservers(message) {
this.observers.forEach(observer => observer.update(message));
}
}
/* Client Code */
const subject = new Subject();
const observer1 = new ConcreteObserver('Observer 1');
const observer2 = new ConcreteObserver('Observer 2');
subject.attach(observer1);
subject.attach(observer2);
subject.notifyObservers('Hello, Observers!');
subject.detach(observer1);
subject.notifyObservers('Second message!');
18. Define the Open/Closed Principle and how design patterns enforce it.
The Open/Closed Principle (OCP) defines that software entities (classes, modules, functions) must be open for extension but closed for modification. This implies you can extend the functionality without changing existing code.
Design patterns such as Strategy and Decorator Patterns enable new behavior to be added by inheriting existing classes or modules instead of modifying them. This follows the OCP by avoiding modifications to the fundamental code while still allowing additional behavior.
Example: Using the Strategy Pattern, new algorithms can be introduced by creating new strategy classes without modifying the existing context code.
19. How is the Bridge Pattern different from the Adapter Pattern?
- The Bridge Pattern is used to separate an abstraction from its implementation, allowing both to vary independently. It’s ideal when you want to decouple a class’s interface from its implementation so that you can change either without affecting the other.
- The Adapter Pattern, on the other hand converts one interface into another expected by the client allowing incompatible interfaces to work together. The Adapter is a structural pattern used to make existing classes compatible with others.
Example: Bridge separates a remote control abstraction from device implementations like TV and radio, whereas Adapter allows a legacy device to work with a new control interface.
20. In what way does the Dependency Inversion Principle facilitate loose coupling and how does it relate to design patterns?
- The Dependency Inversion Principle (DIP) encourages loose coupling by keeping high-level modules dependent on abstractions and not concrete implementations. This facilitates change in implementations without influencing the high-level modules that are dependent upon them.
- Many design patterns, like Factory Method and Dependency Injection, help implement DIP by ensuring that classes depend on abstractions and not on concrete implementations. This results in more flexible, testable, and maintainable code
Example: In the Strategy or Factory Pattern, the client depends on an interface, allowing new implementations to be added without modifying existing code.
21. Give a real-world example of using the Singleton Pattern in a common library or framework.
A common real-world example of using the Singleton Pattern is the Runtime class in Java. The Runtime class is a Singleton that provides access to the runtime environment of the Java application. It ensures that only one instance of the runtime is used throughout the application.
Example:
Runtime runtime = Runtime.getRuntime();This guarantees that the runtime environment is being accessed in a controlled fashion without instantiating multiple objects.
22. Give an example where the Strategy pattern is applied to switch between various Algorithms.
The Strategy Pattern enables a class to alter its behavior (or algorithm) at runtime. An example would be sorting algorithms. You may employ various sorting strategies based on data size or type.
Example:
#include <iostream>
#include <vector>
class SortingStrategy {
public:
virtual void sort(std::vector<int>& array) = 0;
};
// Concrete Strategy 1
class BubbleSort : public SortingStrategy {
public:
void sort(std::vector<int>& array) override {
std::cout << "BubbleSort selected" << std::endl;
}
};
// Concrete Strategy 2
class InsertionSort : public SortingStrategy {
public:
void sort(std::vector<int>& array) override {
std::cout << "InsertionSort selected" << std::endl;
}
};
// Context
class SortContext {
private:
SortingStrategy* strategy;
public:
SortContext(SortingStrategy* strategy) : strategy(strategy) {}
void setStrategy(SortingStrategy* strategy) {
this->strategy = strategy;
}
void sort(std::vector<int>& array) {
strategy->sort(array);
}
};
// Client
int main() {
std::vector<int> data = {3, 1, 2};
SortContext context(new BubbleSort());
context.sort(data);
context.setStrategy(new InsertionSort());
context.sort(data);
return 0;
}
interface SortingStrategy {
void sort(int[] array);
}
// Concrete Strategy 1
class BubbleSort implements SortingStrategy {
public void sort(int[] array) {
System.out.println("BubbleSort selected");
}
}
// Concrete Strategy 2
class InsertionSort implements SortingStrategy {
public void sort(int[] array) {
System.out.println("InsertionSort selected");
}
}
// Context
class SortContext {
private SortingStrategy strategy;
public SortContext(SortingStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] array) {
strategy.sort(array);
}
}
// Client
public class StrategyMiniDemo {
public static void main(String[] args) {
int[] data = {3, 1, 2};
SortContext context = new SortContext(new BubbleSort());
context.sort(data);
context.setStrategy(new InsertionSort());
context.sort(data);
}
}
from abc import ABC, abstractmethod
# Interface
class SortingStrategy(ABC):
@abstractmethod
def sort(self, array):
pass
# Concrete Strategy 1
class BubbleSort(SortingStrategy):
def sort(self, array):
print("BubbleSort selected")
# Concrete Strategy 2
class InsertionSort(SortingStrategy):
def sort(self, array):
print("InsertionSort selected")
# Context
class SortContext:
def __init__(self, strategy):
self.strategy = strategy
def set_strategy(self, strategy):
self.strategy = strategy
def sort(self, array):
self.strategy.sort(array)
# Client
if __name__ == '__main__':
data = [3, 1, 2]
context = SortContext(BubbleSort())
context.sort(data)
context.set_strategy(InsertionSort())
context.sort(data)
// Interface
class SortingStrategy {
sort(array) {
throw new Error('This method should be overridden!');
}
}
// Concrete Strategy 1
class BubbleSort extends SortingStrategy {
sort(array) {
console.log('BubbleSort selected');
}
}
// Concrete Strategy 2
class InsertionSort extends SortingStrategy {
sort(array) {
console.log('InsertionSort selected');
}
}
// Context
class SortContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(array) {
this.strategy.sort(array);
}
}
// Client
const data = [3, 1, 2];
const context = new SortContext(new BubbleSort());
context.sort(data);
context.setStrategy(new InsertionSort());
context.sort(data);
23. When should you avoid using design patterns, and how can you prevent over-engineering?
Design patterns should be avoided when they add unnecessary complexity to a problem that can be solved with a simpler approach. Overusing patterns can lead to rigid, hard-to-maintain code and reduced readability.
- Avoid patterns when they introduce unnecessary abstraction or complexity
- Use patterns only when there is a clear design problem to solve
- Prefer simple solutions first; apply patterns only when needed
Example: Using the Strategy Pattern for tax calculation when there is only one fixed tax rule adds unnecessary classes and complexity. A simple method is sufficient, and the pattern should be introduced only if multiple tax algorithms are needed in the future.
24. How do design patterns help in managing dependencies in large-scale applications?
Design patterns help manage dependencies by structuring interactions through abstractions instead of direct class-to-class references.
- Reduce tight coupling using interfaces and indirection
- Make dependency changes localized and predictable
Example: In a large microservices-based system, Factory and Dependency Injection patterns manage object creation and wiring without spreading dependency logic across the codebase.
25. Give an example of how the Decorator Pattern can be applied to extend the functionality of a pre-existing class in a Codebase.
The Decorator Pattern can be employed to extend an object dynamically without modifying its structure. A typical example is in text processing software, where you would like to include facilities such as spell-checking, formatting, or encryption in a text editor.
Example:
#include <iostream>
#include <string>
// Component interface
class Text {
public:
virtual std::string getText() const = 0;
virtual ~Text() {}
};
// Concrete Component
class PlainText : public Text {
private:
std::string text;
public:
PlainText(const std::string& text) : text(text) {}
std::string getText() const override {
return text;
}
};
// Base Decorator
class TextDecorator : public Text {
protected:
Text* wrappedText;
public:
TextDecorator(Text* wrappedText) : wrappedText(wrappedText) {}
std::string getText() const override {
return wrappedText->getText();
}
};
// Concrete Decorator
class BoldTextDecorator : public TextDecorator {
public:
BoldTextDecorator(Text* wrappedText) : TextDecorator(wrappedText) {}
std::string getText() const override {
return "<b>" + TextDecorator::getText() + "</b>";
}
};
// Main function
int main() {
Text* text = new PlainText("Hello Decorator");
std::cout << text->getText() << std::endl;
Text* boldText = new BoldTextDecorator(text);
std::cout << boldText->getText() << std::endl;
delete text;
delete boldText;
return 0;
}
// Component interface
interface Text {
String getText();
}
// Concrete Component
class PlainText implements Text {
private String text;
public PlainText(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
// Base Decorator
class TextDecorator implements Text {
protected Text wrappedText;
public TextDecorator(Text wrappedText) {
this.wrappedText = wrappedText;
}
public String getText() {
return wrappedText.getText();
}
}
// Concrete Decorator
class BoldTextDecorator extends TextDecorator {
public BoldTextDecorator(Text wrappedText) {
super(wrappedText);
}
@Override
public String getText() {
return "<b>" + wrappedText.getText() + "</b>";
}
}
// Main class (THIS WAS MISSING)
public class GFG {
public static void main(String[] args) {
Text text = new PlainText("Hello Decorator");
System.out.println(text.getText());
Text boldText = new BoldTextDecorator(text);
System.out.println(boldText.getText());
}
}
# Component interface
from abc import ABC, abstractmethod
class Text(ABC):
@abstractmethod
def get_text(self):
pass
# Concrete Component
class PlainText(Text):
def __init__(self, text):
self.text = text
def get_text(self):
return self.text
# Base Decorator
class TextDecorator(Text):
def __init__(self, wrapped_text):
self.wrapped_text = wrapped_text
def get_text(self):
return self.wrapped_text.get_text()
# Concrete Decorator
class BoldTextDecorator(TextDecorator):
def get_text(self):
return f"<b>{super().get_text()}</b>"
# Main code
if __name__ == "__main__":
text = PlainText("Hello Decorator")
print(text.get_text())
bold_text = BoldTextDecorator(text)
print(bold_text.get_text())
// Component interface
class Text {
getText() {
throw new Error('getText method must be implemented.');
}
}
// Concrete Component
class PlainText extends Text {
constructor(text) {
super();
this.text = text;
}
getText() {
return this.text;
}
}
// Base Decorator
class TextDecorator extends Text {
constructor(wrappedText) {
super();
this.wrappedText = wrappedText;
}
getText() {
return this.wrappedText.getText();
}
}
// Concrete Decorator
class BoldTextDecorator extends TextDecorator {
getText() {
return `<b>${super.getText()}</b>`;
}
}
// Main code
const text = new PlainText('Hello Decorator');
console.log(text.getText());
const boldText = new BoldTextDecorator(text);
console.log(boldText.getText());
26. What Is Inversion of Control?
Inversion of Control (IoC) is a design principle in which the management of object creation and control is moved from the program to a container or framework. It is often applied in dependency injection frameworks, where objects are injected into classes instead of being instantiated within them. This decouples things and enhances flexibility.
Example: In Spring, objects are created and injected by the container instead of being instantiated directly using new.
27. How can the Command Pattern be used in a User Interface (UI) Framework?
The Command Pattern may be applied in a UI framework to encapsulate user actions (such as button clicks) into command objects. These commands can be executed, undone, or stored in a history for features like undo/redo.
Example: Every user action (e.g., button click) is wrapped into a command object, which is then run by the UI framework. To implement undo/redo, prior commands are saved and replayed appropriately.
28. What is the purpose of a UML diagram in displaying design patterns?
Design patterns, classes, their relationships, and interactions are visualized using UML (Unified Modeling Language) diagrams. They facilitate clear communication of design ideas, making it simple to comprehend the structure of the pattern and how it would be placed within a system.
Example: A UML diagram for the Observer Pattern shows the Subject, Observer interface, and concrete observers with their relationships, making the pattern easier to grasp.
29. How can Design Patterns assist in refactoring existing code?
Design patterns offer proven solutions to recurring design issues. While refactoring code, you can spot areas where these patterns can be applied in order to enhance code structure, maintainability, and flexibility. which eliminates code duplication and makes the system easier to scale.
Example: A large conditional block can be refactored into the Strategy Pattern, where each condition becomes a separate strategy class.
30. What Is the Role of Design Patterns in Software Development?
Design patterns offer proven solutions to common issues in software development. They encourage best practices, improve code maintainability, and increase software scalability, and this makes the codebase easier and more flexible to maintain over the long term.
Example: Using the Factory Pattern for object creation decouples client code from concrete classes, making the system easier to extend and maintain.
31. What problem does the Builder Pattern solve that constructors cannot?
The Builder Pattern solves the problem of creating complex objects with many optional parameters, where constructors become difficult to manage and understand.
- Prevents constructor telescoping caused by multiple overloaded constructors
- Improves readability when setting optional fields
- Supports step-by-step object creation
Example: While creating a Computer object with optional fields like RAM, SSD, and GPU, a builder allows setting only required parts using chained methods instead of passing many parameters to a constructor.
32. How does the Prototype Pattern differ from object cloning using clone() in Java?
The Prototype Pattern focuses on creating new objects by copying existing ones, while Java’s clone() is a low-level mechanism for object copying.
- Prototype Pattern provides controlled and explicit cloning logic
- clone() requires implementing Cloneable and can lead to shallow copy issues
- Prototype hides cloning complexity behind an interface
Example: In a game, different enemy objects can be created by copying a prototype enemy. The Prototype Pattern ensures proper deep copying, whereas using clone() directly may accidentally share internal objects like weapons or health state.
33. When should the Abstract Factory Pattern be preferred over the Factory Method Pattern?
The Abstract Factory Pattern should be preferred when you need to create families of related or dependent objects without specifying their concrete classes.
- When multiple related products must be created together and be compatible
- When switching entire product families at runtime is required
Example: In a UI toolkit, Abstract Factory can create Windows buttons and menus or Mac buttons and menus together, while Factory Method would handle only one product at a time.
34. What are the disadvantages of using the Singleton Pattern?
The Singleton Pattern can introduce hidden design and testing problems despite ensuring a single instance.
- Creates global state, making code harder to test and maintain
- Introduces tight coupling and limits flexibility
Example: A Singleton database connection can make unit testing difficult because tests cannot easily replace it with a mock or create isolated instances.
35. How does the Lazy Initialization technique work in the Singleton Pattern?
Lazy Initialization in the Singleton Pattern delays object creation until it is actually needed, instead of creating it at class loading time.
- Instance is created only when getInstance() is called
- Saves memory and startup time for heavy objects
Example: A logging service singleton is created only when the application first logs a message, not when the application starts.
36. Explain the difference between Composition and Inheritance with respect to design patterns.
Composition focuses on building behavior by combining objects, while inheritance relies on extending classes to reuse behavior.
- Composition provides better flexibility and avoids tight coupling
- Inheritance creates rigid hierarchies and can break with change
Example: In the Strategy Pattern, a class uses composition to delegate behavior to a strategy object, instead of inheriting different behavior through subclasses.
37. What role does encapsulation play in implementing behavioral design patterns?
Encapsulation helps behavioral design patterns by hiding how behavior is implemented and exposing only what the client needs to use.
- Separates what an object does from how it does it
- Allows behavior to change without affecting client code
Example: In the Command Pattern, encapsulation wraps a request inside a command object, so the caller does not know how the request is executed.
38. Explain how the Facade Pattern simplifies complex subsystems.
The Facade Pattern provides a single, unified interface that hides the complexity of multiple interacting subsystems.
- Reduces coupling between clients and subsystem classes
- Makes the system easier to use and understand
Example: In a home theater system, a facade exposes methods like watchMovie(), while internally coordinating the projector, sound system, and lights.
39. In what scenarios is the Flyweight Pattern most effective?
The Flyweight Pattern is most effective when a system needs to handle a large number of similar objects efficiently by sharing common state.
- When memory usage must be minimized for many fine-grained objects
- When objects can share intrinsic (common) data
Example: In a text editor, character objects share font and style data using Flyweight, while position and content are stored separately.
40. How does the Proxy Pattern differ from the Decorator Pattern?
The Proxy Pattern controls access to an object, while the Decorator Pattern adds new behavior to an object dynamically.
- Proxy focuses on access control, lazy loading, or security
- Decorator focuses on extending functionality without changing the original class
Example: A Proxy may check user permissions before accessing a file, whereas a Decorator may add logging or compression to file access without restricting it.
41. What problem does the Chain of Responsibility Pattern solve?
The Chain of Responsibility Pattern solves the problem of coupling a request sender to a specific request handler by passing the request through a chain of handlers.
- Allows multiple objects to handle a request without the sender knowing which one will process it
- Promotes loose coupling and flexible request handling
Example: In an approval system, a request passes through manager, director, and CEO handlers until one of them approves it.
42. How does the Mediator Pattern reduce coupling between objects?
The Mediator Pattern reduces coupling by centralizing communication logic between objects into a single mediator.
- Objects interact through the mediator instead of referencing each other directly
- Changes in communication affect only the mediator, not all objects
Example: In a chat application, users send messages through a chat mediator rather than communicating with each user directly.
43. What are the key components of the Observer Pattern?
The Observer Pattern defines a one-to-many dependency so that when one object changes state, all its dependents are notified automatically.
- Subject that maintains a list of observers and notifies them
- Observer interface that defines the update method
Example: In a stock price system, the stock acts as the subject and multiple displays act as observers that get updated whenever the price changes.
44. How does the Strategy Pattern support the Open/Closed Principle?
The Strategy Pattern supports the Open/Closed Principle by allowing new behaviors to be added without modifying existing code.
- New strategies can be introduced as new classes
- The context class remains unchanged
Example: A payment system can add new payment methods like UPI or crypto by creating new strategy classes, without changing the checkout logic.
45. What is the role of context in the Strategy Pattern?
The context in the Strategy Pattern is responsible for holding a reference to a strategy and delegating the behavior to it.
- Selects and uses a strategy without knowing its implementation details
- Allows the strategy to be changed at runtime
Example: In a payment system, the checkout class acts as the context and delegates payment processing to a selected strategy like card, UPI, or net banking.
46. When is the Visitor Pattern useful despite its complexity?
The Visitor Pattern is useful when you need to add new operations to a stable object structure without modifying the existing classes.
- When the object structure rarely changes but operations change frequently
- When related operations need to be grouped together
Example: In a compiler, Visitor is used to perform operations like type checking or code generation on syntax tree nodes without changing the node classes.
47. How does the State Pattern differ from the Strategy Pattern?
The State Pattern changes an object’s behavior based on its internal state, while the Strategy Pattern changes behavior by swapping algorithms externally.
- State transitions are managed inside the context
- Strategy selection is usually done by the client
Example: In a vending machine, behavior changes automatically based on states like no-coin, has-coin, or sold, whereas Strategy would require the client to choose the behavior.
48. Explain how the Command Pattern supports undo and redo functionality.
The Command Pattern encapsulates a request as an object, allowing it to be stored, executed, and reversed later.
- Each command stores the information needed to undo an action
- Commands can be kept in a history stack for undo and redo
Example: In a text editor, typing or deleting text is stored as command objects, enabling undo and redo by reversing or re-executing those commands.
49. How is the Template Method Pattern different from the Strategy Pattern?
The Template Method Pattern defines the skeleton of an algorithm in a base class, while the Strategy Pattern defines interchangeable algorithms.
- Template Method uses inheritance to vary specific steps
- Strategy uses composition to swap behavior at runtime
Example: In a data processing flow, Template Method fixes the steps but allows subclasses to change certain steps, whereas Strategy allows selecting different processing algorithms dynamically.
50. What design pattern is commonly used in logging frameworks and why?
The Singleton Pattern is commonly used in logging frameworks to ensure a single, shared logging instance across the application.
- Prevents multiple logger instances writing inconsistently
- Provides a global access point for logging
Example: A Logger singleton is used by different classes to log messages to the same file or console without creating multiple logger objects.
51. What are anti-patterns, and how do they differ from design patterns?
Anti-patterns are common solutions to recurring problems that are ineffective or harmful, while design patterns are proven best practices for solving recurring design problems.
- Anti-patterns describe what not to do and their negative consequences
- Design patterns provide reusable and effective solutions
Example: Using God Object is an anti-pattern where one class does too much, whereas applying patterns like Facade or Strategy helps distribute responsibilities properly.
52. How do design patterns help in achieving loose coupling?
Design patterns help achieve loose coupling by reducing direct dependencies between classes and relying on abstractions instead.
- Promote interaction through interfaces rather than concrete classes
- Make systems easier to change, extend, and test
Example: In the Observer Pattern, subjects do not depend on concrete observers, allowing observers to be added or removed without changing the subject.
53. Can multiple design patterns be combined in a single solution? Provide examples.
Yes, multiple design patterns are often combined to solve complex design problems more effectively.
- Patterns complement each other by addressing different concerns
- Improves flexibility, scalability, and maintainability
Example: In an MVC architecture, Observer is used for view updates, Strategy for interchangeable business logic, and Factory for creating objects.
54. What factors should be considered before choosing a design pattern?
Choosing a design pattern requires understanding the problem context and long-term impact on the system.
- Nature of the problem, complexity, and change frequency
- Impact on flexibility, performance, and maintainability
Example: Using Singleton may seem simple for shared configuration, but considering testing and scalability needs might lead to choosing Dependency Injection instead.
55. How do design patterns improve testability of code?
Design patterns improve testability by promoting loose coupling and separation of responsibilities.
- Dependencies can be easily mocked or replaced
- Behavior is isolated into smaller, testable components
Example: In the Strategy Pattern, different strategies can be tested independently by injecting mock strategies into the context.
56. How do design patterns evolve with changing software requirements?
Design patterns evolve by being adapted, combined, or replaced as software requirements and constraints change over time.
- Patterns may be refactored or composed to fit new requirements
- Some patterns become unnecessary as frameworks and languages evolve
Example: Early systems used Singleton for shared resources, but modern applications often evolve toward Dependency Injection frameworks as scalability and testability requirements grow.
57. How does the Null Object Pattern help eliminate null checks in code?
The Null Object Pattern replaces null references with a non-functional object that implements the same interface.
- Avoids repetitive null checks and conditional logic
- Makes code safer and easier to read
Example: Instead of checking if a Logger is null, a NullLogger is used that performs no operation when log() is called.
58. What is the difference between static factory methods and the Factory Pattern?
Static factory methods are simple methods that return objects, while the Factory Pattern is a structured design approach for object creation using abstraction.
- Static factory methods are tied to a single class and lack polymorphism
- Factory Pattern supports extensibility through interfaces and subclasses
Example: A static createUser() method returns a User object directly, whereas a Factory Pattern allows creating different User types without changing client code.
59. How does the Iterator Pattern support encapsulation?
The Iterator Pattern supports encapsulation by allowing clients to traverse a collection without exposing its internal structure.
- Collection implementation details remain hidden
- Traversal logic is separated from the collection
Example: In a custom data structure, an iterator lets clients loop through elements without knowing whether the data is stored in an array, list, or tree.
60. When is the Memento Pattern preferred for state management?
The Memento Pattern is preferred when an object’s state needs to be saved and restored without exposing its internal details.
- Preserves encapsulation while supporting state rollback
- Useful for undo or snapshot functionality
Example: In a text editor, the document state is saved as mementos so the user can undo changes without directly accessing internal fields.