Core Java

Object-Oriented Design Patterns with Java

1. Introduction

Object-Oriented Design Patterns came out from software engineers recognizing recurring problems and formalizing solutions as reusable, named patterns. In 1977, Christopher Alexander published “A Pattern Language” in architecture which introduced the concept of patterns in building design. In 1987, Kent Beck and Ward Cunningham experimented with applying Alexander’s ideas to software. In 1994, 23 Object-Oriented Design Patterns across 3 categories were published by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides in the “Design Patterns: Elements of Reusable Object-Oriented Software” book. After 2000, dependency injection, concurrency, distributed systems, microservices, front-end frameworks design patterns, etc were added. In this article, I will create simple Java programs for the following patterns.

2. Singleton Pattern

The Singleton Pattern is one of the five object creation mechanisms introduced in “Design Patterns: Elements of Reusable Object-Oriented Software”. It solves the problem of ensuring only one instance of a class exists in an application and provides a global point of access to that instance. Configuration or properties manager, such as logger, connection pool manager are candidates for the Singleton Pattern.

Creational PatternObjectiveJava Example
SingletonEnsure there’s only one instance of a class, and make it easily accessible everywhere.Runtime.getRuntime()
Factory MethodCreate objects without specifying the exact class.java.util.Calendar.getInstance()
Abstract FactoryCreate families of related objects.javax.xml.parsers.DocumentBuilderFactory
BuilderBuild a complex object by a step-by-step process where the object requires many optional parameters.java.lang.StringBuilder
PrototypeClone existing objects instead of creating from scratch.java.lang.Object.clone()

The Singleton design pattern addresses the following problems:

  1. Controlled access to a single resource
    • Sometimes you only ever want one instance of something (e.g., a configuration manager, logging system, thread pool, cache).
    • Without Singleton, developers might accidentally create multiple instances, which could cause inconsistency or unnecessary overhead.
  2. Global access without global variables
    • Instead of scattering global state across the program, Singleton provides a controlled and centralized way to access a shared resource.
  3. Lazy initialization
    • The instance should be created only when it’s needed.

2.1 Basic Singleton Class

In this step, I will create a BasicSingleton.java that guarantees only one instance of that class will ever be created.

BasicSingleton.java

package org.zheng.oodexample.singletonpattern;

public class BasicSingleton {

	private static BasicSingleton instance;

	public static BasicSingleton getInstance() {
		if (instance == null) {
			System.out.println("BasicSingleton is created.");
			instance = new BasicSingleton();
		}
		return instance;
	}

	public void foo(String message) {
		System.out.println("BasicSingleton: " + message);
	}
}
  • Line 5: the static keyword ensures only one instance is created.
  • Line 7, 8: ensure the instance will be created only when it is used.

2.2 Thread Safe Singleton Class

In this step, I will create a ThreadSafeSingleton.java that guarantees only one instance of that class will ever be created in a muliti-thread application.

ThreadSafeSingleton.java

package org.zheng.oodexample.singletonpattern;

public final class ThreadSafeSingleton {

	private static class Holder {
		private static final ThreadSafeSingleton INSTANCE = new ThreadSafeSingleton();
	}

	public static ThreadSafeSingleton getInstant() {
		return Holder.INSTANCE;
	}

	// private constructor prevent instantiation
	private ThreadSafeSingleton() {
		System.out.println("ThreadSafeSingletonDemo is created");
	}
}
  • Line 5-6: the static Holder class ensures only one instance is created.
  • Line 9-10: ensure the instance will be created only when it is used.

2.3 Demo Singleton Class

In this step, I will create a DemoSingletonApp.java to demonstrate two Singleton classes: java.lang.Runtime and SingletonDemo created in step 2.1.

DemoSingletonApp.java

package org.zheng.oodexample;

import org.zheng.oodexample.singletonpattern.BasicSingleton;
import org.zheng.oodexample.singletonpattern.ThreadSafeSingleton;

public class DemoSingletonApp {

	public static void main(String[] args) {

		Runtime rt1 = Runtime.getRuntime();
		Runtime rt2 = Runtime.getRuntime();
		System.out.println("Verify rt1 and rt2 are same: " + rt1.equals(rt2));

		System.out.println("FreeMemory=" + rt1.freeMemory());
		System.out.println("AvailableProcessors=" + rt2.availableProcessors());

		BasicSingleton.getInstance().foo("Mary");

		Runnable task = () -> {
			ThreadSafeSingleton singletonI = ThreadSafeSingleton.getInstant();
			System.out.println(Thread.currentThread().getName() + ": " + singletonI);
		};

		for (int i = 0; i < 5; i++) {
			new Thread(task, "Thread-" + i).start();
		}
	}

}
  • Line 10-12: demonstrate that two local variables rt1 and rt2 are the same instance.
  • Line 17: the normal way to get the BasicSingleton instance and invoke its method.
  • Line 19-21: the ThreadSafeSingleton is created once even in a five-thread application.

Run DemoSingletonApp and capture the output:

DemoSingletonApp Output

Verify rt1 and rt2 are same: true
FreeMemory=261221008
AvailableProcessors=12
BasicSingleton is created.
BasicSingleton: Mary
ThreadSafeSingletonDemo is created
Thread-3: org.zheng.oodexample.singletonpattern.ThreadSafeSingleton@4eb55d09
Thread-4: org.zheng.oodexample.singletonpattern.ThreadSafeSingleton@4eb55d09
Thread-0: org.zheng.oodexample.singletonpattern.ThreadSafeSingleton@4eb55d09
Thread-1: org.zheng.oodexample.singletonpattern.ThreadSafeSingleton@4eb55d09
Thread-2: org.zheng.oodexample.singletonpattern.ThreadSafeSingleton@4eb55d09
  • the output confirmed only one instance is ever created.

2.4 Singleton Pattern Drawback

Singleton is basically a global variable per JVM. Any class can call Singleton.getInstance() that ends with implicit dependencies between that class and the singleton class. In a multithreaded application, use the ThreadSafeSingleton instead of BasicSingleton to avoid concurrency issues. In clustered applications, there will be more instances, one per each JVM, that requires complex synchronization.

Note: Spring framework annotation @Scope("singleton") ensures that each Spring container maintains exactly one instance of the bean. But it’s not a Singleton as instances still can be created via the new operator.

3. Factory Pattern

A factory provides a way to create objects without exposing the instantiation logic to the client. The Factory Pattern creates one object at a time and subclasses or implementations decide the exact class type. Clients call a factory method that decides what concrete implementation object to create instead of using the new operator.

3.1 ShapeFactory

In this step, I will create a ShapeFactory.java that creates an object of either Circle or Rectangle based on the type argument.

Object-Oriented Design Patterns with Java
Figure 1. Factory Pattern

ShapeFactory.java

package org.zheng.oodexample.factorypattern;

public class ShapeFactory {
	public static Shape getShape(String type, double w, double h) {
		Shape ret;
		switch (type) {
		case "circle":
			ret = new Circle(w);
			break;
		case "rectangle":
			ret = new Rectangle(w, h);
			break;
		default:
			throw new IllegalArgumentException("Unknown shape " + type);
		}

		return ret;
	}
}
  • Line 4: the ShapeFactory.getShape method creates the object based on the type argument.
  • Line 7-8: create a Circle object if the type is “circle“.
  • Line 10-11: create a Rectangle object if the type is “rectangle“.

3.2 Shape Interface & Circle and Rectangle Classes

In this step, I will create a Shape.java interface and Circle and Rectangle classes to implement the Shape interface.

Shape.java

package org.zheng.oodexample.factorypattern;

public interface Shape {
	void draw();
}
  • Line 4: the draw method is used for the Factory Pattern.

Circle.java

package org.zheng.oodexample.factorypattern;

public class Circle implements Shape {

	public double radius;

	public Circle(double radius) {
		super();
		this.radius = radius;
	}

	@Override
	public void draw() {
		System.out.println("Draw a circle with radius" + this.radius);
	}

	@Override
	public String toString() {
		return "Circle [radius=" + radius + "]";
	}
}
  • line 13: override the draw method.

Rectangle.java

package org.zheng.oodexample.factorypattern;

public class Rectangle implements Shape {
	public double height;
	public double width;

	public Rectangle(double width, double height) {
		super();
		this.width = width;
		this.height = height;
	}

	@Override
	public void draw() {
		System.out.printf("\nDraw a Rectangle with width/height = %f/%f", width, height);
	}

	@Override
	public String toString() {
		return "Rectangle [width=" + width + ", height=" + height + "]";
	}
}
  • line 14: override the draw method.

3.3 DemoFactoryApp

In this step, I will create a DemoFactoryApp.java that creates an object of either Circle or Rectangle based on the type argument.

DemoFactoryApp.java

package org.zheng.oodexample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zheng.oodexample.factorypattern.Shape;
import org.zheng.oodexample.factorypattern.ShapeFactory;

public class DemoFactoryApp {

	private static void demoShape() {
		Shape circle = ShapeFactory.getShape("circle", 1, 1);
		circle.draw();

		Shape rectangle = ShapeFactory.getShape("rectangle", 3, 4);
		rectangle.draw();

	}

	public static void main(String[] args) {
		// for slf4j loggerFactory
		Logger log = LoggerFactory.getLogger(DemoFactoryApp.class);

		log.info("Mary test LoggerFactory");

		demoShape();
	}

}
  • Line 11, 14: The ShapeFactory.getShape() creates a Circle and Rectangle object.
  • Line 21: the org.slf4j.LoggerFactory.getLogger() creates a Logger object.

Run DemoFactoryApp and capture the output.

DemoFactoryApp Output

11:44:58.417 [main] INFO org.zheng.oodexample.DemoFactoryApp -- Mary test LoggerFactory
Draw a circle with radius1.0

Draw a Rectangle with width/height = 3.000000/4.000000

3.4 Factory Pattern Drawback

The Factory Pattern is a great choice when you need to isolate client code from the concrete classes, object creation is complex, and easy switching among implementations in the future. For example, the logging libraries, database drivers, etc. There are some drawbacks:

  • Less flexible, see step 3.1 as the factory method uses conditions to decide which object to create. When a new type is added, it requires an update to the getShape() method.
  • Overhead in simple scenarios as the new operator is simpler than the factory method.
  • Hard to trace instantiation as it is not obvious which concrete class is being created.

4. Decorator Pattern

The Decorator Pattern is one of seven structural patterns by “Gang of Four”. It attaches new behaviors to objects dynamically without altering their structure.

Structural PatternObjectiveJava Example
AdapterMake incompatible interface work togetherjava.util.Arrays.asList
BridgeSeparate abstraction from implementationJDBC driver interface vs implementation
CompositeTreat individual and group objects uniformlyjavax.swing.JComponent
DecoratorAdd responsibilities dynamicallyjava.io.BufferReader
FacadeSimplify complex subsystemsjavax.faces.context.FacesContext
FlyweightShare common object state
ProxyProvide a placeholder for another objectjava.lang.reflect.Proxy

Java I/O streams utilize the Decorator Pattern. The Decorator Pattern has four parts:

  • The Interface defines methods.
  • Concrete component for the base object.
  • Abstract Decorator wraps the object.
  • Concrete Decorators to add new behavior dynamically.
Object-Oriented Design Patterns with Java
Figure 2. Decorator Pattern

4.1 Coffee Interface

In this step, I will create a Coffee Interface with two methods: getDescription and cost.

Coffee.java

package org.zheng.oodexample.decoratorpattern;

import java.math.BigDecimal;

public interface Coffee {
	BigDecimal cost();
	String getDescription();
}

4.2 StandardCoffee Class

In this step, I will create a StandardCoffee.java that implements the Coffee interface defined in step 4.1.

StandardCoffee.java

package org.zheng.oodexample.decoratorpattern;

import java.math.BigDecimal;

public class StandardCoffee implements Coffee {

	@Override
	public BigDecimal cost() {
		return new BigDecimal(2);
	}

	@Override
	public String getDescription() {
		return "Standard Coffee";
	}

}

4.3 Abstract CoffeeDecorator Class

In this step, I will create an abstract CoffeeDecorator class that wraps the Coffee object.

CoffeeDecorator.java

package org.zheng.oodexample.decoratorpattern;

import java.math.BigDecimal;

public abstract class CoffeeDecorator implements Coffee {

	protected Coffee decoratedCoffee;

	public CoffeeDecorator(Coffee decoratedCoffee) {
		super();
		this.decoratedCoffee = decoratedCoffee;
	}

	@Override
	public BigDecimal cost() {
		return decoratedCoffee.cost();
	}

	@Override
	public String getDescription() {
		return decoratedCoffee.getDescription();
	}

}
  • Line 7: the coffee is wrapped inside the CoffeeDecorator.

4.4 CreamDecorator Class

In this step, I will create a CreamDecorator that extends from CoffeeDecorator. The cost is increased by 0.3 when adding cream to the coffee.

CreamDecorator.java

package org.zheng.oodexample.decoratorpattern;

import java.math.BigDecimal;

public class CreamDecorator extends CoffeeDecorator {

	public CreamDecorator(Coffee decoratedCoffee) {
		super(decoratedCoffee);
	}

	@Override
	public BigDecimal cost() {
		return super.cost().add(new BigDecimal(0.3));
	}

	@Override
	public String getDescription() {
		return super.getDescription() + ", Cream";
	}

}

4.5 MilkDecorator

In this step, I will create a MilkDecorator that extends from CoffeeDecorator. The cost is increased by 0.5 when adding milk to the coffee.

MilkDecorator.java

package org.zheng.oodexample.decoratorpattern;

import java.math.BigDecimal;

public class MilkDecorator extends CoffeeDecorator {

	public MilkDecorator(Coffee decoratedCoffee) {
		super(decoratedCoffee);
	}

	@Override
	public BigDecimal cost() {
		return super.cost().add(new BigDecimal(0.5));
	}

	@Override
	public String getDescription() {
		return super.getDescription() + ", Milk";
	}

}

4.6 SugarDecorator

In this step, I will create a SugarDecorator that extends from CoffeeDecorator. The cost is increased by 0.2 when adding sugar to the coffee.

SugarDecorator.java

package org.zheng.oodexample.decoratorpattern;

import java.math.BigDecimal;

public class SugarDecorator extends CoffeeDecorator {

	public SugarDecorator(Coffee decoratedCoffee) {
		super(decoratedCoffee);
	}

	@Override
	public BigDecimal cost() {
		return super.cost().add(new BigDecimal(0.2));
	}

	@Override
	public String getDescription() {
		return super.getDescription() + ", Sugar";
	}

}

4.7 Demo Decorator Pattern

In this step, I will create a DemoDecoratorApp to show how clients use it.

DemoDecoratorApp.java

package org.zheng.oodexample;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.text.DecimalFormat;

import org.zheng.oodexample.decoratorpattern.Coffee;
import org.zheng.oodexample.decoratorpattern.CreamDecorator;
import org.zheng.oodexample.decoratorpattern.MilkDecorator;
import org.zheng.oodexample.decoratorpattern.StandardCoffee;
import org.zheng.oodexample.decoratorpattern.SugarDecorator;

public class DemoDecoratorApp {

	private static void demoCoffee() {
		Coffee sCoffee = new StandardCoffee();
		DecimalFormat df = new DecimalFormat("#.##");

		System.out.println(sCoffee.getDescription() + " $" + df.format(sCoffee.cost()));

		sCoffee = new MilkDecorator(sCoffee);

		System.out.println(sCoffee.getDescription() + " $" + df.format(sCoffee.cost()));

		sCoffee = new SugarDecorator(sCoffee);

		System.out.println(sCoffee.getDescription() + " $" + df.format(sCoffee.cost()));

		sCoffee = new CreamDecorator(sCoffee);

		System.out.println(sCoffee.getDescription() + " $" + df.format(sCoffee.cost()));
	}

	private static void demoInputStream() {
		// Java IO stream with decorator
		try {
			InputStream fileStream = new FileInputStream("C:\\MaryZheng\\test.txt");
			InputStream bufferedStrem = new BufferedInputStream(fileStream);
			Reader reader = new InputStreamReader(bufferedStrem);
			BufferedReader bufferedReader = new BufferedReader(reader);

			String line;
			while ((line = bufferedReader.readLine()) != null) {
				System.out.println(line);
			}

			bufferedReader.close();

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static void demoNestedCoffee() {
		Coffee sCoffee = new StandardCoffee();
		DecimalFormat df = new DecimalFormat("#.##");

		System.out.println(sCoffee.getDescription() + " $" + df.format(sCoffee.cost()));

		sCoffee = new CreamDecorator(new SugarDecorator(new MilkDecorator(sCoffee)));

		System.out.println(sCoffee.getDescription() + " $" + df.format(sCoffee.cost()));
	}

	public static void main(String[] args) {
		demoCoffee();
		demoNestedCoffee();
		demoInputStream();
	}

}
  • Line 20, 25, 29, 33: the same sCoffee object is updated dynamically without changing its structure.
  • Line 59, 64: nested object is changed.
  • Line 41-44: Java I/O streams use the Decorator Pattern.

Run DemoDecoratorApp and capture the output.

DemoDecoratorApp Output

Standard Coffee $2
Standard Coffee, Milk $2.5
Standard Coffee, Milk, Sugar $2.7
Standard Coffee, Milk, Sugar, Cream $3
Standard Coffee $2
Standard Coffee, Milk, Sugar, Cream $3
this is a Test.
This is a test for the inputStream decorator.

4.8 Decorator Pattern Drawback

The Decorator Pattern gives flexibility and extensibility, but at the cost of complexity and manageability. Here are a few common drawbacks:

  • Complexity in many layers when applying multiple decorators.
  • Hard to configure and manage as clients need to know which decorators to apply and in which order.
  • Not transparent for type checks.
  • Performance overhead as each call is delegated through multiple wrapper layers.

5. Composite Pattern

The Composite Pattern treats individual and group objects uniformly to build tree-like structures of objects. It has three parts:

  • Component to declare common operations.
  • Leaf to represent Individual objects.
  • A composite object represents an object that can contain children.
              ┌────────────────┐
              │  FileSystem    │
              │+display(indent)│
              └───────┬────────┘
                      │
      ┌───────────────┴───────────────┐
      │                               │
 ┌─────────────┐                 ┌─────────────┐
 │    Leaf     │                 │  Composite  │
 │   (File)    │                 │   (Folder)  │
 │- name       │                 │- children[] │
 └─────────────┘                 └─────────────┘

5.1 FileSystem Interface

In this step, I will create a FileSystem.java that has a display method.

FileSystem.java

package org.zheng.oodexample.compositepattern;

public interface FileSystem {
	void display(String indent);
}

5.2 FileRecord

In this step, I will create a FileRecord.java record for the leaf.

FileRecord.java

package org.zheng.oodexample.compositepattern;

public record FileRecord(String name) implements FileSystem {

	@Override
	public void display(String indent) {
		System.out.println(indent + " - File: " + name);
	}
}

5.3 Folder

In this step, I will create a Folder.java class as the composite object.

Folder.java

package org.zheng.oodexample.compositepattern;

import java.util.ArrayList;
import java.util.List;

public class Folder implements FileSystem {

	private final List children = new ArrayList();
	private final String name;

	public Folder(String name) {
		super();
		this.name = name;
	}

	public void add(FileSystem child) {
		children.add(child);
	}

	@Override
	public void display(String indent) {
		System.out.println(indent + " + Folder: " + name);
		children.forEach(child -> child.display(indent + "     "));
	}

	public void remove(FileSystem child) {
		children.remove(child);
	}

}

5.4 Demo Composite Pattern

In this step, I will create a DemoCompositeApp.java class.

DemoCompositeApp.java

package org.zheng.oodexample;

import org.zheng.oodexample.compositepattern.FileRecord;
import org.zheng.oodexample.compositepattern.FileSystem;
import org.zheng.oodexample.compositepattern.Folder;

public class DemoCompositeApp {

	public static void main(String[] args) {
		Folder root = new Folder("root");

		FileSystem file1 = new FileRecord("first file");
		FileSystem file2 = new FileRecord("second file");

		Folder images = new Folder("images");
		images.add(new FileRecord("image1"));
		images.add(new FileRecord("image2"));

		root.add(file1);
		root.add(file2);
		root.add(images);

		root.display("");

	}

}

Run DemoCompositeApp and capture the output.

DemoCompositeApp Output

 + Folder: root
      - File: first file
      - File: second file
      + Folder: images
           - File: image1
           - File: image2

5.5 Composite Pattern Drawback

The Composite Pattern simplifies client code by providing a unified way to handle both simple and complex elements in a tree structure. But if the structure is not simple, then using it adds unnecessary complexity.

6. Proxy Pattern

The Proxy Pattern provides a placeholder for another object to control access to it.

6.1 Image Interface

In this step, I will create a Image.java that has a display method.

Image.java

package org.zheng.oodexample.proxypattern;

public sealed interface Image permits RealImage, ProxyImage {
	void display();
}

6.2 RealImage Classes

In this step, I will create a RealImage.java class that implements the Image interface.

RealImage.java

package org.zheng.oodexample.proxypattern;

public final class RealImage implements Image {

	private final String fileName;

	public RealImage(String fileName) {
		super();
		this.fileName = fileName;
		loadFromDisk();
	}

	@Override
	public void display() {
		System.out.println("Displaying image " + fileName);

	}

	private void loadFromDisk() {
		System.out.printf("Load image %s from disk. ", fileName);

	}

}

6.3 ProxyImage Classes

In this step, I will create a ProxyImage.java class that implements the Image interface.

ProxyImage.java

package org.zheng.oodexample.proxypattern;

public final class ProxyImage implements Image {

	private final String fileName;
	private RealImage realImage;

	public ProxyImage(String fileName) {
		super();
		this.fileName = fileName;
	}

	@Override
	public void display() {
		if (realImage == null) {
			realImage = new RealImage(fileName); //lazy initialization
		}
		realImage.display();
	}

}
  • Line 6: create the placeholder for the RealImage.
  • Line 16: lazy initialization for the realImage.

6.4 Demo Proxy Pattern

In this step, I will create a DemoProxyApp.java class that implements the Image interface.

DemoProxyApp.java

package org.zheng.oodexample;

import org.zheng.oodexample.proxypattern.Image;
import org.zheng.oodexample.proxypattern.ProxyImage;

public class DemoProxyApp {

	public static void main(String[] args) {

		Image image1 = new ProxyImage("photo1.jpg");
		Image image2 = new ProxyImage("photo2.jpg");

		image1.display();
		image1.display();
		image2.display();
	}
}

Run DemoProxyApp and capture the output.

DemoProxyApp Output

Load image photo1.jpg from disk. Displaying image photo1.jpg
Displaying image photo1.jpg
Load image photo2.jpg from disk. Displaying image photo2.jpg

6.5 Proxy Pattern Drawback

The Proxy Pattern trades simplicity for control and flexibility. It’s excellent for lazy loading and access control but it increases complexity and adds performance overhead as it adds extra method calls. The lazy loading might delay errors until runtime.

7. Visitor Pattern

The Visitor Pattern is a behavioral design pattern that lets you separate the algorithms from the objects on which they operate. It’s often used when you have a structure of different element types and you want to apply different operations without changing these element classes.

Behavioral PatternObjectiveJava Example
Chain of ResponsibilityPass request along a chainjava.util.logging.Logger
CommandEncapsulate request as an objectjava.lang.Runnable
InterpreterDefine grammar and interpretjava.util.regex.Pattern
IteratorSequential access to a collectionjava.util.Iterator
MediatorCentralize communicationjava.util.Timer
MementoSave and restore statejava.util.Date
ObserverPublish-subscribe modeljava.util.Observer
StateChange behavior when state changesjavax.faces.lifecycle.Lifecycle
StrategySelect algorithm at runtimejava.util.Comparator
Template methodDefine algorithm skeletonjava.util.AbstractList

7.1 ShapeVisitor & Shape Interfaces

In this step, I will create a ShapeVisitor.java with an overloaded visit method(one for each type).

ShapeVisitor.java

package org.zheng.oodexample.visitorpattern;

public interface ShapeVisitor {
	void visit(Circle circle);
	void visit(Rectangle rectangle);
}

When a new element type is added, then we need to add an overloaded visit method for the new type.

And create a ShapeObj interface with an accept method.

Shape.java

package org.zheng.oodexample.visitorpattern;

public interface Shape {
	void accept(ShapeVisitor visitor);
}

7.2 Circle & Rectangle Classes

In this step, I will create a Circle.java implementing Shape interface.

Circle.java

package org.zheng.oodexample.visitorpattern;

public class Circle implements Shape {

	public double radius;

	public Circle(double radius) {
		super();
		this.radius = radius;
	}

	@Override
	public void accept(ShapeVisitor visitor) {
		visitor.visit(this);

	}

	@Override
	public String toString() {
		return "Circle [radius=" + radius + "]";
	}

}

And create a Retangle.java implementing Shape interface.

Retangle.java

package org.zheng.oodexample.visitorpattern;

public class Rectangle implements Shape {

	public double height;

	public double width;

	public Rectangle(double width, double height) {
		super();
		this.width = width;
		this.height = height;
	}

	@Override
	public void accept(ShapeVisitor visitor) {
		visitor.visit(this);
	}

	@Override
	public String toString() {
		return "Rectangle [width=" + width + ", height=" + height + "]";
	}

}

7.3 AreaCalculator Class

In this step, I will create a AreaCalculator.java with overloaded visit methods (one for each shape type). A new visit method has to be be added when a new type is introduced.

AreaCalculator.java

package org.zheng.oodexample.visitorpattern;

public class AreaCalculator implements ShapeVisitor {

	@Override
	public void visit(Circle circle) {
		double area = Math.PI * circle.radius * circle.radius;
		System.out.println("Area of Circle: " + area);
	}

	@Override
	public void visit(Rectangle rectangle) {
		double area = Math.PI * rectangle.width * rectangle.height;
		System.out.println("Area of Rectangle: " + area);
	}

}

7.4 PerimeterCalculator Interface

In this step, I will create a PerimeterCalculator.java with an overloaded visit method(one for each type).

PerimeterCalculator.java

package org.zheng.oodexample.visitorpattern;

public class PerimeterCalculator implements ShapeVisitor {

	@Override
	public void visit(Circle circle) {
		double perimeter = 2 * Math.PI * circle.radius;
		System.out.println("Perimeter of Circle: " + perimeter);
	}

	@Override
	public void visit(Rectangle rectangle) {
		double perimeter = 2 * (rectangle.width + rectangle.height);
		System.out.println("Perimeter of Rectangle: " + perimeter);
	}

}

7.5 Demo Visitor Pattern

In this step, I will create a DemoVisitorApp.java with an overloaded visit method(one for each type).

DemoVisitorApp.java

package org.zheng.oodexample;

import org.zheng.oodexample.visitorpattern.AreaCalculator;
import org.zheng.oodexample.visitorpattern.Circle;
import org.zheng.oodexample.visitorpattern.PerimeterCalculator;
import org.zheng.oodexample.visitorpattern.Rectangle;
import org.zheng.oodexample.visitorpattern.Shape;
import org.zheng.oodexample.visitorpattern.ShapeVisitor;

public class DemoVisitorApp {

	public static void main(String[] args) {

		Shape circle = new Circle(1);
		Shape rectangle = new Rectangle(3, 4);

		ShapeVisitor areaVisitor = new AreaCalculator();
		ShapeVisitor perimeterVisitor = new PerimeterCalculator();

		System.out.println("Calculate area and perimeter for " + circle.toString());
		circle.accept(areaVisitor);
		circle.accept(perimeterVisitor);

		System.out.println("Calculate area and perimeter for " + rectangle.toString());
		rectangle.accept(areaVisitor);
		rectangle.accept(perimeterVisitor);
	}

}

Run DemoVisitorApp and capture the output.

DemoVisitorApp Output

Calculate area and perimeter for Circle [radius=1.0]
Area of Circle:3.141592653589793
Perimeter of Circle:6.283185307179586
Calculate area and perimeter for Rectangle [width=3.0, height=4.0]
Area of Rectangle:37.69911184307752
Perimeter of Rectangle:14.0

7.6 Visitor Pattern Drawback

The Visitor Pattern is powerful when the element structure is stable. It becomes painful when the elements change often. Here are some drawbacks of the Visitor Pattern:

  • Hard to add new element types as each new type requires modifications to the existing visitors to handle it.
  • Double dispatch complexity as it relies on double dispatch: element.accept(visitor) calls visitor.visit(element).
  • Maintenance Overhead if there are many elements and many visitors.

8. Strategy Pattern

The Strategy Pattern selects algorithms at runtime.

           +-------------------+
           |   PaymentStrategy |
           |-------------------|
           | + pay(amount):void|
           +-------------------+
                  ▲     ▲     ▲
                  |     |     |
   +-------------------+ +---------------+ +---------------------+
   | CreditCardPayment | | PaypalPayment | | TODOPayment         |
   |-------------------| |---------------| |---------------------|
   | - cardNumber      | | - email       | | - accountNumber     |
   | + pay(amount)     | | + pay(amount) | | + pay(amount)       |
   +-------------------+ +---------------+ +---------------------+

                      +-------------------+
                      |      Checkout     |
                      |-------------------|
                      | + processOrder()  |
                      +-------------------+
                               |
                               v
                   Uses a PaymentStrategy

8.1 PaymentStrategy

In this step, I will create a PaymentStrategy.java that has a pay method to pay an amount.

PaymentStrategy.java

package org.zheng.oodexample.strategypattern;

import java.math.BigDecimal;

public interface PaymentStrategy {
	void pay(BigDecimal amount);
}

8.2 CreditCardPayment & PaypalPayment Classes

In this step, I will create a CreditCardPayment.java and PaypalPayment.java classes to implement the PaymentStrategy interface.

CreditCardPayment.java

package org.zheng.oodexample.strategypattern;

import java.math.BigDecimal;

public class CreditCardPayment implements PaymentStrategy {

	private final String ccNumber;

	public CreditCardPayment(String ccNumber) {
		super();
		this.ccNumber = ccNumber;
	}

	@Override
	public void pay(BigDecimal amount) {
		System.out.printf("\nPaid $%.2f via Credit Card %s", amount, ccNumber);
	}
}

PaypalPayment.java

package org.zheng.oodexample.strategypattern;

import java.math.BigDecimal;

public class PaypalPayment implements PaymentStrategy {

	private final String email;

	public PaypalPayment(String email) {
		super();
		this.email = email;
	}

	@Override
	public void pay(BigDecimal amount) {
		System.out.printf("\nPaid $%.2f via Paypal %s", amount, email);
	}
}

8.3 Checkout Class

In this step, I will create a Checkout.java class.

Checkout.java

package org.zheng.oodexample.strategypattern;

import java.math.BigDecimal;

public class Checkout {

	private final PaymentStrategy strategy;

	public Checkout(PaymentStrategy strategy) {
		super();
		this.strategy = strategy;
	}

	public void processOrder(BigDecimal amount) {
		strategy.pay(amount);
	}

}

8.4 Demo Strategy Pattern

In this step, I will create a DemoStrategyApp.java class.

DemoStrategyApp.java

package org.zheng.oodexample;

import java.math.BigDecimal;

import org.zheng.oodexample.strategypattern.Checkout;
import org.zheng.oodexample.strategypattern.CreditCardPayment;
import org.zheng.oodexample.strategypattern.PaypalPayment;

public class DemoStrategyApp {

	public static void main(String[] args) {
		Checkout ccCheckout = new Checkout(new CreditCardPayment("12345678901234"));
		ccCheckout.processOrder(new BigDecimal(20));

		ccCheckout = new Checkout(new PaypalPayment("test@gmail.com"));
		ccCheckout.processOrder(new BigDecimal(10));
	}
}
  • Line 12,15: client needs to know which PaymentStrategy class to use.

Run DemoStategyApp.java and capture output.

DemoStategyApp output

Paid $20.00 via Credit Card 12345678901234
Paid $10.00 via Paypal test@gmail.com

8.5 Strategy Pattern Drawback

The Strategy Pattern defines a family of algorithms, encapsulates each one and makes them interchangeable. Each algorithm/strategy requires its own class that may lead too many classes. The behavior is determined at runtime that makes debugging harder.

9. State Pattern

The State Pattern changes behavior when the state changes.

                +-----------------+
                |    Document     |
                |-----------------|
                | - state: State  |
                |-----------------|
                | + setState()    |
                | + advance()     |
                | + reject()      |
                | + currentState()|
                +-------+---------+
                        |
                        v
                +----------------+
                |     State      |
                |----------------|
                | + next(doc)    |
                | + reject(doc)  |
                +----------------+
                 /       |        \
                /        |         \
               v         v          v
    +----------------+ +----------------+ +----------------+
    |   DraftState   | | ModerationState| | PublishedState |
    |----------------| |----------------| |----------------|
    | + next(doc)    | | + next(doc)    | | + next(doc)    |
    | + reject(doc)  | | + reject(doc)  | | + reject(doc)  |
    +----------------+ +----------------+ +----------------+

9.1 State Interface

In this step, I will create a State.java that has next and reject methods.

State.java

package org.zheng.oodexample.statepattern;

public interface State {
	void next(Document doc);
	void reject(Document doc);
}

9.2 DraftState, ModerationState, & PublishState

In this step, I will create a DraftState, ModerationState, and PublishState classes to implement the State interface.

DraftState.java

package org.zheng.oodexample.statepattern;

public class DraftState implements State {

	@Override
	public void next(Document doc) {
		System.out.println("Changes from Draft to Moderation.");
		doc.setState(new ModerationState());
	}

	@Override
	public void reject(Document doc) {
		System.out.println("Draft cannot be rejected.");
	}
}
  • Line 6, 12: the behavior for both next and reject methods are changed.

ModerationState.java

package org.zheng.oodexample.statepattern;

public class ModerationState implements State {

	@Override
	public void next(Document doc) {
		System.out.println("Changes from Moderation to Published.");
		doc.setState(new PublishedState());
	}

	@Override
	public void reject(Document doc) {
		System.out.println("Reject to Draft.");
		doc.setState(new DraftState());
	}

}

PublishState.java

package org.zheng.oodexample.statepattern;

public class PublishedState implements State {

	@Override
	public void next(Document doc) {
		System.out.println("Document is already published.");
	}

	@Override
	public void reject(Document doc) {
		System.out.println("Can not reject published document.");
	}

}

9.3 Document

In this step, I will create a Document.java class that has a State and advance and reject methods.

Document.java

package org.zheng.oodexample.statepattern;

public class Document {

	private State state = new DraftState();// initial state

	public void advance() {
		state.next(this);
	}

	public String currentState() {
		return state.getClass().getSimpleName();
	}

	public void reject() {
		state.reject(this);
	}

	public void setState(State state) {
		this.state = state;
	}
}

9.4 Demo State Pattern

In this step, I will create a DemoStrategyApp.java.

DemoStateApp.java

package org.zheng.oodexample;

import org.zheng.oodexample.statepattern.Document;

public class DemoStateApp {

	public static void main(String[] args) {
		Document doc = new Document();
		System.out.println("Current State = " + doc.currentState());

		doc.advance();
		System.out.println("Current State = " + doc.currentState());
		
		doc.reject();
		System.out.println("Current State = " + doc.currentState());
		
		doc.reject();
		System.out.println("Current State = " + doc.currentState());
		
		doc.advance();
		System.out.println("Current State = " + doc.currentState());
		
		doc.advance();
		System.out.println("Current State = " + doc.currentState());
		
		doc.advance();
		System.out.println("Current State = " + doc.currentState());
	}
}

Execute DemoStateApp and capture the output.

DemoStateApp Output

Current State = DraftState
Changes from Draft to Moderation.
Current State = ModerationState
Reject to Draft.
Current State = DraftState
Draft cannot be rejected.
Current State = DraftState
Changes from Draft to Moderation.
Current State = ModerationState
Changes from Moderation to Published.
Current State = PublishedState
Document is already published.
Current State = PublishedState

9.5 State Pattern Drawback

The State pattern requires its own class for each state. It may end up with lots of classes for a simple application.

10. Dependency Injection Pattern

The Dependency Injection(DI) Pattern is one of the most important design patterns used in Spring.
It’s basically about inverting the control of object creation: instead of classes creating their own dependencies, those dependencies are provided (“injected”) from the outside.

10.1 Constructor Injection Class

In this step, I will create a ConstructorInjection.java that injects a StandardCoffee via its constructor.

ConstructorInjection.java

package org.zheng.oodexample.djpattern;

import org.zheng.oodexample.DemoCompositeApp;

public class ConstructorInjection {

	private final DemoCompositeApp injectObj;

	public ConstructorInjection(DemoCompositeApp injectObj) {
		super();
		this.injectObj = injectObj;
	}

	public String demo() {
		return injectObj.getClass().getName();
	}
}
  • Line 7: the injectObj will be injected at the constructor.
  • Line 11: the injectObj is injected at the constructor.

10.2 Demo Constructor Injection

In this step, I will create a DemoDependencyInjectApp.java that injects a StandardCoffee via its constructor.

DemoDependencyInjectApp.java

package org.zheng.oodexample;

import org.zheng.oodexample.djpattern.ConstructorInjection;

public class DemoDependencyInjectApp {

	public static void main(String[] args) {

		DemoCompositeApp obj = new DemoCompositeApp();

		ConstructorInjection cIn = new ConstructorInjection(obj);

		System.out.println("" + cIn.demo());

	}

}
  • Line 11: inject the obj via the ConstructorInjection constructor.

Run DemoDependencyInjectApp and capture the output.

DemoDependencyInjectApp Output

org.zheng.oodexample.DemoCompositeApp

10.3 Dependency Injection Drawback

Dependency Injection is very powerful but it also has some drawbacks. DI increased complexity as it required configuring dependencies via constructor, annotations, or configuration classes. It’s harder to debug as sometimes it is not obvious where a dependency is coming from. Application may throw runtime error instead of compile time error when dependencies are misconfigured.

11. Conclusion

In this example, I created small programs to explain nine design patterns: Singleton, Factory, Decorator, Composite, Proxy, Visitor, Strategy, State, and Dependency Injection. I also pointed out some of the drawbacks. There are many more design patterns and developers should look at existing patterns for the problem and weigh the pros and cons before developing the solution.

12. Download

This was an example of a maven project which included Object-Oriented design patterns.

Download
You can download the full source code of this example here: Object-Oriented Design Patterns with Java

Mary Zheng

Mary graduated from the Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She worked as a lead Software Engineer where she led and worked with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Why & Thanks
Why & Thanks
3 months ago

Why in 5.3 the Folder() constructor have a call to super();? seems it calls to Object.super() [not necessary]

public interface FileSystem {
    void display(String indent);
}
// ...
public class Folder implements FileSystem {
    // ...
    public Folder(String name) {
        super(); // why?
        this.name = name;
    }
    // ...
}

Thanks in advanced!

Back to top button