Design patterns
Design patterns are reusable solutions to common software design problems that have been proven to work well over time. They are general solutions that can be adapted to a specific context or problem, and they provide a way to capture and communicate knowledge and best practices in software design.
Design patterns can be classified into several categories based on their purpose and scope. Some common categories include:
- Creational patterns: These patterns provide a way to create objects in a flexible and reusable way. Examples of creational patterns include Singleton, Factory Method, and Abstract Factory.
- Structural patterns: These patterns provide a way to organize objects into larger structures, and to define the relationships between objects. Examples of structural patterns include Adapter, Decorator, and Facade.
- Behavioral patterns: These patterns provide a way to manage the interactions and responsibilities of objects, and to define how they communicate and collaborate with each other. Examples of behavioral patterns include Observer, Strategy, and Template Method.
Design patterns are not a silver bullet for solving all software design problems. They are not a one-size-fits-all solution, and they should not be blindly applied without considering the context and requirements of a specific problem. However, they provide a valuable set of tools and best practices that can be used to improve the quality, maintainability, and scalability of software applications.
By using design patterns effectively, you can create more modular, reusable, and maintainable code that can adapt to changing requirements and scale to meet the needs of a growing user base.
Creational Patterns
Creational design patterns are a category of design patterns that provide a way to create objects in a flexible and reusable way. They allow you to create objects without exposing the creation logic to the client, and they provide a way to decouple the creation of objects from their use.
Creational design patterns can be classified into several subcategories based on their purpose and scope. Some common subcategories include:
- Singleton Pattern: This pattern ensures that a class has only one instance, and provides a global point of access to that instance.
- Factory Method Pattern: This pattern provides a way to create objects without specifying their exact class. It defines a separate method for creating objects, which can be overridden in subclasses to create different types of objects.
- Abstract Factory Pattern: This pattern provides a way to create families of related objects without specifying their concrete classes. It defines an interface for creating related objects, which can be implemented by different factories to create different families of related objects.
- Builder Pattern: This pattern provides a way to create complex objects by separating the construction of the object from its representation. It defines a separate builder object that is responsible for constructing the object, which can be used to create different representations of the same object.
- Prototype Pattern: This pattern provides a way to create new objects by cloning existing objects. It defines a prototype object that can be used to create new objects by copying its state.
Creational patterns are useful in many scenarios, such as when you need to create objects with complex initialization logic, when you want to create objects in a flexible and reusable way, or when you want to decouple the creation of objects from their use. By using creational patterns effectively, you can create more modular, maintainable, and extensible code that can adapt to changing requirements and scale to meet the needs of a growing user base.
Singleton
The Singleton pattern is a creational design pattern that ensures that a class has only one instance, and provides a global point of access to that instance. The Singleton pattern is used when you want to ensure that a class has only one instance, and that this instance can be accessed from anywhere in your code.
Here's an example of the Singleton pattern:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
// private constructor to prevent instantiation from outside the
class
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
In this example, the Singleton class is defined with a private constructor to prevent instantiation from outside the class, and a private static field instance that holds the single instance of the class.
The class also provides a public static method getInstance() that returns the single instance of the class. If the instance field is null, the method creates a new instance of the Singleton class and returns it. If the instance field is not null, the method simply returns the existing instance.
The Singleton pattern provides several benefits, including:
- Controlled access to the single instance: The Singleton pattern provides a single point of access to the instance of the class, which allows you to control how the instance is accessed and used in your code.
- Global access to the single instance: The Singleton pattern provides a global point of access to the instance of the class, which allows you to access the instance from anywhere in your code.
- Flexibility and extensibility: The Singleton pattern allows you to define and use a single instance of a class, while still allowing you to extend and modify the behavior of the class in the future.
However, the Singleton pattern also has some potential drawbacks, including:
- Difficulty in testing: The Singleton pattern can be difficult to test, as it creates a dependency on a global state.
- Concurrency issues: The Singleton pattern can have concurrency issues if multiple threads try to access the instance of the class simultaneously.
Factory Method
The Factory Method pattern is a creational design pattern that provides a way to create objects without specifying their exact class. It defines a separate method for creating objects, which can be overridden in subclasses to create different types of objects.
The Factory Method pattern is used when you want to provide a way to create objects in a flexible and extensible way, without requiring the client to know the exact type of the object being created.
Here's an example of the Factory Method pattern:
public abstract class Animal {
public abstract String makeSound();
}
public class Dog extends Animal {
public String makeSound() {
return "Woof!";
}
}
public class Cat extends Animal {
public String makeSound() {
return "Meow!";
}
}
public abstract class AnimalFactory {
public abstract Animal createAnimal();
}
public class DogFactory extends AnimalFactory {
public Animal createAnimal() {
return new Dog();
}
}
public class CatFactory extends AnimalFactory {
public Animal createAnimal() {
return new Cat();
}
}
In this example, the Animal class is an abstract class that defines a single abstract method makeSound(). The Dog and Cat classes are concrete implementations of the Animal class that provide different implementations of the makeSound() method.
The AnimalFactory class is an abstract class that defines a single abstract method createAnimal(). The DogFactory and CatFactory classes are concrete implementations of the AnimalFactory class that provide different implementations of the createAnimal() method.
The createAnimal() method is responsible for creating a new instance of an Animal subclass, but the exact type of the Animal is not specified in the method. Instead, the createAnimal() method is overridden in the DogFactory and CatFactory classes to create new instances of the Dog and Cat classes, respectively.
The Factory Method pattern provides several benefits, including:
- Flexible and extensible: The Factory Method pattern allows you to create objects in a flexible and extensible way, without requiring the client to know the exact type of the object being created.
- Encapsulation: The Factory Method pattern encapsulates the creation of objects in a separate class, which makes it easier to modify the creation logic without affecting the client code.
- Simplified client code: The Factory Method pattern simplifies the client code by removing the need to know the exact type of the object being created.
However, the Factory Method pattern can also have some potential drawbacks, including:
- Complexity: The Factory Method pattern can add complexity to the code by introducing additional classes and interfaces.
- Overhead: The Factory Method pattern can have some overhead, as it requires additional classes and interfaces to be defined and implemented.
Abstract Factory
The Abstract Factory pattern is a creational design pattern that provides a way to create families of related objects without specifying their concrete classes. It defines an interface for creating related objects, which can be implemented by different factories to create different families of related objects.
The Abstract Factory pattern is used when you want to create a family of related objects that have a common theme, such as a set of UI components for a specific platform or a set of products in a specific product line.
Here's an example of the Abstract Factory pattern:
public interface Animal {
String getAnimal();
String makeSound();
}
public class Dog implements Animal {
public String getAnimal() {
return "Dog";
}
public String makeSound() {
return "Woof!";
}
}
public class Cat implements Animal {
public String getAnimal() {
return "Cat";
}
public String makeSound() {
return "Meow!";
}
}
public interface AnimalFactory {
Animal createAnimal();
}
public class DogFactory implements AnimalFactory {
public Animal createAnimal() {
return new Dog();
}
}
public class CatFactory implements AnimalFactory {
public Animal createAnimal() {
return new Cat();
}
}
public class AnimalFactoryProducer {
public static AnimalFactory getFactory(boolean isDogFactory) {
if (isDogFactory) {
return new DogFactory();
} else {
return new CatFactory();
}
}
}
In this example, the Animal interface defines two methods getAnimal() and makeSound(), which are implemented by the Dog and Cat classes. The Dog and Cat classes are concrete implementations of the Animal interface that provide different implementations of the getAnimal() and makeSound() methods.
The AnimalFactory interface defines a single method createAnimal(), which is implemented by the DogFactory and CatFactory classes. The DogFactory and CatFactory classes are concrete implementations of the AnimalFactory interface that provide different implementations of the createAnimal() method.
The AnimalFactoryProducer class is responsible for creating instances of the DogFactory and CatFactory classes, depending on a boolean parameter that is passed to the getFactory() method. If the parameter is true, the getFactory() method creates a new instance of the DogFactory class. If the parameter is false, the getFactory() method creates a new instance of the CatFactory class.
The Abstract Factory pattern provides several benefits, including:
- Encapsulation: The Abstract Factory pattern encapsulates the creation of related objects in a separate class, which makes it easier to modify the creation logic without affecting the client code.
- Flexibility and extensibility: The Abstract Factory pattern allows you to create families of related objects in a flexible and extensible way, by defining a common interface for creating related objects.
- Simplified client code: The Abstract Factory pattern simplifies the client code by removing the need to know the exact type of the object being created.
However, the Abstract Factory pattern can also have some potential drawbacks, including:
- Complexity: The Abstract Factory pattern can add complexity to the code by introducing additional classes and interfaces.
- Overhead: The Abstract Factory pattern can have some overhead, as it requires additional classes and interfaces to be defined and implemented.
It's worth noting that the Abstract Factory pattern is closely related to the Factory Method pattern. The main difference between the two patterns is that the Factory Method pattern provides a way to create a single type of object, while the Abstract Factory pattern provides a way to create families of related objects.
In general, you should use the Factory Method pattern when you need to create a single type of object, and the Abstract Factory pattern when you need to create families of related objects. However, in some cases, you may need to use both patterns together to create a more complex object hierarchy.
Builder Pattern
The Builder pattern is a creational design pattern that provides a way to create complex objects step by step. It separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
The Builder pattern is used when you need to create complex objects that require a large number of parameters, or when you need to create different representations of the same object.
Here's an example of the Builder pattern:
public class Car {
private String make;
private String model;
private int year;
private int numDoors;
private int numSeats;
private Car(CarBuilder builder) {
this.make = builder.make;
this.model = builder.model;
this.year = builder.year;
this.numDoors = builder.numDoors;
this.numSeats = builder.numSeats;
}
public static class CarBuilder {
private String make;
private String model;
private int year;
private int numDoors;
private int numSeats;
public CarBuilder(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
public CarBuilder numDoors(int numDoors) {
this.numDoors = numDoors;
return this;
}
public CarBuilder numSeats(int numSeats) {
this.numSeats = numSeats;
return this;
}
public Car build() {
return new Car(this);
}
}
}
In this example, the Car class defines the complex object that we want to create. The class has several fields that define the properties of the car.
The CarBuilder class is a nested class that provides a way to create the Car object step by step. The CarBuilder class has several methods that allow you to set the properties of the car, such as the number of doors and the number of seats.
The CarBuilder class returns itself after each method call, which allows you to chain the method calls together to set multiple properties at once. The CarBuilder class also has a build() method that creates the Car object and returns it.
To create a new Car object, you can use the following code:
Car car = new Car.CarBuilder("Toyota", "Camry", 2021)
.numDoors(4)
.numSeats(5)
.build();
In this example, the CarBuilder class is used to create a new Car object with the make "Toyota", model "Camry", year 2021, four doors, and five seats.
The Builder pattern provides several benefits, including:
- Flexibility and extensibility: The Builder pattern allows you to create complex objects in a flexible and extensible way, by separating the construction of the object from its representation.
- Clear and readable code: The Builder pattern can make the code clearer and more readable, by providing a step-by-step process for creating complex objects.
- Encapsulation: The Builder pattern encapsulates the construction of complex objects in a separate class, which makes it easier to modify the construction process without affecting the client code.
However, the Builder pattern can also have some potential drawbacks, including:
- Overhead: The Builder pattern can have some overhead, as it requires the creation of a separate builder class and multiple method calls to set the properties of the object.
- Complexity: The Builder pattern can add complexity to the code by introducing a separate builder class and a step-by-step process for creating objects.
Prototype Pattern
The Prototype pattern is a creational design pattern that provides a way to create new objects by cloning or copying existing objects, rather than creating them from scratch.
The Prototype pattern is used when you need to create new objects that are similar to existing objects, or when you need to create new objects with a large number of properties or complex dependencies.
Here's an example of the Prototype pattern:
public abstract class Shape implements Cloneable {
private String id;
protected String type;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public abstract void draw();
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
public class Circle extends Shape {
public Circle() {
type = "Circle";
}
public void draw() {
System.out.println("Inside Circle::draw()
method.");
}
}
public class Square extends Shape {
public Square() {
type = "Square";
}
public void draw() {
System.out.println("Inside Square::draw()
method.");
}
}
public class ShapeCache {
private static Map<String, Shape> shapeMap = new
HashMap<String, Shape>();
public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone();
}
public static void loadCache() {
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(),circle);
Square square = new Square();
square.setId("2");
shapeMap.put(square.getId(),square);
}
}
In this example, the Shape class is an abstract class that defines the prototype for different types of shapes. The Shape class has an ID and a type, and provides an abstract draw() method that is implemented by concrete shape classes.
The Circle and Square classes are concrete shape classes that extend the Shape class. They provide a default constructor that sets the type field to the appropriate value.
The ShapeCache class provides a way to create and cache different shapes. It has a Map that maps shape IDs to shape objects. The loadCache() method is used to pre-populate the cache with different types of shapes, while the getShape() method is used to retrieve a shape from the cache by ID and return a cloned copy of the shape.
To use the Prototype pattern, you can use the following code:
ShapeCache.loadCache();
Shape clonedShape1 = ShapeCache.getShape("1");
System.out.println("Shape : " + clonedShape1.getType());
Shape clonedShape2 = ShapeCache.getShape("2");
System.out.println("Shape : " + clonedShape2.getType());
In this example, the ShapeCache class is used to create and cache different types of shapes. The loadCache() method is called to pre-populate the cache with a Circle and a Square object. The getShape() method is used to retrieve a shape from the cache by ID and return a cloned copy of the shape.
The Prototype pattern provides several benefits, including:
- Flexibility and extensibility: The Prototype pattern allows you to create new objects by cloning or copying existing objects, rather than creating them from scratch. This makes it easy to create new objects with similar properties or dependencies.
- Improved performance: The Prototype pattern can improve performance by reducing the number of object creation operations, since new objects are created by copying existing objects rather than creating them from scratch.
- Encapsulation: The Prototype pattern encapsulates the creation of new objects in a separate class, which makes it easier to modify the creation process without affecting the client code.
However, the Prototype pattern can also have some potential drawbacks, including:
- Complexity: The Prototype pattern can add complexity to the code by introducing a separate prototype class and a cloning mechanism.
- Security: The Prototype pattern can introduce security risks if the cloned objects are not properly sanitized, since they can contain sensitive data or code.
Structural Patterns
Structural design patterns are a category of design patterns that provide a way to organize objects in a flexible and reusable way. They allow you to define relationships between objects, and to compose objects into larger structures while still maintaining flexibility and modularity.
Structural patterns can be classified into several subcategories based on their purpose and scope. Some common subcategories include:
- Adapter Pattern: This pattern provides a way to adapt an existing class to meet the requirements of a new interface. It defines a separate adapter class that wraps an existing class and provides a new interface that is compatible with the requirements of the client code.
- Bridge Pattern: This pattern provides a way to separate the abstraction of an object from its implementation. It defines two separate hierarchies, one for the abstraction and one for the implementation, and provides a way to link the two hierarchies together.
- Composite Pattern: This pattern provides a way to compose objects into a tree-like structure, where individual objects and groups of objects are treated in a uniform way. It defines a separate component interface that is implemented by both individual objects and groups of objects.
- Decorator Pattern: This pattern provides a way to add new functionality to an existing object without modifying its structure. It defines a separate decorator class that wraps an existing object and provides additional behavior without modifying the original object.
- Facade Pattern: This pattern provides a way to simplify a complex system by providing a simplified interface to the client code. It defines a separate facade class that provides a simple interface to a complex system of classes and objects.
Structural patterns are useful in many scenarios, such as when you need to organize objects into a more complex structure, when you want to provide a simplified interface to a complex system, or when you want to add new behavior to an existing object without modifying its structure. By using structural patterns effectively, you can create more modular, maintainable, and extensible code that can adapt to changing requirements and scale to meet the needs of a growing user base.
Adapter
The Adapter pattern is a structural design pattern that allows two incompatible interfaces to work together. It converts the interface of a class into another interface that the client expects, allowing objects with incompatible interfaces to collaborate.
The Adapter pattern is used when you need to use an existing class that doesn't meet the requirements of the client code, or when you want to reuse existing code that can't be modified.
Here's an example of the Adapter pattern:
public interface MediaPlayer {
public void play(String audioType, String fileName);
}
public interface AdvancedMediaPlayer {
public void playVlc(String fileName);
public void playMp4(String fileName);
}
public class VlcPlayer implements AdvancedMediaPlayer {
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: "+
fileName);
}
public void playMp4(String fileName) {
// do nothing
}
}
public class Mp4Player implements AdvancedMediaPlayer{
public void playVlc(String fileName) {
// do nothing
}
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: "+
fileName);
}
}
public class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType){
if(audioType.equalsIgnoreCase("vlc") ){
advancedMusicPlayer = new
VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer = new Mp4Player();
}
}
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")){
advancedMusicPlayer.playVlc(fileName);
}else if(audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer.playMp4(fileName);
}
}
}
public class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("mp3")){
System.out.println("Playing mp3 file. Name:
"+
fileName);
}else if(audioType.equalsIgnoreCase("vlc") ||
audioType.equalsIgnoreCase("mp4")){
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
}else{
System.out.println("Invalid media. "+
audioType + " format not supported");
}
}
}
In this example, the MediaPlayer interface defines the interface that the client code expects. The AdvancedMediaPlayer interface defines a more advanced interface that some existing music players implement.
The VlcPlayer and Mp4Player classes are existing music players that implement the AdvancedMediaPlayer interface.
The MediaAdapter class is an adapter class that converts the AdvancedMediaPlayer interface of the VlcPlayer and Mp4Player classes into the MediaPlayer interface that the client code expects.
The AudioPlayer class is a music player that can play different types of music files. It checks the type of the music file and uses either the MediaPlayer interface or the MediaAdapter class to play the file.
To use the Adapter pattern, you can use the following code:
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
In this example, the AudioPlayer class is used to play different types of music files, including MP3, MP4, and VLC files. The play() method of the AudioPlayer class checks the type of the file and uses either the MediaPlayer interface or the MediaAdapter class to play the file.
The Adapter pattern provides several benefits, including:
- Reusability: The Adapter pattern allows you to reuse existing code that can't be modified, by converting the existing interface into an interface that the client code expects.
- Flexibility: The Adapter pattern allows you to use different types of classes with different interfaces, by providing a way to convert incompatible interfaces into compatible ones.
- Maintainability: The Adapter pattern encapsulates the conversion logic in a separate class, making it easier to modify the conversion process without affecting the client code.
However, the Adapter pattern can also have some potential drawbacks, including:
- Complexity: The Adapter pattern can add complexity to the code by introducing an adapter class and a conversion mechanism.
- Performance: The Adapter pattern can introduce performance overhead if the conversion process is computationally expensive.
Bridge
The Bridge pattern is a structural design pattern that decouples an abstraction from its implementation, allowing both to vary independently. It provides a way to separate a complex class into two separate hierarchies, one for the abstraction and one for the implementation.
The Bridge pattern is used when you need to decouple an abstraction from its implementation, or when you need to provide a way to change the implementation of an abstraction at runtime.
Here's an example of the Bridge pattern:
public interface DrawAPI {
public void drawCircle(int radius, int x, int y);
}
public class RedCircle implements DrawAPI {
public void drawCircle(int radius, int x, int y) {
System.out.println("Drawing Circle[ color: red, radius:
" + radius + ", x: " + x + ", " + y + "]");
}
}
public class GreenCircle implements DrawAPI {
public void drawCircle(int radius, int x, int y) {
System.out.println("Drawing Circle[ color: green, radius:
" + radius + ", x: " + x + ", " + y + "]");
}
}
public abstract class Shape {
protected DrawAPI drawAPI;
protected Shape(DrawAPI drawAPI){
this.drawAPI = drawAPI;
}
public abstract void
draw();
}
public class Circle extends Shape {
private int x, y, radius;
public Circle(int x, int y, int radius, DrawAPI drawAPI) {
super(drawAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
public void draw() {
drawAPI.drawCircle(radius,x,y);
}
}
In this example, the DrawAPI interface defines the implementation of a shape drawing operation. The RedCircle and GreenCircle classes are concrete implementations of the DrawAPI interface.
The Shape abstract class defines the abstraction of a shape, and has a reference to an implementation of the DrawAPI interface. The Circle class is a concrete implementation of the Shape abstract class, and has an x, y, and radius parameter, as well as a reference to an implementation of the DrawAPI interface.
To use the Bridge pattern, you can use the following code:
Shape redCircle = new Circle(100,100, 10, new RedCircle());
Shape greenCircle = new Circle(100,100, 10, new GreenCircle());
redCircle.draw();
greenCircle.draw();
In this example, the Circle class is used to draw red and green circles with different radii and colors. The draw() method of the Circle class uses the implementation of the DrawAPI interface to draw the circle.
The Bridge pattern provides several benefits, including:
- Decoupling: The Bridge pattern decouples an abstraction from its implementation, allowing both to vary independently. This makes it easier to modify the implementation without affecting the abstraction, or to modify the abstraction without affecting the implementation.
- Extensibility: The Bridge pattern provides a way to extend the abstraction and implementation hierarchies independently, allowing you to add new abstractions and implementations without affecting the existing code.
- Maintainability: The Bridge pattern provides a way to encapsulate the implementation details in a separate class, making it easier to maintain and modify the code.
However, the Bridge pattern can also have some potential drawbacks, including:
- Complexity: The Bridge pattern can add complexity to the code by introducing an additional layer of abstraction and indirection.
- Performance: The Bridge pattern can introduce performance overhead if the abstraction and implementation hierarchies are too large or if the conversion process between them is computationally expensive.
Composite
The Composite pattern is a structural design pattern that allows you to treat a group of objects as a single object. It provides a way to represent part-whole hierarchies of objects, and allows you to work with individual objects and groups of objects in a uniform way.
The Composite pattern is used when you need to represent complex object hierarchies that can be composed of individual objects or groups of objects, or when you need to treat a group of objects as a single object.
Here's an example of the Composite pattern:
public interface Employee {
public void add(Employee employee);
public void remove(Employee employee);
public Employee getChild(int i);
public String getName();
public double getSalary();
public void print();
}
public class Developer implements Employee {
private String name;
private double salary;
public Developer(String name,double salary){
this.name = name;
this.salary = salary;
}
public void add(Employee employee) {
// do nothing
}
public Employee getChild(int i) {
return null;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void print() {
System.out.println("Name: " + getName() + ",
Salary: " + getSalary());
}
public void remove(Employee employee) {
// do nothing
}
}
public class Manager implements Employee {
private String name;
private double salary;
private List<Employee> employees = new
ArrayList<Employee>();
public Manager(String name,double salary){
this.name = name;
this.salary = salary;
}
public void add(Employee employee) {
employees.add(employee);
}
public Employee getChild(int i) {
return employees.get(i);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void print() {
System.out.println("Name: " + getName() + ",
Salary: " + getSalary());
for (Employee employee : employees) {
employee.print();
}
}
public void remove(Employee employee) {
employees.remove(employee);
}
}
In this example, the Employee interface defines the behavior of an employee, and has methods for adding and removing employees from a group, getting a child employee by index, getting the name and salary of the employee, and printing the employee details.
The Developer class is a leaf node in the object hierarchy, and represents an individual employee.
The Manager class is a composite node in the object hierarchy, and represents a group of employees. It has a list of child employees and implements the methods of the Employee interface.
To use the Composite pattern, you can use the following code:
Employee dev1 = new Developer("John", 10000);
Employee dev2 = new Developer("David", 15000);
Employee manager1 = new Manager("Daniel", 25000);
manager1.add(dev1);
manager1.add(dev2);
Employee dev3 = new Developer("Michael", 20000);
Employee dev4 = new Developer("Joe", 30000);
Employee manager2 = new Manager("Chris", 35000);
manager2.add(dev3);
manager2.add(dev4);
manager2.add(manager1);
manager2.print();
In this example, several developers and managers are created, and added to each other to form a hierarchy. The print() method of the top-level manager is called, which prints the details of the entire employee hierarchy.
The Composite pattern provides several benefits, including:
- Flexibility: The Composite pattern provides a way to represent part-whole hierarchies of objects, and allows you to work with individual objects and groups of objects in a uniform way. This makes it easier to represent complex object hierarchies, and to add or remove objects from the hierarchy.
- Extensibility: The Composite pattern provides a way to extend the object hierarchy by adding new types of objects, without affecting the existing code.
- Encapsulation: The Composite pattern encapsulates the details of the object hierarchy in a single class, making it easier to maintain and modify the code.
However, the Composite pattern can also have some potential drawbacks, including:
- Performance: The Composite pattern can introduce performance overhead if the object hierarchy is too large or if the operations on the hierarchy are computationally expensive.
- Complexity: The Composite pattern can add complexity to the code by introducing a hierarchy of objects and a uniform interface for working with the objects.
Decorator
The Decorator pattern is a structural design pattern that allows you to add new behavior to an object without changing its class. It provides a way to wrap an object with one or more decorators that add new behavior to the object.
The Decorator pattern is used when you need to add new behavior to an object, but you can't modify its class. It is also used when you need to add behavior to an object dynamically at runtime.
Here's an example of the Decorator pattern:
public interface Shape {
void draw();
}
public class Circle implements Shape {
public void draw() {
System.out.println("Shape: Circle");
}
}
public abstract class ShapeDecorator implements Shape {
protected Shape decoratedShape;
public ShapeDecorator(Shape decoratedShape){
this.decoratedShape = decoratedShape;
}
public void draw(){
decoratedShape.draw();
}
}
public class RedShapeDecorator extends ShapeDecorator {
public RedShapeDecorator(Shape decoratedShape) {
super(decoratedShape);
}
@Override
public void draw() {
decoratedShape.draw();
setRedBorder(decoratedShape);
}
private void setRedBorder(Shape decoratedShape){
System.out.println("Border Color: Red");
}
}
In this example, the Shape interface defines the behavior of a shape, and has a method for drawing the shape. The Circle class is a concrete implementation of the Shape interface, and has a method for drawing a circle.
The ShapeDecorator abstract class is a decorator class that implements the Shape interface, and has a reference to an object of the Shape interface. The RedShapeDecorator class is a concrete implementation of the ShapeDecorator class, and adds a red border to the decorated shape.
To use the Decorator pattern, you can use the following code:
Shape circle = new Circle();
Shape redCircle = new RedShapeDecorator(new Circle());
Shape redRectangle = new RedShapeDecorator(new Rectangle());
circle.draw();
redCircle.draw();
redRectangle.draw();
In this example, a circle, a red circle, and a red rectangle are created. The draw() method of each object is called, which draws the shape and, in the case of the red shapes, adds a red border to the shape.
The Decorator pattern provides several benefits, including:
- Extensibility: The Decorator pattern provides a way to add new behavior to an object without changing its class. This makes it easy to add new functionality to an object, and to create new decorators that add even more behavior.
- Flexibility: The Decorator pattern allows you to add behavior to an object dynamically at runtime. This makes it easy to modify the behavior of an object without affecting its existing functionality.
- Encapsulation: The Decorator pattern encapsulates the details of the added behavior in a separate decorator class, making it easier to maintain and modify the code.
However, the Decorator pattern can also have some potential drawbacks, including:
- Complexity: The Decorator pattern can add complexity to the code by introducing multiple decorator classes and a complex hierarchy of objects.
- Performance: The Decorator pattern can introduce performance overhead if the decorators are too numerous or if the added behavior is computationally expensive.
Facade
The Facade pattern is a structural design pattern that provides a simple interface to a complex system of classes, libraries, and APIs. It provides a way to simplify the complexity of a system by providing a unified interface that abstracts away the details of the underlying system.
The Facade pattern is used when you need to simplify the complexity of a system by providing a simple interface that hides the details of the underlying implementation. It is also used when you need to integrate multiple subsystems into a single system with a simple interface.
Here's an example of the Facade pattern:
public class CPU {
public void freeze() { ... }
public void jump(long position) { ... }
public void execute() { ... }
}
public class Memory {
public void load(long position, byte[] data) { ... }
}
public class HardDrive {
public byte[] read(long lba, int size) { ... }
}
public class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void start() {
cpu.freeze();
memory.load(0, hardDrive.read(0, 1024));
cpu.jump(0);
cpu.execute();
}
}
In this example, the CPU, Memory, and HardDrive classes represent the components of a computer system. The ComputerFacade class is a facade that provides a simple interface to the computer system. It has references to the CPU, Memory, and HardDrive objects, and provides a start() method that starts the computer system by freezing the CPU, loading the memory with data from the hard drive, jumping to the start position, and executing the program.
To use the Facade pattern, you can use the following code:
ComputerFacade computer = new ComputerFacade();
computer.start();
In this example, a ComputerFacade object is created, and the start() method is called to start the computer system. The details of the underlying implementation are hidden behind the simple start() method.
The Facade pattern provides several benefits, including:
- Simplicity: The Facade pattern provides a simple interface to a complex system, making it easier to use and understand.
- Encapsulation: The Facade pattern encapsulates the details of the underlying implementation in a single class, making it easier to maintain and modify the code.
- Flexibility: The Facade pattern allows you to change the underlying implementation of a system without affecting the client code.
However, the Facade pattern can also have some potential drawbacks, including:
- Limited functionality: The Facade pattern provides a simplified interface to a system, but it may not expose all of the functionality of the underlying system.
- Performance: The Facade pattern can introduce performance overhead if the underlying system is computationally expensive and the facade methods are called frequently.
Behavioral Patterns
Behavioral patterns are a set of design patterns that focus on the communication and interaction between objects in a system. They provide solutions for common problems related to the interaction and communication between objects, and help to make the code more modular, reusable, and maintainable.
The key benefit of behavioral patterns is that they provide a way to manage the communication and interaction between objects in a system. This makes it easier to create complex systems that are flexible, scalable, and easy to maintain.
Some examples of behavioral patterns include:
- Iterator Pattern: This pattern provides a way to access the elements of an object without exposing its underlying representation.
- Observer Pattern: This pattern defines a one-to-many relationship between objects, where changes in one object are automatically propagated to other objects that depend on it.
- Strategy Pattern: This pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently from clients that use it.
- Command Pattern: This pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
- Template Method Pattern: This pattern defines the skeleton of an algorithm in a superclass, but lets subclasses override specific steps of the algorithm without changing its structure.
These patterns, and others like them, provide a way to manage the communication and interaction between objects in a system. They can help to make the code more modular, reusable, and maintainable, and they are widely used in software development to create complex systems that are flexible, scalable, and easy to maintain.
Iterator Pattern
The Iterator pattern is a behavioral design pattern that provides a way to access the elements of an object without exposing its underlying representation. It separates the way an object is traversed from the actual traversal logic, allowing for more flexibility and modularity in the code.
The Iterator pattern is used when you need to provide a way to access the elements of an object in a sequential manner, without exposing the underlying structure of the object. It is also useful when you need to provide a common interface for traversing different types of collections, without the need to know their specific implementation details.
Here's an example of the Iterator pattern:
public interface Iterator {
public boolean hasNext();
public Object next();
}
public interface Container {
public Iterator getIterator();
}
public class NameRepository implements Container {
public String names[] = {"John", "Mary",
"Peter", "Lucy"};
public Iterator getIterator() {
return new NameIterator();
}
private class NameIterator implements Iterator {
int index;
public boolean hasNext() {
if(index < names.length){
return true;
}
return false;
}
public Object next() {
if(this.hasNext()){
return names[index++];
}
return null;
}
}
}
In this example, the Iterator interface defines the hasNext() and next() methods that will be implemented by the concrete iterator classes. The Container interface defines the getIterator() method that will return an iterator for the underlying collection.
The NameRepository class represents the object whose elements need to be traversed, and it has a reference to the NameIterator class, which implements the Iterator interface. The NameIterator class represents the actual iterator and keeps track of the current index position in the names array.
To use the Iterator pattern, you can use the following code:
NameRepository namesRepository = new NameRepository();
for(Iterator iter = namesRepository.getIterator(); iter.hasNext();){
String name = (String)iter.next();
System.out.println("Name : " + name);
}
In this example, a NameRepository object is created and its elements are traversed using the Iterator interface. The hasNext() and next() methods are called on the iterator object to traverse the names array, and the output is displayed to the console.
The Iterator pattern provides several benefits, including:
- Encapsulation: The Iterator pattern encapsulates the traversal logic and provides a common interface for accessing the elements of an object, without exposing its underlying structure.
- Flexibility: The Iterator pattern provides a way to traverse different types of collections, without the need to know their specific implementation details. This makes it easy to switch between different collection types without affecting the client code.
- Reusability: The Iterator pattern allows you to reuse the same iterator implementation in different contexts, making it easy to create flexible and modular code.
However, the Iterator pattern can also have some potential drawbacks, including:
- Overhead: The Iterator pattern can introduce performance overhead if the iterator objects are created frequently or the traversal logic is computationally expensive.
- Complexity: The Iterator pattern can introduce complexity to the code, as it requires the creation of additional classes to represent the iterator objects.
Observer
The Observer pattern is a behavioral design pattern that allows an object to notify other objects when its state changes. It provides a way to establish one-to-many relationships between objects, where a single subject (the object being observed) can notify multiple observers (the objects interested in its state).
The Observer pattern is used when you need to establish a communication between objects in a decoupled way, where changes in one object need to be propagated to other objects without the need to know which objects need to be updated. The Observer pattern is also useful when you have a need for a dynamic subscription model where multiple objects can subscribe to the events generated by another object.
Here's an example of the Observer pattern:
public interface Observer {
void update();
}
public class Subject {
private List<Observer> observers = new
ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public void attach(Observer observer){
observers.add(observer);
}
public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}
public class BinaryObserver implements Observer {
private Subject subject;
public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
public void update() {
System.out.println( "Binary String: " +
Integer.toBinaryString( subject.getState() ) );
}
}
public class OctalObserver implements Observer {
private Subject subject;
public OctalObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
public void update() {
System.out.println( "Octal String: " +
Integer.toOctalString( subject.getState() ) );
}
}
public class HexaObserver implements Observer {
private Subject subject;
public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
public void update() {
System.out.println( "Hex String: " +
Integer.toHexString( subject.getState() ).toUpperCase() );
}
}
In this example, the Observer interface defines the update() method that will be called when the state of the subject changes. The Subject class represents the object being observed and has a list of observers that it notifies when its state changes.
The BinaryObserver, OctalObserver, and HexaObserver classes represent the observers and implement the Observer interface. Each observer attaches to the Subject object to receive updates, and when notified by the Subject, they convert the state of the subject to binary, octal, and hexadecimal format, respectively, and output it to the console.
To use the Observer pattern, you can use the following code:
Subject subject = new Subject();
new HexaObserver(subject);
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change:
15");
subject.setState(15);
System.out.println("Second state change:
10");
subject.setState(10);
In this example, a Subject object is created and three observers are attached to it. The setState() method is called twice to change the state of the Subject object. After each state change, the observers are notified and the output is displayed to the console.
The Observer pattern provides several benefits, including:
- Decoupling: The Observer pattern decouples the subject and the observers, making it easy to change either without affecting the other.
- Scalability: The Observer pattern provides a way to add and remove observers dynamically, making it easy to scale the system to meet changing requirements.
- Reusability: The Observer pattern allows observers to be reused with different subjects, making it easy to create flexible and modular code.
However, the Observer pattern can also have some potential drawbacks, including:
- Overhead: The Observer pattern can introduce performance overhead if there are a large number of observers and/or the subject is updated frequently.
- Memory leaks: The Observer pattern can cause memory leaks if observers are not detached from the subject when they are no longer needed.
Strategy
The Strategy pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. It provides a way to vary the behavior of an object at runtime by selecting an algorithm from a family of algorithms.
The Strategy pattern is used when you need to change the behavior of an object dynamically, without changing its interface. It is also useful when you have a need to provide different implementations of the same functionality, and you want to be able to switch between these implementations at runtime.
Here's an example of the Strategy pattern:
public interface Strategy {
int doOperation(int num1, int num2);
}
public class OperationAdd implements Strategy {
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract implements Strategy {
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMultiply implements Strategy {
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
In this example, the Strategy interface defines the doOperation() method that will be implemented by the concrete strategy classes. The OperationAdd, OperationSubtract, and OperationMultiply classes represent the concrete strategies and implement the Strategy interface.
The Context class represents the object whose behavior needs to be varied at runtime. It has a reference to the Strategy object and provides a executeStrategy() method that takes two numbers as input and delegates the operation to the current Strategy object.
To use the Strategy pattern, you can use the following code:
Context context = new Context(new
OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(new
OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context = new Context(new
OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
In this example, a Context object is created with a reference to an OperationAdd object. The executeStrategy() method is called with two numbers as input, and the output is displayed to the console. The same process is repeated with the OperationSubtract and OperationMultiply objects to demonstrate the ability to change the behavior of the Context object at runtime.
The Strategy pattern provides several benefits, including:
- Flexibility: The Strategy pattern provides a way to change the behavior of an object at runtime, without changing its interface. This makes it easy to add new strategies or switch between existing ones without affecting the client code.
- Reusability: The Strategy pattern allows you to reuse the same strategy implementation in different contexts, making it easy to create flexible and modular code.
- Testability: The Strategy pattern makes it easy to test each strategy implementation independently, as they are encapsulated in separate classes.
However, the Strategy pattern can also have some potential drawbacks, including:
- Complexity: The Strategy pattern can introduce complexity to the code, as it requires the creation of additional classes to represent each strategy implementation.
- Overhead: The Strategy pattern can introduce performance overhead if the strategy objects are created frequently or the algorithms are computationally expensive.
Command
The Command pattern is a behavioral design pattern that provides a way to encapsulate a request as an object, thereby allowing for the separation of the requester from the actual request. It provides a way to parameterize clients with different requests, queue or log requests, and support undoable operations.
The Command pattern is used when you need to decouple the object that issues a request from the object that actually performs the request. It is also useful when you need to support undoable operations or when you need to queue or log requests for later processing.
Here's an example of the Command pattern:
public interface Command {
public void execute();
}
public class Light {
public void turnOn() {
System.out.println("Light is on");
}
public void turnOff() {
System.out.println("Light is off");
}
}
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute(){
light.turnOn();
}
}
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOff();
}
}
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
In this example, the Command interface defines the execute() method that will be implemented by the concrete command classes. The Light class represents the object that needs to be controlled, and it has turnOn() and turnOff() methods to perform the actual commands.
The LightOnCommand and LightOffCommand classes represent the concrete commands and implement the Command interface. They have a reference to the Light object and call its methods to perform the commands.
The RemoteControl class represents the object that issues the commands, and it has a reference to the current Command object. The setCommand() method is used to set the current command, and the pressButton() method is used to execute the command.
To use the Command pattern, you can use the following code:
Light light = new Light();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
RemoteControl remoteControl = new RemoteControl();
remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton();
remoteControl.setCommand(lightOffCommand);
remoteControl.pressButton();
In this example, a Light object is created, and LightOnCommand and LightOffCommand objects are created with references to the Light object. A RemoteControl object is created, and the current command is set to LightOnCommand. The pressButton() method is called to execute the current command, which turns on the light. The current command is then set to LightOffCommand, and the pressButton() method is called again to execute the current command, which turns off the light.
The Command pattern provides several benefits, including:
- Decoupling: The Command pattern decouples the object that issues a request from the object that actually performs the request, making it easy to add new commands or switch between existing ones without affecting the client code.
- Undo/Redo: The Command pattern provides a way to support undoable operations by keeping a record of the commands that have been executed, making it easy to undo or redo them as needed.
- Logging: The Command pattern provides a way to log commands for later processing or analysis.
However, the Command pattern can also have some potential drawbacks, including:
- Complexity: The Command pattern can introduce complexity to the code, as it requires the creation of additional classes to represent the commands.
- Overhead: The Command pattern can introduce performance overhead if the commands are created frequently or the command objects are large.
- Tight coupling: If the command objects require access to the receiver object's internal state, this can lead to tight coupling between the two objects.
Template Method Pattern
The Template Method pattern is a behavioral design pattern that provides a way to define the skeleton of an algorithm in a base class, while allowing subclasses to provide specific implementations of certain steps in the algorithm. It is used when you want to define an algorithm that can be customized by its subclasses without changing the overall structure of the algorithm.
The Template Method pattern is composed of two main components:
- Abstract Class: This defines the skeleton of the algorithm as a set of abstract methods and concrete methods. The abstract methods represent the steps of the algorithm that must be implemented by the subclasses, while the concrete methods provide default behavior that can be overridden by the subclasses if needed.
- Concrete Class: This extends the abstract class and provides specific implementations of the abstract methods.
Here's an example of the Template Method pattern:
public abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
public final void play(){
initialize();
startPlay();
endPlay();
}
}
public class Cricket extends Game {
@Override
void endPlay() {
System.out.println("Cricket Game Finished!");
}
@Override
void initialize() {
System.out.println("Cricket Game Initialized! Start
playing.");
}
@Override
void startPlay() {
System.out.println("Cricket Game Started. Enjoy the
game!");
}
}
public class Football extends Game {
@Override
void endPlay() {
System.out.println("Football Game Finished!");
}
@Override
void initialize() {
System.out.println("Football Game Initialized! Start
playing.");
}
@Override
void startPlay() {
System.out.println("Football Game Started. Enjoy the
game!");
}
}
In this example, the Game class defines the template method play() that calls the abstract methods initialize(), startPlay(), and endPlay(). The Cricket and Football classes extend the Game class and provide specific implementations of the abstract methods.
To use the Template Method pattern, you can use the following code:
Game game = new Cricket();
game.play();
game = new Football();
game.play();
In this example, a Cricket object and a Football object are created, and their play() methods are called. The play() method calls the abstract methods initialize(), startPlay(), and endPlay() in a specific order, and the output is displayed to the console.
The Template Method pattern provides several benefits, including:
- Code reuse: The Template Method pattern provides a way to reuse the common parts of an algorithm across different subclasses, making it easy to add new variations or change the behavior of the algorithm without affecting the existing code.
- Flexibility: The Template Method pattern provides a way to customize the behavior of an algorithm in a flexible and modular way, without changing the overall structure of the algorithm.
- Abstraction: The Template Method pattern provides a high level of abstraction, making it easy to reason about the algorithm and its variations.
However, the Template Method pattern can also have some potential drawbacks, including:
- Complexity: The Template Method pattern can introduce complexity to the code, especially if the algorithm has many steps or the subclasses need to implement many abstract methods.
- Inflexibility: The Template Method pattern can be inflexible if the overall structure of the algorithm needs to be changed, as this may require changes to the base class and all of its subclasses.