In this lesson, we'll step into the world of the Decorator pattern. The Decorator pattern is a structural design pattern that allows you to dynamically add behavior to an object without altering its structure. This pattern is particularly useful when you want to add responsibilities to objects without subclassing.
In this lesson, you will master:
- The fundamental concept of the Decorator pattern.
- How to implement the Decorator pattern with clear, practical examples.
- The significance of the Decorator pattern in the software design landscape.
Let's see the Decorator Pattern in action through a practical example.
To understand the Decorator pattern, we'll use an example involving different types of cars and decorations (or features) that can be dynamically added to them. In our example, the objective is to create customizable cars with dynamic features such as a sports car and a luxury car.
- Component (Car Interface): This interface defines the core functionality.
- Concrete Component (Basic Car): This is the class that we want to dynamically add features to.
- Decorator: This abstract class wraps a
Car
object and implements theCar
interface. - Concrete Decorators (Sports Car, Luxury Car): These classes extend the
Decorator
class to add specific features to theCar
.
First, we define the Car
interface, which declares the core method for assembling a car.
Java1public interface Car { 2 String assemble(); 3}
In this example, Car
is the component interface that declares the assemble
method. All decorator classes will implement this interface.
Next, we create the BasicCar
class that implements the Car
interface.
Java1public class BasicCar implements Car { 2 @Override 3 public String assemble() { 4 return "Basic Car"; 5 } 6}
Here, BasicCar
provides the base functionality by implementing the assemble
method to return the string "Basic Car."
The CarDecorator
class implements the Car
interface and contains a reference to a Car
object.
Java1public class CarDecorator implements Car { 2 private Car decoratedCar; 3 4 public CarDecorator(Car car) { 5 this.decoratedCar = car; 6 } 7 8 @Override 9 public String assemble() { 10 return decoratedCar.assemble(); 11 } 12}
The CarDecorator
class acts as a wrapper for a Car
object, delegating the call to the assemble
method of the wrapped object.
The concrete decorators extend the CarDecorator
class to add specific features.
Java1public class SportsCar extends CarDecorator { 2 public SportsCar(Car car) { 3 super(car); 4 } 5 6 @Override 7 public String assemble() { 8 return super.assemble() + " + Sports Car Features"; 9 } 10}
In the SportsCar
class, we extend CarDecorator
and add sports car features to the assemble
method.
Java1public class LuxuryCar extends CarDecorator { 2 public LuxuryCar(Car car) { 3 super(car); 4 } 5 6 @Override 7 public String assemble() { 8 return super.assemble() + " + Luxury Car Features"; 9 } 10}
Similarly, the LuxuryCar
class extends CarDecorator
and adds luxury car features to the assemble
method.
Here's how you can use the SportsCar
and LuxuryCar
decorators to add features to a BasicCar
.
Java1public class Main { 2 public static void main(String[] args) { 3 Car sportsCar = new SportsCar(new BasicCar()); 4 System.out.println(sportsCar.assemble()); // Outputs: Basic Car + Sports Car Features 5 6 Car luxuryCar = new LuxuryCar(new BasicCar()); 7 System.out.println(luxuryCar.assemble()); // Outputs: Basic Car + Luxury Car Features 8 } 9}
In the Main
class, we create a BasicCar
and then wrap it with SportsCar
and LuxuryCar
decorators to dynamically add their respective features.
The Decorator pattern is crucial in software development for several reasons:
- Flexibility: It allows you to add or remove features dynamically at runtime without altering the original class structure.
- Reusability: You can create multiple decorators, which can be combined with various objects, promoting code reuse.
- Single Responsibility Principle: It enables you to divide functions into different classes, each with a single responsibility.
By mastering the Decorator pattern, you can design more adaptable and maintainable systems. It encourages the development of flexible code that can evolve with changing requirements.
Ready to solidify your understanding with some hands-on practice? Let's dive into the practice section!