Java 객체지향 설계 시 사용하는 디자인 패턴 예시

생성 패턴

싱글톤 패턴 (Singleton)

싱글톤 패턴 예시 1

public class Theme {
  private static Theme instance;
  private String themeColor;

  private Theme() {
    this.themeColor = "light"; // Default theme
  }

  public static Theme getInstance() {
    if (instance == null) {
      instance = new Theme();
    }
    return instance;
  }

  public String getThemeColor() {
    return themeColor;
  }
  public void setThemeColor(String themeColor) {
    this.themeColor = themeColor;
  }
}

public class Button {
  private String label;

  public Button(String label) {
    this.label = label;
  }

  public void display() {
    String themeColor = Theme.getInstance().getThemeColor();
    System.out.println("Button [" + label + "] displayed in " + themeColor + " theme.");
  }
}

public class TextField {
  private String text;

  public TextField(String text) {
    this.text = text;
  }

  public void display() {
    String themeColor = Theme.getInstance().getThemeColor();
    System.out.println("TextField [" + text + "] displayed in " + themeColor + " theme.");
  }
}

public class Label {
  private String text;

  public Label(String text) {
    this.text = text;
  }

  public void display() {
    String themeColor = Theme.getInstance().getThemeColor();
    System.out.println("Label [" + text + "] displayed in " + themeColor + " theme.");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Button button = new Button("Submit");
    TextField textField = new TextField("Enter your name");
    Label label = new Label("Username");

    button.display();
    textField.display();
    label.display();

    Theme.getInstance().setThemeColor("dark");

    button.display();
    textField.display();
    label.display();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Button [Submit] displayed in light theme.
TextField [Enter your name] displayed in light theme.
Label [Username] displayed in light theme.

Button [Submit] displayed in dark theme.
TextField [Enter your name] displayed in dark theme.
Label [Username] displayed in dark theme.

팩토리 메서드 패턴 (Factory Method)

팩토리 메서드 패턴 예시 1

// Product interface
interface Vehicle {
  void drive();
}

// Concrete products
class Car implements Vehicle {
  @Override
  public void drive() {
    System.out.println("Driving a car");
  }
}

class Motorcycle implements Vehicle {
  @Override
  public void drive() {
    System.out.println("Riding a motorcycle");
  }
}

// Creator abstract class
abstract class VehicleFactory {
  // Factory method
  abstract Vehicle createVehicle();
  
  // Operations using the factory method
  public void deliverVehicle() {
    Vehicle vehicle = createVehicle();
    System.out.println("Delivering the vehicle:");
    vehicle.drive();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    VehicleFactory carFactory = new CarFactory();
    carFactory.deliverVehicle();
    
    VehicleFactory motorcycleFactory = new MotorcycleFactory();
    motorcycleFactory.deliverVehicle();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Delivering the vehicle:
Driving a car
Delivering the vehicle:
Riding a motorcycle

팩토리 메서드 패턴 예시 2

// Product interface
public interface Product {
  void create();
}

public class Electronics implements Product {
  @Override
  public void create() {
    System.out.println("Electronics product created.");
  }
}

public class Clothing implements Product {
  @Override
  public void create() {
    System.out.println("Clothing product created.");
  }
}

public class Book implements Product {
  @Override
  public void create() {
    System.out.println("Book product created.");
  }
}

// ProductFactory class
public abstract class ProductFactory {
  // Factory Method
  public abstract Product createProduct(String type);

  public Product orderProduct(String type) {
    Product product = createProduct(type);
    product.create();
    return product;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    ProductFactory factory = new ConcreteProductFactory();

    // Create an electronics product
    Product electronics = factory.orderProduct("electronics");

    // Create a clothing product
    Product clothing = factory.orderProduct("clothing");

    // Create a book product
    Product book = factory.orderProduct("book");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Electronics product created.
Clothing product created.
Book product created.

팩토리 메서드 패턴 예시 3

// Payment interface
interface Payment {
  void processPayment(double amount);
}

class CreditCardPayment implements Payment {
  private String creditCardNumber;
  public CreditCardPayment(String creditCardNumber) {
    this.creditCardNumber = creditCardNumber;
  }
  
  @Override
  public void processPayment(double amount) {
    System.out.println("Credit card: $" + amount);
  }
}

class PayPalPayment implements Payment {
  private String payPalEmail;
  public PayPalPayment(String payPalEmail) {
    this.payPalEmail = payPalEmail;
  }
  
  @Override
  public void processPayment(double amount) {
    System.out.println("PayPal: $" + amount);
  }
}

class BankTransferPayment implements Payment {
  private String bankAccountNumber;
  public BankTransferPayment(String bankAccountNumber) {
    this.bankAccountNumber = bankAccountNumber;
  }

  @Override
  public void processPayment(double amount) {
    System.out.println("Bank transfer: $" + amount);
  }
}

class FinancialInfo {
  String creditCardNumber;
  String payPalEmail;
  String bankAccountNumber;

  public FinancialInfo(
    String creditCardNumber, 
    String payPalEmail, 
    String bankAccountNumber
  ) {
    this.creditCardNumber = creditCardNumber;
    this.payPalEmail = payPalEmail;
    this.bankAccountNumber = bankAccountNumber;
  }
}

// Abstract factory class
abstract class PaymentFactory {
  abstract Payment createPayment(FinancialInfo info);
}

class CreditCardPaymentFactory extends PaymentFactory {
  @Override
  Payment createPayment(FinancialInfo info) {
    return new CreditCardPayment(info.creditCardNumber);
  }
}

class PayPalPaymentFactory extends PaymentFactory {
  @Override
  Payment createPayment(FinancialInfo info) {
    return new PayPalPayment(info.payPalEmail);
  }
}

class BankTransferPaymentFactory extends PaymentFactory {
  @Override
  Payment createPayment(FinancialInfo info) {
    return new BankTransferPayment(info.bankAccountNumber);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    FinancialInfo userInfo = new FinancialInfo(
      "1234-5678-9012-3456", "user@example.com", "987654321"
    );

    PaymentFactory factory = new CreditCardPaymentFactory();
    Payment payment = factory.createPayment(userInfo);
    payment.processPayment(100.0);

    factory = new PayPalPaymentFactory();
    payment = factory.createPayment(userInfo);
    payment.processPayment(200.0);

    factory = new BankTransferPaymentFactory();
    payment = factory.createPayment(userInfo);
    payment.processPayment(300.0);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Credit card: $100.0
PayPal: $200.0
Bank transfer: $300.0

추상 팩토리 패턴 (Abstract Factory)

추상 팩토리 패턴 예시 1

// Abstract product interfaces
interface Button {
  void paint();
}

interface Checkbox {
  void paint();
}

class WindowsButton implements Button {
  @Override
  public void paint() {
    System.out.println("Rendering a button in Windows style");
  }
}

class WindowsCheckbox implements Checkbox {
  @Override
  public void paint() {
    System.out.println("Rendering a checkbox in Windows style");
  }
}

class MacOSButton implements Button {
  @Override
  public void paint() {
    System.out.println("Rendering a button in MacOS style");
  }
}

class MacOSCheckbox implements Checkbox {
  @Override
  public void paint() {
    System.out.println("Rendering a checkbox in MacOS style");
  }
}

// Abstract factory interface
interface GUIFactory {
  Button createButton();
  Checkbox createCheckbox();
}

// Concrete factories
class WindowsFactory implements GUIFactory {
  @Override
  public Button createButton() {
    return new WindowsButton();
  }

  @Override
  public Checkbox createCheckbox() {
    return new WindowsCheckbox();
  }
}

class MacOSFactory implements GUIFactory {
  @Override
  public Button createButton() {
    return new MacOSButton();
  }

  @Override
  public Checkbox createCheckbox() {
    return new MacOSCheckbox();
  }
}

// Client code
class Application {
  private Button button;
  private Checkbox checkbox;

  public Application(GUIFactory factory) {
    button = factory.createButton();
    checkbox = factory.createCheckbox();
  }

  public void paint() {
    button.paint();
    checkbox.paint();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Create Windows GUI
    GUIFactory windowsFactory = new WindowsFactory();
    Application windowsApp = new Application(windowsFactory);
    windowsApp.paint();

    System.out.println();

    // Create MacOS GUI
    GUIFactory macFactory = new MacOSFactory();
    Application macApp = new Application(macFactory);
    macApp.paint();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Rendering a button in Windows style
Rendering a checkbox in Windows style

Rendering a button in MacOS style
Rendering a checkbox in MacOS style

추상 팩토리 패턴 예시 2

// Abstract products
interface Connection {
  void open();
  void close();
}

interface Command {
  void execute(String query);
}

interface ResultSet {
  void getResults();
}

// Abstract factory
interface DatabaseFactory {
  Connection createConnection();
  Command createCommand();
  ResultSet createResultSet();
}

// Concrete products for MySQL
class MySQLConnection implements Connection {
  public void open() {
    System.out.println("Opening MySQL connection");
  }
  public void close() {
    System.out.println("Closing MySQL connection");
  }
}

class MySQLCommand implements Command {
  public void execute(String query) {
    System.out.println("Executing MySQL query: " + query);
  }
}

class MySQLResultSet implements ResultSet {
  public void getResults() {
    System.out.println("Getting results from MySQL database");
  }
}

// Concrete products for PostgreSQL
class PostgreSQLConnection implements Connection {
  public void open() {
    System.out.println("Opening PostgreSQL connection");
  }
  public void close() {
    System.out.println("Closing PostgreSQL connection");
  }
}

class PostgreSQLCommand implements Command {
  public void execute(String query) {
    System.out.println("Executing PostgreSQL query: " + query);
  }
}

class PostgreSQLResultSet implements ResultSet {
  public void getResults() {
    System.out.println("Getting results from PostgreSQL database");
  }
}

// Concrete factories
class MySQLFactory implements DatabaseFactory {
  public Connection createConnection() {
    return new MySQLConnection();
  }
  public Command createCommand() {
    return new MySQLCommand();
  }
  public ResultSet createResultSet() {
    return new MySQLResultSet();
  }
}

class PostgreSQLFactory implements DatabaseFactory {
  public Connection createConnection() {
    return new PostgreSQLConnection();
  }
  public Command createCommand() {
    return new PostgreSQLCommand();
  }
  public ResultSet createResultSet() {
    return new PostgreSQLResultSet();
  }
}

// Client code
class DatabaseClient {
  private Connection connection;
  private Command command;
  private ResultSet resultSet;

  public DatabaseClient(DatabaseFactory factory) {
    connection = factory.createConnection();
    command = factory.createCommand();
    resultSet = factory.createResultSet();
  }

  public void performDatabaseOperations() {
    connection.open();
    command.execute("SELECT * FROM users");
    resultSet.getResults();
    connection.close();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    DatabaseClient mysqlClient = new DatabaseClient(new MySQLFactory());
    mysqlClient.performDatabaseOperations();

    System.out.println("\nSwitching to PostgreSQL...\n");

    DatabaseClient postgresClient = new DatabaseClient(new PostgreSQLFactory());
    postgresClient.performDatabaseOperations();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Opening MySQL connection
Executing MySQL query: SELECT * FROM users
Getting results from MySQL database
Closing MySQL connection

Switching to PostgreSQL...

Opening PostgreSQL connection
Executing PostgreSQL query: SELECT * FROM users
Getting results from PostgreSQL database
Closing PostgreSQL connection

빌더 패턴 (Builder)

빌더 패턴 예시 1

// Product class
class Pizza {
  private String dough;
  private String sauce;
  private String topping;

  // Private constructor to enforce the use of Builder
  private Pizza(PizzaBuilder builder) {
    this.dough = builder.dough;
    this.sauce = builder.sauce;
    this.topping = builder.topping;
  }

  @Override
  public String toString() {
    return "Pizza with " + dough + " dough, "
      + sauce + " sauce, and " + topping + " topping.";
  }

  public static class PizzaBuilder {
    private String dough;
    private String sauce;
    private String topping;

    public PizzaBuilder dough(String dough) {
      this.dough = dough;
      return this;
    }

    public PizzaBuilder sauce(String sauce) {
      this.sauce = sauce;
      return this;
    }
    
    public PizzaBuilder topping(String topping) {
      this.topping = topping;
      return this;
    }

    public Pizza build() {
      return new Pizza(this);
    }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Pizza myPizza = new Pizza.PizzaBuilder()
          .dough("Thin Crust")
          .sauce("Tomato")
          .topping("Cheese")
          .build();

    System.out.println(myPizza);

    String orderType = "Veggie";

    Pizza.PizzaBuilder pizzaBuilder = new Pizza.PizzaBuilder().dough("Regular");

    pizzaBuilder.sauce("Pesto");

    if (orderType.equals("Veggie")) {
      pizzaBuilder.topping("Vegetables");
    } else {
      pizzaBuilder.topping("Pepperoni");
    }

    Pizza customPizza = pizzaBuilder.build();
    System.out.println(customPizza);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Pizza with Thin Crust dough, Tomato sauce, and Cheese topping.
Pizza with Regular dough, Pesto sauce, and Vegetables topping.

빌더 패턴 예시 2

// Product class
public class HttpRequest {
  private String method;
  private String url;
  private Map<String, String> headers;
  private Map<String, String> parameters;
  private String body;
  
  // private constructor
  private HttpRequest(Builder builder) {
    this.method = builder.method;
    this.url = builder.url;
    this.headers = builder.headers;
    this.parameters = builder.parameters;
    this.body = builder.body;
  }
  
  @Override
  public String toString() {
    return "HttpRequest [method=" + method + ", url=" + url + 
          ", headers=" + headers + ", parameters=" + parameters + 
          ", body=" + body + "]";
  }

  public static class Builder {
    private String method;
    private String url;
    private Map<String, String> headers = new HashMap<>();
    private Map<String, String> parameters = new HashMap<>();
    private String body;

    public Builder(String method, String url) {
      this.method = method;
      this.url = url;
    }

    public Builder addHeader(String key, String value) {
      this.headers.put(key, value);
      return this; }

    public Builder addParameter(String key, String value) {
      this.parameters.put(key, value);
      return this; }

    public Builder setBody(String body) {
      this.body = body;
      return this; }

    public HttpRequest build() {
      return new HttpRequest(this); }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
  
    HttpRequest getRequest = new HttpRequest.Builder(
      "GET", "https://example.com/api")
      .addHeader("Authorization", "Bearer token")
      .addParameter("query", "builder-pattern")
      .build();

    HttpRequest postRequest = new HttpRequest.Builder(
      "POST", "https://example.com/api")
      .addHeader("Authorization", "Bearer token")
      .setBody("{ \"name\": \"John\", \"age\": 30 }")
      .build();
      
    System.out.println(getRequest);
    System.out.println(postRequest);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

HttpRequest [method=GET, url=https://example.com/api,
 headers={Authorization=Bearer token}, parameters={query=builder-pattern},
 body=null]
 
HttpRequest [method=POST, url=https://example.com/api,
 headers={Authorization=Bearer token}, parameters={},
 body={ "name": "John", "age": 30 }]

프로토타입 패턴 (Prototype)

프로토타입 패턴 예시 1

interface Prototype {
    Prototype clone();
}

class Person implements Prototype {
  private String name;
  private int age;
  private String address;

  public Person(
    String name, int age, String address
  ) {
    this.name = name;
    this.age = age;
    this.address = address;
  }

  public Person(Person other) {
    this.name = other.name;
    this.age = other.age;
    this.address = other.address;
  }

  @Override
  public Person clone() {
    return new Person(this);
  }

  public void setAddress(String newAddress) {
    this.address = newAddress;
  }

  public void displayInfo() {
    System.out.println("Name: " + name + ", Age: " + age+ ", Address: " + address);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Person original = new Person("John", 30, "123 Main St");
    original.displayInfo();

    Person cloned = original.clone();
    cloned.setAddress("456 Clone St");

    System.out.println("\nAfter cloning and modifying the clone:");
    original.displayInfo();
    cloned.displayInfo();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Name: John, Age: 30, Address: 123 Main St

After cloning and modifying the clone:
Name: John, Age: 30, Address: 123 Main St
Name: John, Age: 30, Address: 456 Clone St

프로토타입 패턴 예시 2

// Simple Prototype interface
interface Prototype {
  Prototype clone();
}

// Document interface extending Prototype
interface Document extends Prototype {
  void setContent(String content);
  String getContent();
}

// Concrete document class
class TextDocument implements Document {
  private String content;

  public TextDocument(String content) {
    this.content = content;
  }

  @Override
  public Document clone() {
    return new TextDocument(this.content);
  }

  @Override
  public void setContent(String content) {
    this.content = content;
  }

  @Override
  public String getContent() {
    return content;
  }
}

// Template manager
class DocumentTemplateManager {
  private static final Map<String, Document> templates
    = new HashMap<>();

  public static void addTemplate(String name, Document doc) {
    templates.put(name, doc);
  }

  public static Document createDocument(String templateName) {
    Document template = templates.get(templateName);
    if (template == null) {
      throw new IllegalArgumentException(
        "Template not found: " + templateName);
    }
    return (Document) template.clone();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    DocumentTemplateManager.addTemplate(
      "welcome",
      new TextDocument("Welcome, {name}!"));
    DocumentTemplateManager.addTemplate(
      "meeting",
      new TextDocument(
          "Meeting scheduled on {date} at {time}"));

    Document welcomeDoc = DocumentTemplateManager
      .createDocument("welcome");
    welcomeDoc.setContent(
      welcomeDoc
      .getContent()
      .replace("{name}", "John Doe"));
    
    System.out.println("Welcome document: " + welcomeDoc.getContent());

    Document meetingDoc = DocumentTemplateManager
      .createDocument("meeting");
    meetingDoc.setContent(meetingDoc.getContent()
          .replace("{date}", "2024-10-01")
          .replace("{time}", "14:00"));
            
    System.out.println("Meeting document: " + meetingDoc.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Welcome document: Welcome, John Doe!
Meeting document: Meeting scheduled on 2024-10-01 at 14:00

구조 패턴

어댑터 패턴 (Adapter)

어댑터 패턴 예시 1

// Target interface
interface ModernMessageSender {
  void sendMessage(String message, String recipient);
}

// Adaptee interface
interface OldMessageSender {
  int send(String[] messageData);
}

// Concrete Adaptee
class OldMessageSystem implements OldMessageSender {
  @Override
  public int send(String[] messageData) {
    System.out.println("OldMessageSystem: Sending message: " + messageData[0] + " to " + messageData[1]);
    return 1; // Success code
  }
}

// Adapter
class MessageAdapter implements ModernMessageSender {
  private OldMessageSender oldSystem;

  public MessageAdapter(OldMessageSender oldSystem) {
    this.oldSystem = oldSystem;
  }

  @Override
  public void sendMessage(String message, String recipient) {
    String[] messageData = {message, recipient};
    int result = oldSystem.send(messageData);
    if (result != 1) {
      System.out.println("Failed to send message");
    }
  }
}

// Client
class Main {
  public static void main(String[] args) {
    OldMessageSender oldSystem = new OldMessageSystem();
    ModernMessageSender adapter = new MessageAdapter(oldSystem);

    adapter.sendMessage("Hello, World!", "john@example.com");
  }
}

// The target interface
interface DisplayAdapter {
  void display();
}

// Adaptees
class USB {
  void connectWithUsbCable(String data) {
    System.out.println("Displaying via USB with data: " + data);
  }
}

class HDMI {
  void connectWithHdmiCable(int resolution) {
    System.out.println(
    "Displaying via HDMI with resolution: " + resolution + "p"
    );
  }
}

class VGA {
  void connectWithVgaCable(boolean highQuality) {
    System.out.println(
    "Displaying via VGA with high quality: " + highQuality
    );
  }
}

class USBAdapter implements DisplayAdapter {
  private USB usb;
  private String data;
  public USBAdapter(USB usb, String data) {
    this.usb = usb;
    this.data = data;
  }

  @Override
  public void display() {
    usb.connectWithUsbCable(data);
  }
}

class HDMIAdapter implements DisplayAdapter {
  private HDMI hdmi;
  private int resolution;
  public HDMIAdapter(HDMI hdmi, int resolution) {
    this.hdmi = hdmi;
    this.resolution = resolution;
  }

  @Override
  public void display() {
    hdmi.connectWithHdmiCable(resolution);
  }
}

class VGAAdapter implements DisplayAdapter {
  private VGA vga;
  private boolean highQuality;
  public VGAAdapter(VGA vga, boolean highQuality) {
    this.vga = vga;
    this.highQuality = highQuality;
  }

  @Override
  public void display() {
    vga.connectWithVgaCable(highQuality);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    USB usb = new USB();
    HDMI hdmi = new HDMI();
    VGA vga = new VGA();

    List<DisplayAdapter> adapters = new ArrayList<>();
    adapters.add(new USBAdapter(usb, "Video Data"));
    adapters.add(new HDMIAdapter(hdmi, 1080));
    adapters.add(new VGAAdapter(vga, true));

    for (DisplayAdapter adapter : adapters) {
      adapter.display();
    }
  }
}

브리지 패턴 (Bridge)

브리지 패턴 예시 1

// Implementor
interface Device {
  void turnOn();
  void turnOff();
  void setVolume(int volume);
  boolean isEnabled();
}

// Concrete Implementors
class TV implements Device {
  private boolean on = false;
  private int volume = 30;

  @Override
  public void turnOn() {
    on = true;
    System.out.println("TV is now ON.");
  }

  @Override
  public void turnOff() {
    on = false;
    System.out.println("TV is now OFF.");
  }

  @Override
  public void setVolume(int volume) {
    this.volume = volume;
    System.out.println("TV volume set to " + volume);
  }

  @Override
  public boolean isEnabled() {
    return on;
  }
}

class Radio implements Device {
  private boolean on = false;
  private int volume = 30;

  @Override
  public void turnOn() {
    on = true;
    System.out.println("Radio is now ON.");
  }

  @Override
  public void turnOff() {
    on = false;
    System.out.println("Radio is now OFF.");
  }

  @Override
  public void setVolume(int volume) {
    this.volume = volume;
    System.out.println("Radio volume set to " + volume);
  }

  @Override
  public boolean isEnabled() {
    return on;
  }
}

// Abstraction
abstract class Remote {
  protected Device device;

  protected Remote(Device device) {
    this.device = device;
  }

  public abstract void power();

  public void volumeUp() {
    device.setVolume(device.isEnabled() ? 1 : 0);
  }

  public void volumeDown() {
    device.setVolume(device.isEnabled() ? -1 : 0);
  }
}

// Refined Abstractions
class BasicRemote extends Remote {
  public BasicRemote(Device device) {
    super(device);
  }

  @Override
  public void power() {
    if (device.isEnabled()) {
      device.turnOff();
    } else {
      device.turnOn();
    }
  }
}

class AdvancedRemote extends Remote {
  public AdvancedRemote(Device device) {
    super(device);
  }

  @Override
  public void power() {
    if (device.isEnabled()) {
      device.turnOff();
    } else {
      device.turnOn();
    }
  }

  public void mute() {
    device.setVolume(0);
    System.out.println("Device is muted.");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Device tv = new TV();
    Remote basicRemote = new BasicRemote(tv);
    basicRemote.power();
    basicRemote.volumeUp();
    
    System.out.println();

    Device radio = new Radio();
    AdvancedRemote advancedRemote = new AdvancedRemote(radio);
    advancedRemote.power();
    advancedRemote.mute();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

TV is now ON.
TV volume set to 1

Radio is now ON.
Radio volume set to 0
Device is muted.

브리지 패턴 예시 2

// Implementor
interface MessageSender {
  void sendMessage(String message);
}

// Concrete Implementors
class EmailSender implements MessageSender {
  @Override
  public void sendMessage(String message) {
    System.out.println("Sending email with message: " + message);
  }
}

class SMSSender implements MessageSender {
  @Override
  public void sendMessage(String message) {
    System.out.println("Sending SMS with message: " + message);
  }
}

// Abstraction
abstract class Message {
  protected MessageSender messageSender;

  protected Message(MessageSender messageSender) {
    this.messageSender = messageSender;
  }

  public abstract void send(String message);
}

// Refined Abstractions
class TextMessage extends Message {
  public TextMessage(MessageSender messageSender) {
    super(messageSender);
  }
  @Override
  public void send(String message) {
    messageSender.sendMessage("Text Message: " + message);
  }
}

class EncryptedMessage extends Message {
  public EncryptedMessage(MessageSender messageSender) {
    super(messageSender);
  }
  @Override
  public void send(String message) {
    String encryptedMessage = encrypt(message);
    messageSender.sendMessage(
      "Encrypted Message: " + encryptedMessage);
  }

  private String encrypt(String message) {
    return new StringBuilder(message).reverse().toString();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    MessageSender emailSender = new EmailSender();
    MessageSender smsSender = new SMSSender();

    Message textMessage = new TextMessage(emailSender);
    textMessage.send("Hello World!");

    Message encryptedMessage = new EncryptedMessage(smsSender);
    encryptedMessage.send("Hello World!");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Sending email with message: Text Message: Hello World!

Sending SMS with message: Encrypted Message: !dlroW olleH

복합체 패턴 (Composite)

복합체 패턴 예시 1

// Component
interface FileSystemComponent {
  void printName();
  int getSize();
  String getName();
}

// Leaf
class File implements FileSystemComponent {
  private String name;
  private int size;

  public File(String name, int size) {
    this.name = name;
    this.size = size;
  }

  @Override
  public void printName() {
    System.out.println("File: " + name);
  }

  @Override
  public int getSize() {
    return size;
  }

  @Override
  public String getName() {
    return name;
  }
}

// Composite
class Directory implements FileSystemComponent {
  private String name;
  private List<FileSystemComponent> components
    = new ArrayList<>();

  public Directory(String name) {
    this.name = name;
  }

  public void add(FileSystemComponent component) {
    components.add(component);
  }

  public void remove(FileSystemComponent component) {
    components.remove(component);
  }

  public void remove(String name) {
    components.removeIf(component -> component.getName().equals(name));
  }

  @Override
  public void printName() {
    System.out.println("Directory: " + name);
    for (FileSystemComponent component : components) {
      component.printName();
    }
  }

  @Override
  public int getSize() {
    int totalSize = 0;
    for (FileSystemComponent component : components) {
      totalSize += component.getSize();
    }
    return totalSize;
  }

  @Override
  public String getName() {
    return name;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    File file1 = new File("Document.txt", 100);
    File file2 = new File("Image.jpg", 200);

    Directory subDir = new Directory("SubFolder");
    subDir.add(new File("SubFile.txt", 50));

    Directory rootDir = new Directory("RootFolder");
    rootDir.add(file1);
    rootDir.add(file2);
    rootDir.add(subDir);

    System.out.println("Initial structure:");
    rootDir.printName();
    System.out.println("Total size: " + rootDir.getSize() + " KB");

    System.out.println("\nRemoving Image.jpg:");
    rootDir.remove("Image.jpg");
    rootDir.printName();
    System.out.println("Total " + rootDir.getSize() + " KB");

    System.out.println("\nRemoving SubFolder:");
    rootDir.remove(subDir);
    rootDir.printName();
    System.out.println("Total size: " + rootDir.getSize() + " KB");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Initial structure:
Directory: RootFolder
File: Document.txt
File: Image.jpg
Directory: SubFolder
File: SubFile.txt
Total size: 350 KB

Removing Image.jpg:
Directory: RootFolder
File: Document.txt
Directory: SubFolder
File: SubFile.txt
Total size: 150 KB

Removing SubFolder:
Directory: RootFolder
File: Document.txt
Total size: 100 KB

복합체 패턴 예시 2

interface UIComponent {
  void render();
  void add(UIComponent component);
  void remove(UIComponent component);
}

// Leaves
class Button implements UIComponent {
  private String label;

  public Button(String label) {
    this.label = label;
  }

  @Override
  public void render() {
    System.out.println("Button: " + label);
  }

  @Override
  public void add(UIComponent component) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void remove(UIComponent component) {
    throw new UnsupportedOperationException();
  }
}

class TextBox implements UIComponent {
  private String text;

  public TextBox(String text) {
    this.text = text;
  }

  @Override
  public void render() {
    System.out.println("TextBox: " + text);
  }

  @Override
  public void add(UIComponent component) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void remove(UIComponent component) {
    throw new UnsupportedOperationException();
  }
}

// Composites
class Panel implements UIComponent {
  private String name;
  private List<UIComponent> components = new ArrayList<>();

  public Panel(String name) {
    this.name = name;
  }

  @Override
  public void render() {
    System.out.println("Panel: " + name);
    components.forEach(UIComponent::render);
  }

  @Override
  public void add(UIComponent component) {
    components.add(component);
  }

  @Override
  public void remove(UIComponent component) {
    components.remove(component);
  }
}

class Window implements UIComponent {
  private String title;
  private List<UIComponent> components = new ArrayList<>();

  public Window(String title) {
    this.title = title;
  }

  @Override
  public void render() {
    System.out.println("Window: " + title);
    components.forEach(UIComponent::render);
  }

  @Override
  public void add(UIComponent component) {
    components.add(component);
  }

  @Override
  public void remove(UIComponent component) {
    components.remove(component);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Button submitButton = new Button("Submit");
    Button cancelButton = new Button("Cancel");
    TextBox usernameField = new TextBox("Username");

    Panel formPanel = new Panel("Form");
    formPanel.add(submitButton);
    formPanel.add(cancelButton);
    formPanel.add(usernameField);

    Window mainWindow = new Window("Main");
    mainWindow.add(formPanel);
    mainWindow.render();

    System.out.println();

    formPanel.remove(cancelButton);
    mainWindow.render();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Window: Main
Panel: Form
Button: Submit
Button: Cancel
TextBox: Username

Window: Main
Panel: Form
Button: Submit
TextBox: Username

데코레이터 패턴 (Decorator)

데코레이터 패턴 예시 1

// Component interface
interface Coffee {
  String getDescription();
  double getCost();
}

// ConcreteComponent class
class SimpleCoffee implements Coffee {
  @Override
  public String getDescription() {
    return "Simple coffee";
  }

  @Override
  public double getCost() {
    return 5.0;
  }
}

// Decorator class
class CoffeeDecorator implements Coffee {
  protected Coffee decoratedCoffee;

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

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

  @Override
  public double getCost() {
    return decoratedCoffee.getCost();
  }
}

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
  public MilkDecorator(Coffee coffee) {
    super(coffee);
  }

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

  @Override
  public double getCost() {
    return super.getCost() + 1.5;
  }
}

class SugarDecorator extends CoffeeDecorator {
  public SugarDecorator(Coffee coffee) {
    super(coffee);
  }

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

  @Override
  public double getCost() {
    return super.getCost() + 0.5;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
  
    Coffee simpleCoffee = new SimpleCoffee();
    System.out.println(simpleCoffee.getDescription() + " $" + simpleCoffee.getCost());
    
    Coffee milkCoffee = new MilkDecorator(new SimpleCoffee());
    System.out.println(milkCoffee.getDescription() + " $" + milkCoffee.getCost());

    Coffee milkAndSugarCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
    
    System.out.println(milkAndSugarCoffee.getDescription() + " $" + milkAndSugarCoffee.getCost());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Simple coffee $5.0
Simple coffee, Milk $6.5
Simple coffee, Milk, Sugar $7.0

데코레이터 패턴 예시 2

// Base Component
interface Text {
  String getContent();
}

// Concrete Component
class PlainText implements Text {
  private String content;

  public PlainText(String content) {
    this.content = content;
  }

  @Override
  public String getContent() {
    return content;
  }
}

// Base Decorator
abstract class TextDecorator implements Text {
  protected Text decoratedText;

  public TextDecorator(Text text) {
    this.decoratedText = text;
  }

  @Override
  public String getContent() {
    return decoratedText.getContent();
  }
}

// Concrete Decorators
class BoldDecorator extends TextDecorator {
  public BoldDecorator(Text text) {
    super(text);
  }

  @Override
  public String getContent() {
    return "<b>" + super.getContent() + "</b>";
  }
}

class ItalicDecorator extends TextDecorator {
  public ItalicDecorator(Text text) {
    super(text);
  }

  @Override
  public String getContent() {
    return "<i>" + super.getContent() + "</i>";
  }
}

class UnderlineDecorator extends TextDecorator {
  public UnderlineDecorator(Text text) {
    super(text);
  }

  @Override
  public String getContent() {
    return "<u>" + super.getContent() + "</u>";
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Create a plain text
    Text text = new PlainText("Hello, Decorator Pattern!");
    System.out.println("Plain text: " + text.getContent());

    text = new BoldDecorator(text);
    System.out.println("Bold text: " + text.getContent());

    text = new ItalicDecorator(text);
    System.out.println("Bold and italic text: " + text.getContent());

    text = new UnderlineDecorator(text);
    System.out.println("Bold, italic, and underlined text: " + text.getContent());

    Text anotherText = new UnderlineDecorator(new ItalicDecorator(new PlainText("Another example")));
    System.out.println("Underlined and italic text: " + anotherText.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Plain text: Hello, Decorator Pattern!
Bold text: <b>Hello, Decorator Pattern!</b>
Bold and italic text: <i><b>Hello, Decorator Pattern!</b></i>
Bold, italic, and underlined text:
 <u><i><b>Hello, Decorator Pattern!</b></i></u>
Underlined and italic text: <u><i>Another example</i></u>

파사드 패턴 (Facade)

여러 개의 복잡한 서브시스템 클래스들을 하나의 파사드 클래스로 묶습니다.
클라이언트가 서브시스템을 직접 호출하지 않고, 파사드 클래스만 호출하여 기능들을 쉽게 이용할 수 있는 패턴입니다.

파사드 클래스는 내부 구조의 복잡성을 은폐하고, 단순한 인터페이스를 제공합니다.
클라이언트는 파사드 클래스에만 의존하므로 결합도가 낮아집니다.
서브시스템 변경이 생기면 파사드 클래스 내부만 수정하면 되어서, 클라이언트 코드 변경이 최소화됩니다.

파사드 패턴 예시 1 : 스마트홈 아침 루틴

// 서브시스템 클래스 1 : 온도 조절기
public class Thermostat {
  public void setTemperature(int temperature) {
    System.out.println("집 온도 : " + temperature + "로 설정");
  }
}

// 서브시스템 클래스 2 : 전등
public class Lights {
  public void on() {
    System.out.println("전등 켜기");
  }

  public void off() {
    System.out.println("전등 끄기");
  }
}

// 서브시스템 클래스 3 : 커피머신
public class CoffeeMaker {
  public void brewCoffee() {
    System.out.println("커피 내리기");
  }
}

// 파사드 클래스
public class SmartHomeFacade {
  private Thermostat thermostat;
  private Lights lights;
  private CoffeeMaker coffeeMaker;

  public SmartHomeFacade(Thermostat thermostat, Lights lights, CoffeeMaker coffeeMaker) {
    this.thermostat = thermostat;
    this.lights = lights;
    this.coffeeMaker = coffeeMaker;
  }

  // 기상 후 루틴 함수
  public void wakeUp() {
    // 각 서브시스템 함수 호출
    thermostat.setTemperature(22);
    lights.on();
    coffeeMaker.brewCoffee();
  }

  // 외출 시 루틴 함수
  public void leaveHome() {
    // 각 서브시스템 함수 호출
    thermostat.setTemperature(18);
    lights.off();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 서브시스템 객체들 생성
    Thermostat thermostat = new Thermostat();
    Lights lights = new Lights();
    CoffeeMaker coffeeMaker = new CoffeeMaker();
  
    // 파사드 클래스 생성자로 서브시스템 객체들 전달
    SmartHomeFacade smartHome = new SmartHomeFacade(thermostat, lights, coffeeMaker);

    // 기상 후 루틴 함수 호출
    smartHome.wakeUp();
    
    System.out.println();

    // 외출 시 루틴 함수 호출
    smartHome.leaveHome();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

집 온도 : 22로 설정
전등 켜기
커피 내리기

집 온도 : 18로 설정
전등 끄기

파사드 패턴 예시 2 : 파일 시스템 작업

// 서브시스템 클래스 1 : 파일 읽기
class FileReader {
  public String readFile(String filePath) throws IOException {
    // 파일 읽기 기능
    return new String(Files.readAllBytes(Paths.get(filePath)));
  }
}

// 서브시스템 클래스 2 : 파일 쓰기
class FileWriter {
  public void writeFile(String filePath, String content) throws IOException {
    // 파일 쓰기 기능
    Files.write(Paths.get(filePath), content.getBytes());
  }
}

// 서브시스템 클래스 3 : 파일 삭제
class FileDeleter {
  public void deleteFile(String filePath) throws IOException {
    // 파일 삭제 기능
    Files.delete(Paths.get(filePath));
  }
}

// 파사드 클래스
class FileSystemFacade {
  private FileReader fileReader;
  private FileWriter fileWriter;
  private FileDeleter fileDeleter;

  // 생성자에서 서브시스템 객체들 생성
  public FileSystemFacade() {
    this.fileReader = new FileReader();
    this.fileWriter = new FileWriter();
    this.fileDeleter = new FileDeleter();
  }

  // 파일 읽기 함수
  public String readFile(String filePath) {
    try {
      // 파일 읽기 서브시스템 함수 호출
      return fileReader.readFile(filePath);
    } catch (IOException e) {
      System.err.println("Error reading file: " + e.getMessage());
      return null;
    }
  }

  // 파일 쓰기 함수
  public boolean writeFile(String filePath, String content) {
    try {
      // 파일 쓰기 서브시스템 함수 호출
      fileWriter.writeFile(filePath, content);
      return true;
    } catch (IOException e) {
      System.err.println("Error writing file: " + e.getMessage());
      return false;
    }
  }

  // 파일 삭제 함수
  public boolean deleteFile(String filePath) {
    try {
      // 파일 삭제 서브시스템 함수 호출
      fileDeleter.deleteFile(filePath);
      return true;
    } catch (IOException e) {
      System.err.println("Error deleting file: " + e.getMessage());
      return false;
    }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 파사드 클래스 객체 생성
    FileSystemFacade fs = new FileSystemFacade();

    // 파일 쓰기 함수 호출
    boolean writeSuccess = fs.writeFile("test.txt", "Hello, Facade Pattern!");
    System.out.println("File write success: " + writeSuccess);

    // 파일 읽기 함수 호출
    String content = fs.readFile("test.txt");
    System.out.println("File content: " + content);

    // 파일 삭제 함수 호출
    boolean deleteSuccess = fs.deleteFile("test.txt");
    System.out.println("File delete success: " + deleteSuccess);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

File write success: true
File content: Hello, Facade Pattern!
File delete success: true

플라이웨이트 패턴 (Flyweight)

플라이웨이트 패턴 예시 1

// Flyweight class
class Book {
  private final String title; // intrinsic state (shared)

  public Book(String title) {
    this.title = title;
  }

  public void read() {
    System.out.println("Reading the book titled: " + title);
  }
}

// FlyweightFactory class
class Bookshelf {
  private static final Map<String, Book> bookshelf = new HashMap<>();

  public static Book getBook(String title) {
    Book book = bookshelf.get(title);

    if (book == null) {
      book = new Book(title);
      bookshelf.put(title, book);
      System.out.println("Adding a new book to the bookshelf: " + title);
    } else {
      System.out.println("Reusing existing book from the bookshelf: " + title);
    }
    return book;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Book book1 = Bookshelf.getBook("Effective Java");
    book1.read();

    Book book2 = Bookshelf.getBook("Effective Java");
    book2.read();

    Book book3 = Bookshelf.getBook("Clean Code");
    book3.read();

    // Check if book1 and book2 are the same object
    System.out.println(book1 == book2 ? "Same book for 'Effective Java'." : "Different books for 'Effective Java'.");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Adding a new book to the bookshelf: Effective Java
Reading the book titled: Effective Java
Reusing existing book from the bookshelf: Effective Java
Reading the book titled: Effective Java
Adding a new book to the bookshelf: Clean Code
Reading the book titled: Clean Code
Same book for 'Effective Java'.

플라이웨이트 패턴 예시 2

// Flyweight interface
interface Font {
    void apply(String text);
}

// ConcreteFlyweight class
class ConcreteFont implements Font {
  private String font;
  private int size;
  private String color;

  public ConcreteFont(String font, int size, String color) {
    this.font = font;
    this.size = size;
    this.color = color;
  }

  @Override
  public void apply(String text) {
    System.out.println("Text: '" + text + "' with Font: " + font + ", Size: " + size + ", Color: " + color);
  }
}

// Flyweight Factory class
class FontFactory {
  private static final HashMap<String, Font> fontMap = new HashMap<>();

  public static Font getFont(String font, int size, String color) {
    String key = font + size + color;
    Font fontObject = fontMap.get(key);

    if (fontObject == null) {
      fontObject = new ConcreteFont(font, size, color);
      fontMap.put(key, fontObject);
      System.out.println("Creating font: " + key);
    } else {
      System.out.println("Reusing font: " + key);
    }
    return fontObject;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Font font1 = FontFactory.getFont("Arial", 12, "Black");
    font1.apply("Hello, World!");

    Font font2 = FontFactory.getFont("Arial", 12, "Black");
    font2.apply("Flyweight Pattern");

    Font font3 = FontFactory.getFont("Times New Roman", 14, "Blue");
    font3.apply("Design Patterns");

    Font font4 = FontFactory.getFont("Arial", 12, "Black");
    font4.apply("Another Text");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Creating font: Arial12Black
Text: 'Hello, World!' with Font: Arial, Size: 12, Color: Black
Reusing font: Arial12Black
Text: 'Flyweight Pattern' with Font: Arial, Size: 12, Color: Black
Creating font: Times New Roman14Blue
Text: 'Design Patterns' with Font: Times New Roman, Size: 14, Color: Blue
Reusing font: Arial12Black
Text: 'Another Text' with Font: Arial, Size: 12, Color: Black

프록시 패턴 (Proxy)

프록시 패턴 예시 1

// Subject interface
interface Image {
  void display();
  String getFileName();
}

// RealSubject class
class RealImage implements Image {
  private String fileName;

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

  private void loadFromDisk() {
    System.out.println("Loading " + fileName);
  }

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

  @Override
  public String getFileName() {
    return fileName;
  }
}

// Proxy class
class ProxyImage implements Image {
  private RealImage realImage;
  private String fileName;

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

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

  @Override
  public String getFileName() {
    return fileName;
  }

  public String getFileExtension() {
    int lastIndex = fileName.lastIndexOf('.');
    if (lastIndex == -1) {
      return "";
    }
    return fileName.substring(lastIndex + 1);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    ProxyImage image = new ProxyImage("test_image.jpg");

    System.out.println("File name: " + image.getFileName());
    System.out.println("File extension: " + image.getFileExtension());

    image.display();
    image.display();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

File name: test_image.jpg
File extension: jpg
Loading test_image.jpg
Displaying test_image.jpg
Displaying test_image.jpg

프록시 패턴 예시 2

// Subject interface
interface BankAccount {
  void withdraw(double amount);
  void deposit(double amount);
}

// Real Subject class
class RealBankAccount implements BankAccount {
  private double balance;
  
  public RealBankAccount(double initialBalance) {
    this.balance = initialBalance;
  }

  @Override
  public void withdraw(double amount) {
  if (balance >= amount) {
      balance -= amount;
      System.out.println(amount + " withdrawn. Current balance: " + balance);
    } else {
      System.out.println("Insufficient balance.");
    }
  }

  @Override
  public void deposit(double amount) {
    balance += amount;
    System.out.println(amount + " deposited. Current balance: " + balance);
  }
}

// Proxy class
class BankAccountProxy implements BankAccount {
  private RealBankAccount realBankAccount;
  private String userRole;
  
  public BankAccountProxy(String userRole, double initialBalance) {
    this.userRole = userRole;
    this.realBankAccount = new RealBankAccount(initialBalance);
  }

  // Check if the user has Admin access
  private boolean hasAccess() {
    return "Admin".equalsIgnoreCase(userRole);
  }

  @Override
  public void withdraw(double amount) {
    if (hasAccess()) {
      realBankAccount.withdraw(amount);
    } else {
      System.out.println("Access denied. Only Admin can withdraw.");
    }
  }

  @Override
  public void deposit(double amount) {
    realBankAccount.deposit(amount);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // User with Admin access
    BankAccount adminAccount = new BankAccountProxy("Admin", 1000);
    adminAccount.deposit(500);   // Deposit allowed
    adminAccount.withdraw(300);  // Withdraw allowed

    // User without Admin access
    BankAccount userAccount = new BankAccountProxy("User", 1000);
    userAccount.deposit(500);    // Deposit allowed
    userAccount.withdraw(300);   // Withdraw denied
  }
}

위 코드의 실행 결과는 아래와 같습니다.

500.0 deposited. Current balance: 1500.0
300.0 withdrawn. Current balance: 1200.0
500.0 deposited. Current balance: 1500.0
Access denied. Only Admin can withdraw.

행위 패턴

책임 연쇄 패턴 (Chain of Responsibility)

책임 연쇄 패턴 예시 1

abstract class Handler {
    protected Handler next;

    public void setNext(Handler next) {
        this.next = next;
    }

    public void handle(int number) {
        process(number);
        if (next != null) next.handle(number);
    }

    protected abstract void process(int number);
}

class PositiveHandler extends Handler {
    @Override
    protected void process(int number) {
        if (number > 0) {
            System.out.println(number + " is a positive number");
        }
    }
}

class EvenHandler extends Handler {
    @Override
    protected void process(int number) {
        if (number % 2 == 0) {
            System.out.println(number + " is an even number");
        }
    }
}

class DivisibleBy3Handler extends Handler {
    @Override
    protected void process(int number) {
        if (number % 3 == 0) {
            System.out.println(number + " is divible by 3");
        }
    }
}

// 클라이언트 (호출부)
public class Main {
    public static void main(String[] args) {
        Handler positive = new PositiveHandler();
        Handler even = new EvenHandler();
        Handler divisibleBy3 = new DivisibleBy3Handler();

        positive.setNext(even);
        even.setNext(divisibleBy3);

        positive.handle(-2);
        positive.handle(4);
        positive.handle(6);
    }
}

위 코드의 실행 결과는 아래와 같습니다.

-2 is an even number

4 is a positive number
4 is an even number

6 is a positive number
6 is an even number
6 is divible by 3

책임 연쇄 패턴 예시 2

enum LogLevel {
  INFO, DEBUG, WARN
}

abstract class Logger {
  protected LogLevel level;
  protected Logger nextLogger;

  public void setNextLogger(Logger nextLogger) {
    this.nextLogger = nextLogger;
  }

  public void logMessage(LogLevel level, String message) {
    if (this.level.ordinal() <= level.ordinal()) {
      write(message);
    }
    if (nextLogger != null) {
      nextLogger.logMessage(level, message);
    }
  }

  protected abstract void write(String message);
}

class ConsoleLogger extends Logger {
  public ConsoleLogger(LogLevel level) {
    this.level = level;
  }

  @Override
  protected void write(String message) {
    System.out.println("Console::Logger: " + message);
  }
}

class FileLogger extends Logger {
  public FileLogger(LogLevel level) {
    this.level = level;
  }

  @Override
  protected void write(String message) {
    System.out.println("File::Logger: " + message);
  }
}

class NetworkLogger extends Logger {
  public NetworkLogger(LogLevel level) {
    this.level = level;
  }

  @Override
  protected void write(String message) {
    System.out.println("Network::Logger: " + message);
  }
}

// 클라이언트 (호출부)
public class Main {
  private static Logger getChainOfLoggers() {
    Logger networkLogger = new NetworkLogger(LogLevel.WARN);
    Logger fileLogger = new FileLogger(LogLevel.DEBUG);
    Logger consoleLogger = new ConsoleLogger(LogLevel.INFO);

    networkLogger.setNextLogger(fileLogger);
    fileLogger.setNextLogger(consoleLogger);

    return networkLogger;
  }

  public static void main(String[] args) {
    Logger loggerChain = getChainOfLoggers();

    loggerChain.logMessage(LogLevel.INFO, 
        "This is an information.");
    
    loggerChain.logMessage(LogLevel.DEBUG,
        "This is a debug level information.");
    
    loggerChain.logMessage(LogLevel.WARN,
        "This is a warning information.");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Console::Logger: This is an information.

File::Logger: This is a debug level information.
Console::Logger: This is a debug level information.

Network::Logger: This is a warning information.
File::Logger: This is a warning information.
Console::Logger: This is a warning information.

커맨드 패턴 (Command)

커맨드 패턴 예시 1

// Receiver Class
public class Light {
  public void turnOn() {
    System.out.println("Light is ON");
  }

  public void turnOff() {
    System.out.println("Light is OFF");
  }
}

// Command Interface
public interface Command {
  void execute();
}

// Concrete Commands
public class LightOnCommand implements Command {
  private Light light;

  public LightOnCommand(Light light) {
    this.light = light;
  }

  @Override
  public void execute() {
    light.turnOn();
  }
}

public class LightOffCommand implements Command {
  private Light light;

  public LightOffCommand(Light light) {
    this.light = light;
  }

  @Override
  public void execute() {
    light.turnOff();
  }
}

// Invoker Class
public class RemoteControl {
  private Command command;

  public void setCommand(Command command) {
    this.command = command;
  }

  public void pressButton() {
    command.execute();
  }
}

public class Client {
  public static void main(String[] args) {
    Light livingRoomLight = new Light();
    
    Command lightOn = new LightOnCommand(livingRoomLight);
    
    Command lightOff = new LightOffCommand(livingRoomLight);
    
    RemoteControl remote = new RemoteControl();
    
    remote.setCommand(lightOn);
    remote.pressButton(); // "Light is ON"
    
    remote.setCommand(lightOff);
    remote.pressButton(); // "Light is OFF"
  }
}

// Receiver
public class TextEditor {
  private StringBuilder content;

  public TextEditor() {
    this.content = new StringBuilder();
  }

  public void insertText(String text, int position) {
    content.insert(position, text);
  }

  public void deleteText(int position, int length) {
    content.delete(position, position + length);
  }

  public String getTextSubstring(int start, int end) {
    return content.substring(start, end);
  }

  public String getContent() {
    return content.toString();
  }
}

// Command interface
public interface Command {
  void execute();
  void undo();
}

// Concrete commands
public class InsertTextCommand implements Command {
  private TextEditor editor;
  private String text;
  private int position;
  
  public InsertTextCommand(
    TextEditor editor, String text, int position) 
  {
    this.editor = editor;
    this.text = text;
    this.position = position;
  }

  @Override
  public void execute() {
    editor.insertText(text, position);
  }
  @Override
  public void undo() {
    editor.deleteText(position, text.length());
  }
}

public class DeleteTextCommand implements Command {
  private TextEditor editor;
  private String deletedText;
  private int position;

  public DeleteTextCommand(
    TextEditor editor, int position, int length) 
  {
    this.editor = editor;
    this.position = position;
    this.deletedText = editor.getTextSubstring(
        position, position + length);
  }

  @Override
  public void execute() {
    editor.deleteText(position, deletedText.length());
  }
  @Override
  public void undo() {
    editor.insertText(deletedText, position);
  }
}

// Invoker
public class TextEditorInvoker {
  private Stack<Command> undoStack = new Stack<>();
  private Stack<Command> redoStack = new Stack<>();

  public void executeCommand(Command command) {
    command.execute();
    undoStack.push(command);
    redoStack.clear();
  }

  public void undo() {
    if (!undoStack.isEmpty()) {
      Command command = undoStack.pop();
      command.undo();
      redoStack.push(command);
    }
  }

  public void redo() {
    if (!redoStack.isEmpty()) {
      Command command = redoStack.pop();
      command.execute();
      undoStack.push(command);
    }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    TextEditor editor = new TextEditor();
    TextEditorInvoker invoker = new TextEditorInvoker();

    Command insertHello = new InsertTextCommand(editor, "Hello, ", 0);
    invoker.executeCommand(insertHello);

    Command insertWorld = new InsertTextCommand(editor, "World!", 7);
    invoker.executeCommand(insertWorld);

    System.out.println("Current text: " + editor.getContent());

    invoker.undo();
    System.out.println("After undo: " + editor.getContent());

    invoker.redo();
    System.out.println("After redo: " + editor.getContent());

    Command deleteCommand = new DeleteTextCommand(editor, 0, 7);
    invoker.executeCommand(deleteCommand);
    System.out.println("After delete: " + editor.getContent());

    invoker.undo();
    System.out.println("Final text: " + editor.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Current text: Hello, World!
After undo: Hello, 
After redo: Hello, World!
After delete: World!
Final text: Hello, World!

인터프리터 패턴 (Interpreter)

인터프리터 패턴 예시 1

public interface Expression {
  int interpret();
}

public class Number implements Expression {
  private int number;

  public Number(int number) {
    this.number = number;
  }

  @Override
  public int interpret() {
    return this.number;
  }
}

public class Add implements Expression {
  private Expression leftExpression;
  private Expression rightExpression;

  public Add(
    Expression leftExpression,
    Expression rightExpression
  ) {
    this.leftExpression = leftExpression;
    this.rightExpression = rightExpression;
  }

  @Override
  public int interpret() {
      return leftExpression.interpret() + rightExpression.interpret();
  }
}

public class Subtract implements Expression {
  private Expression leftExpression;
  private Expression rightExpression;

  public Subtract(
    Expression leftExpression,
    Expression rightExpression
  ) {
    this.leftExpression = leftExpression;
    this.rightExpression = rightExpression;
  }

  @Override
  public int interpret() {
    return leftExpression.interpret() - rightExpression.interpret();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
  
    Expression five = new Number(5);
    Expression two = new Number(2);
    Expression three = new Number(3);

    Expression addExpression = new Add(five, two);
    
    Expression subtractExpression = new Subtract(addExpression, three);

    System.out.println("(5 + 2) - 3 = " + subtractExpression.interpret());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

(5 + 2) - 3 = 4

인터프리터 패턴 예시 2

// Context
class Context {
  private Map<String, List<Map<String, String>>> tables;

  public Context() {
    this.tables = new HashMap<>();

    // Initialize sample data
    List<Map<String, String>> users = new ArrayList<>();

    users.add(new HashMap<String, String>() );

    users.add(new HashMap<String, String>() );

    tables.put("users", users);
  }

  public List<Map<String, String>> getTable(String name) {
    return tables.get(name);
  }

  public void setTable(
    String name, List<Map<String, String>> table
  ) {
    tables.put(name, table);
  }
}

// Abstract Expression
interface Expression {
  List<Map<String, String>> interpret(Context context);
}

// WHERE clause expression
class WhereExpression implements Expression {
  private String column;
  private String operator;
  private String value;
  private String tableName;

  public WhereExpression(
    String tableName, String column,
    String operator, String value
  ) {
    this.tableName = tableName;
    this.column = column;
    this.operator = operator;
    this.value = value;
  }

  @Override
  public List<Map<String, String>> interpret(Context context) {
    List<Map<String, String>> result = new ArrayList<>();
    List<Map<String, String>> table = context.getTable(tableName);
    for (Map<String, String> row : table) {
      if (evaluate(row.get(column), operator, value)) {
        result.add(row);
      }
    }
    return result;
  }

  private boolean evaluate(
    String columnValue, String operator, String value
  ) {
    switch (operator) {
      case "=":
        return columnValue.equals(value);
      case ">":
        return Integer.parseInt(columnValue)
          > Integer.parseInt(value);
      case "<":
        return Integer.parseInt(columnValue)
          < Integer.parseInt(value);
      default:
        return false;
    }
  }
}

// SELECT statement expression
class SelectExpression implements Expression {
  private String tableName;
  private List<String> columns;
  private Expression whereClause;

  public SelectExpression(String tableName, List<String> columns, Expression whereClause) {
    this.tableName = tableName;
    this.columns = columns;
    this.whereClause = whereClause;
  }

  @Override
  public List<Map<String, String>> interpret(Context context) {
    List<Map<String, String>> table = context.getTable(tableName);
    List<Map<String, String>> result = new ArrayList<>();

    for (Map<String, String> row : table) {
      Context rowContext = new Context();
      rowContext.setTable(
        tableName, Collections.singletonList(row));

      if (whereClause == null || !whereClause.interpret(rowContext).isEmpty()) {
        Map<String, String> newRow = new HashMap<>();
        
        for (String column : columns) {
          if (column.equals("*")) {
            newRow.putAll(row);
          } else {
            newRow.put(column, row.get(column));
          }
        }
        result.add(newRow);
      }
    }

    return result;
  }
}

// SQL Parser
class SQLParser {
  public static Expression parse(String query) {
    String[] parts = query.split("\\s+");
    if (!parts[0].equalsIgnoreCase("SELECT")) {
      throw new RuntimeException(
        "Only SELECT statements are supported");
    }

    List<String> columns = Arrays.asList(parts[1].split(","));
    String tableName = parts[3];

    Expression whereClause = null;
    if (parts.length > 4 
      && parts[4].equalsIgnoreCase("WHERE")) {
      whereClause = new WhereExpression(
        tableName, parts[5], parts[6], parts[7]);
    }

    return new SelectExpression(tableName, columns, whereClause);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Context context = new Context();

    // Test query 1: Select all columns from users
    String query1 = "SELECT * FROM users";
    Expression expr1 = SQLParser.parse(query1);
    List<Map<String, String>> result1 = expr1.interpret(context);
    
    System.out.println("Result of query: " + query1);
    for (Map<String, String> row : result1) {
      System.out.println(row);
    }

    // Test query 2: Select name and age of users older than 27
    String query2 = "SELECT name,age FROM users WHERE age > 27";
    Expression expr2 = SQLParser.parse(query2);
    List<Map<String, String>> result2 = expr2.interpret(context);
    
    System.out.println("\nResult of query: " + query2);
    for (Map<String, String> row : result2) {
      System.out.println(row);
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Result of query: SELECT * FROM users
{name=John, id=1, age=30}
{name=Jane, id=2, age=25}

Result of query: SELECT name,age FROM users WHERE age > 27
{name=John, age=30}

반복자 패턴 (Iterator)

반복자 패턴 예시 1

// Iterator Interface
interface MyIterator {
  boolean hasNext();
  Object next();
}

// Aggregate Interface
interface Collection {
  MyIterator createIterator();
}

// Concrete Aggregate
class MyList implements Collection {
  private Object[] items;
  private int last = 0;

  public MyList(int size) {
    items = new Object[size];
  }

  public void add(Object item) {
    if (last < items.length) {
      items[last] = item;
      last++;
    }
  }

  public Object get(int index) {
    return items[index];
  }

  public int size() {
    return last;
  }

  @Override
  public MyIterator createIterator() {
    return new MyListIterator(this);
  }
}

// ConcreteIterator
class MyListIterator implements MyIterator {
  private MyList list;
  private int index;

  public MyListIterator(MyList list) {
    this.list = list;
    this.index = 0;
  }

  @Override
  public boolean hasNext() {
    return index < list.size();
  }

  @Override
  public Object next() {
    if (this.hasNext()) {
      return list.get(index++);
    }
    return null;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    MyList list = new MyList(3);
    list.add("A");
    list.add("B");
    list.add("C");

    MyIterator iterator = list.createIterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

A
B
C

반복자 패턴 예시 2

// FileSystemItem interface
interface FileSystemItem {
  String getName();
}

// File class
class File implements FileSystemItem {
  private String name;

  public File(String name) {
    this.name = name;
  }

  @Override
  public String getName() {
    return name;
  }
}

// Directory class
class Directory implements FileSystemItem {
  private String name;
  private List<FileSystemItem> contents = new ArrayList<>();

  public Directory(String name) {
    this.name = name;
  }

  public void add(FileSystemItem item) {
    contents.add(item);
  }

  public List<FileSystemItem> getContents() {
    return contents;
  }

  @Override
  public String getName() {
    return name;
  }
}

// FileSystemIterator interface
interface FileSystemIterator {
  boolean hasNext();
  FileSystemItem next();
}

// DepthFirstIterator class
class DepthFirstIterator implements FileSystemIterator {
  private Stack<FileSystemItem> stack = new Stack<>();

  public DepthFirstIterator(Directory root) {
    stack.push(root);
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public FileSystemItem next() {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }

    FileSystemItem current = stack.pop();
    if (current instanceof Directory) {
      List<FileSystemItem> contents = ((Directory) current).getContents();
      for (int i = contents.size() - 1; i >= 0; i--) {
        stack.push(contents.get(i));
      }
    }
    return current;
  }
}

// BreadthFirstIterator class
class BreadthFirstIterator implements FileSystemIterator {
  private Queue<FileSystemItem> queue = new LinkedList<>();

  public BreadthFirstIterator(Directory root) {
    queue.offer(root);
  }

  @Override
  public boolean hasNext() {
    return !queue.isEmpty();
  }

  @Override
  public FileSystemItem next() {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }

    FileSystemItem current = queue.poll();
    if (current instanceof Directory) {
      queue.addAll(((Directory) current).getContents());
    }
    return current;
  }
}

// FileSystem class
class FileSystem {
  private Directory root;

  public FileSystem(Directory root) {
    this.root = root;
  }

  public FileSystemIterator depthFirstIterator() {
    return new DepthFirstIterator(root);
  }

  public FileSystemIterator breadthFirstIterator() {
    return new BreadthFirstIterator(root);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Directory root = new Directory("root");
    Directory home = new Directory("home");
    Directory user = new Directory("user");
    File file1 = new File("file1.txt");
    File file2 = new File("file2.txt");
    File file3 = new File("file3.txt");

    root.add(home);
    home.add(user);
    user.add(file1);
    user.add(file2);
    home.add(file3);

    FileSystem fileSystem = new FileSystem(root);

    System.out.println("Depth-First Traversal:");
    FileSystemIterator depthIterator = fileSystem.depthFirstIterator();
    while (depthIterator.hasNext()) {
      System.out.println(depthIterator.next().getName());
    }

    System.out.println("\nBreadth-First Traversal:");
    FileSystemIterator breadthIterator = fileSystem.breadthFirstIterator();
    while (breadthIterator.hasNext()) {
      System.out.println(breadthIterator.next().getName());
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Depth-First Traversal:
root
home
user
file1.txt
file2.txt
file3.txt

Breadth-First Traversal:
root
home
user
file3.txt
file1.txt
file2.txt

중재자 패턴 (Mediator)

중재자 패턴 예시 1

// Mediator interface
public interface ChatMediator {
  void sendMessage(String message, User user);
  void addUser(User user);
}

// Concrete implementation of the Mediator interface
public class ChatMediatorImpl implements ChatMediator {
  private List<User> users;

  public ChatMediatorImpl() {
    this.users = new ArrayList<>();
  }

  @Override
  public void addUser(User user) {
    this.users.add(user);
  }

  @Override
  public void sendMessage(String message, User user) {
    for (User u : this.users) {
      if (u != user) {
        u.receive(message);
      }
    }
  }
}

// Abstract User class
public abstract class User {
  protected ChatMediator mediator;
  protected String name;

  public User(ChatMediator mediator, String name) {
    this.mediator = mediator;
    this.name = name;
  }

  public abstract void send(String message);
  public abstract void receive(String message);
}

// Concrete implementation of the User class
public class UserImpl extends User {

  public UserImpl(ChatMediator mediator, String name) {
    super(mediator, name);
  }

  @Override
  public void send(String message) {
    System.out.println(this.name + ": Sending Message = " + message);
    mediator.sendMessage(message, this);
  }

  @Override
  public void receive(String message) {
    System.out.println(kthis.name + ": Received Message = " + message);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    ChatMediator mediator = new ChatMediatorImpl();

    User user1 = new UserImpl(mediator, "John");
    User user2 = new UserImpl(mediator, "Jane");
    User user3 = new UserImpl(mediator, "Bob");
    User user4 = new UserImpl(mediator, "Alice");

    mediator.addUser(user1);
    mediator.addUser(user2);
    mediator.addUser(user3);
    mediator.addUser(user4);

    user1.send("Hi All");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

John: Sending Message = Hi All
Jane: Received Message = Hi All
Bob: Received Message = Hi All
Alice: Received Message = Hi All

중재자 패턴 예시 2

interface AirportMediator {
  boolean isRunwayAvailable();
  void setRunwayAvailability(boolean status);
}

class AirportControlTower implements AirportMediator {
  private boolean isRunwayAvailable = true;

  public boolean isRunwayAvailable() {
    return isRunwayAvailable;
  }

  public void setRunwayAvailability(boolean status) {
    isRunwayAvailable = status;
  }
}

class Flight {
  private AirportMediator mediator;
  private String flightNumber;

  public Flight(AirportMediator mediator, String flightNumber) {
    this.mediator = mediator;
    this.flightNumber = flightNumber;
  }

  public void land() {
    if (mediator.isRunwayAvailable()) {
      System.out.println("Flight " + flightNumber + " is landing.");
      mediator.setRunwayAvailability(false);
    } else {
      System.out.println("Flight " + flightNumber + " is waiting to land.");
    }
  }
}

class Runway {
  private AirportMediator mediator;

  public Runway(AirportMediator mediator) {
    this.mediator = mediator;
  }

  public void clear() {
    System.out.println("Runway is clear.");
    mediator.setRunwayAvailability(true);
  }
}

public class AirportSystem {
  public static void main(String[] args) {
    AirportMediator controlTower = new AirportControlTower();

    Flight flight1 = new Flight(controlTower, "KE123");
    Flight flight2 = new Flight(controlTower, "OZ456");
    Runway runway = new Runway(controlTower);

    flight1.land();
    flight2.land();
    runway.clear();
    flight2.land();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Flight KE123 is landing.
Flight OZ456 is waiting to land.
Runway is clear.
Flight OZ456 is landing.

메멘토 패턴 (Memento)

메멘토 패턴 예시 1

// Memento
class GameMemento {
  private String level;
  private int score;

  public GameMemento(String level, int score) {
    this.level = level;
    this.score = score;
  }

  public String getLevel() {
    return level;
  }

  public int getScore() {
    return score;
  }
}

// Originator
class Game {
  private String level;
  private int score;

  public void set(String level, int score) {
    this.level = level;
    this.score = score;
    System.out.println("Game state set to - Level: " + level + ", Score: " + score);
  }

  public GameMemento save() {
    return new GameMemento(level, score);
  }

  public void restore(GameMemento memento) {
    this.level = memento.getLevel();
    this.score = memento.getScore();
    System.out.println("Game state restored to - Level: " + level + ", Score: " + score);
  }
}

// Caretaker
class GameCaretaker {
  private List<GameMemento> mementoList = new ArrayList<>();

  public void add(GameMemento state) {
    mementoList.add(state);
  }

  public GameMemento get(int index) {
    return mementoList.get(index);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Game game = new Game();
    GameCaretaker caretaker = new GameCaretaker();

    game.set("Level 1", 100);
    caretaker.add(game.save());

    game.set("Level 2", 200);
    caretaker.add(game.save());

    game.set("Level 3", 300);

    game.restore(caretaker.get(1));
    game.restore(caretaker.get(0));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Game state set to - Level: Level 1, Score: 100
Game state set to - Level: Level 2, Score: 200
Game state set to - Level: Level 3, Score: 300
Game state restored to - Level: Level 2, Score: 200
Game state restored to - Level: Level 1, Score: 100

메멘토 패턴 예시 2

// Memento
class DocumentMemento {
  private final String content;

  public DocumentMemento(String content) {
    this.content = content;
  }

  public String getContent() {
    return content;
  }
}

// Originator
class Document {
  private StringBuilder content;

  public Document() {
    this.content = new StringBuilder();
  }

  public void write(String text) {
    content.append(text);
  }

  public String getContent() {
    return content.toString();
  }

  public DocumentMemento save() {
    return new DocumentMemento(content.toString());
  }

  public void restore(DocumentMemento memento) {
    this.content = new StringBuilder(memento.getContent());
  }
}

// Caretaker
class DocumentHistory {
  private final Stack<DocumentMemento> history = new Stack<>();

  public void push(DocumentMemento memento) {
    history.push(memento);
  }

  public DocumentMemento pop() {
    if (!history.isEmpty()) {
      return history.pop();
    }
    return null;
  }
}

// Client
class Editor {
  private final Document document;
  private final DocumentHistory history;

  public Editor() {
    this.document = new Document();
    this.history = new DocumentHistory();
  }

  public void write(String text) {
    history.push(document.save());
    document.write(text);
  }

  public void undo() {
    DocumentMemento memento = history.pop();
    if (memento != null) {
      document.restore(memento);
    }
  }

  public String getContent() {
    return document.getContent();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Editor editor = new Editor();

    editor.write("Hello, ");
    editor.write("this is Memento pattern. ");
    System.out.println(editor.getContent());

    editor.undo();
    System.out.println(editor.getContent());

    editor.write("This is an example implemented in Java.");
    System.out.println(editor.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Hello, this is Memento pattern. 
Hello, 
Hello, This is an example implemented in Java.

관찰자 패턴 (Opserver)

관찰자 패턴 예시 1

// Subject (Publisher) interface
interface Subject {
  void registerObserver(Observer observer);
  void removeObserver(Observer observer);
  void notifyObservers();
}

// Observer interface
interface Observer {
  void update(String news);
}

// Concrete Subject
class NewsAgency implements Subject {
  private List<Observer> observers = new ArrayList<>();
  private String news;

  @Override
  public void registerObserver(Observer observer) {
    observers.add(observer);
  }
  @Override
  public void removeObserver(Observer observer) {
    observers.remove(observer);
  }

  @Override
  public void notifyObservers() {
    for (Observer observer : observers) {
      observer.update(news);
    }
  }

  public void setNews(String news) {
    this.news = news;
    notifyObservers();
  }
}

// Concrete Observer
class NewsChannel implements Observer {
  private String name;

  public NewsChannel(String name) {
    this.name = name;
  }

  @Override
  public void update(String news) {
    System.out.println(name + " received news: " + news);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    NewsAgency agency = new NewsAgency();

    NewsChannel channel1 = new NewsChannel("Channel 1");
    NewsChannel channel2 = new NewsChannel("Channel 2");

    agency.registerObserver(channel1);
    agency.registerObserver(channel2);

    agency.setNews("Breaking news: Observer Pattern in action!");

    agency.removeObserver(channel2);

    agency.setNews("Another update: Channel 2 unsubscribed.");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Channel 1 received news: Breaking news: Observer Pattern in action!
Channel 2 received news: Breaking news: Observer Pattern in action!
Channel 1 received news: Another update: Channel 2 unsubscribed.

관찰자 패턴 예시 2

// Subject interface
interface WeatherStation {
  void registerObserver(WeatherObserver o);
  void removeObserver(WeatherObserver o);
  void notifyObservers();
}

// Observer interface
interface WeatherObserver {
  void update(float temp, float humidity, float pressure);
}

// Concrete Subject
class WeatherData implements WeatherStation {
  private List<WeatherObserver> observers = new ArrayList<>();
  private float temperature, humidity, pressure;

  public void setMeasurements(
    float temperature, float humidity, float pressure
  ) {
    this.temperature = temperature;
    this.humidity = humidity;
    this.pressure = pressure;
    notifyObservers();
  }

  @Override
  public void registerObserver(WeatherObserver o) {
    observers.add(o);
  }
  
  @Override
  public void removeObserver(WeatherObserver o) {
    observers.remove(o);
  }

  @Override
  public void notifyObservers() {
    for (WeatherObserver observer : observers) {
      observer.update(temperature, humidity, pressure);
    }
  }
}

// Concrete Observer 1
class CurrentConditionsDisplay implements WeatherObserver {
  @Override
  public void update(float temp, float humidity, float pressure) {
    System.out.println("Current: " + temp + "F, " + humidity + "% humidity");
  }
}

// Concrete Observer 2
class StatisticsDisplay implements WeatherObserver {
  @Override
  public void update(float temp, float humidity, float pressure) {
    System.out.println("Avg/Max/Min temp: " + temp + "/" + (temp + 2) + "/" + (temp - 2));
  }
}

// Concrete Observer 3
class ForecastDisplay implements WeatherObserver {
  @Override
  public void update(float temp, float humidity, float pressure) {
    System.out.println("Forecast: " + (pressure < 29.92 ? "Rain" : "Sunny"));
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    WeatherData weatherData = new WeatherData();

    CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
    StatisticsDisplay statisticsDisplay = new StatisticsDisplay();
    ForecastDisplay forecastDisplay = new ForecastDisplay();

    weatherData.registerObserver(currentDisplay);
    weatherData.registerObserver(statisticsDisplay);
    weatherData.registerObserver(forecastDisplay);

    weatherData.setMeasurements(80, 65, 30.4f);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Current: 80.0F, 65.0% humidity
Avg/Max/Min temp: 80.0/82.0/78.0
Forecast: Sunny

발행-구독 패턴 (Publisher-Subscriber)

발행-구독 패턴 예시 1

class Message {
  private String content;
  private String topic;

  public Message(String content, String topic) {
    this.content = content;
    this.topic = topic;
  }

  public String getContent() {
    return content;
  }

  public String getTopic() {
    return topic;
  }
}

// Publisher interface
interface Publisher {
  void publish(Message message);
}

// Subscriber interface
interface Subscriber {
  void update(Message message);
}

class Broker {
  private Map<String, List<Subscriber>> subscribers = new HashMap<>();

  public void subscribe(String topic, Subscriber subscriber) {
    subscribers.computeIfAbsent(topic, k -> new ArrayList<>()).add(subscriber);
  }

  public void publish(Message message) {
    List<Subscriber> topicSubscribers = subscribers.get(message.getTopic());
    
    if (topicSubscribers != null) {
      for (Subscriber subscriber : topicSubscribers) {
        subscriber.update(message);
      }
    }
  }
}

// Concrete Publisher
class NewsPublisher implements Publisher {
  private Broker broker;

  public NewsPublisher(Broker broker) {
    this.broker = broker;
  }

  @Override
  public void publish(Message message) {
    System.out.println("Publishing: " + message.getContent() + " on topic: " + message.getTopic());
    broker.publish(message);
  }
}

// Concrete Subscriber
class NewsSubscriber implements Subscriber {
  private String name;

  public NewsSubscriber(String name) {
    this.name = name;
  }

  @Override
  public void update(Message message) {
    System.out.println(name + " received: " + message.getContent() + " on topic: " + message.getTopic());
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Broker broker = new Broker();

    NewsPublisher publisher = new NewsPublisher(broker);

    NewsSubscriber subscriber1 = new NewsSubscriber("Subscriber 1");
    NewsSubscriber subscriber2 = new NewsSubscriber("Subscriber 2");

    broker.subscribe("sports", subscriber1);
    broker.subscribe("weather", subscriber2);
    broker.subscribe("sports", subscriber2);

    publisher.publish(new Message("Liverpool won the match", "sports"));
    publisher.publish(new Message("It's sunny today", "weather"));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Publishing: Liverpool won the match on topic: sports
Subscriber 1 received: Liverpool won the match on topic: sports
Subscriber 2 received: Liverpool won the match on topic: sports
Publishing: It's sunny today on topic: weather
Subscriber 2 received: It's sunny today on topic: weather

발행-구독 패턴 예시 2

// Publisher
class MarketingDepartment {
  private EmailDeliveryService emailService;
  private String eventType;

  public MarketingDepartment(
    EmailDeliveryService emailService,
    String eventType
  ) {
    this.emailService = emailService;
    this.eventType = eventType;
  }

  public void launchCampaign(String message) {
    System.out.println("Launching campaign: " + message);
    emailService.sendEmails(eventType, message);
  }
}

// Subscriber Interface
interface Customer {
  void receiveEmail(String message);
}

// Concrete Subscriber
class IndividualCustomer implements Customer {
  private String name;

  public IndividualCustomer(String name) {
    this.name = name;
  }

  @Override
  public void receiveEmail(String message) {
    System.out.println(name + " is receiving email async: " + message);
    try {
      Thread.sleep(5000);  // Simulating email reading time
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    System.out.println(name + " finished reading email: " + message);
  }
}

// Broker (Asynchronous)
class EmailDeliveryService {
  private Map<String, List<Customer>> customerGroups = new HashMap<>();
  private ExecutorService executor = Executors.newCachedThreadPool();

  public void subscribe(String eventType, Customer customer) {
    customerGroups.computeIfAbsent(eventType, k -> new ArrayList<>()).add(customer);
  }

  public void sendEmails(String eventType, String message) {
    List<Customer> customers = customerGroups.get(eventType);
    if (customers != null) {
      for (Customer customer : customers) {
        executor.submit(() -> customer.receiveEmail(message));
      }
    }
  }
  public void shutdown() { executor.shutdown(); }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    EmailDeliveryService emailService = new EmailDeliveryService();

    MarketingDepartment marketing = new MarketingDepartment(emailService, "ProductLaunch");

    Customer customer1 = new IndividualCustomer("Customer 1");
    Customer customer2 = new IndividualCustomer("Customer 2");

    emailService.subscribe("ProductLaunch", customer1);
    emailService.subscribe("ProductLaunch", customer2);

    marketing.launchCampaign("New Product");

    Customer customer3 = new IndividualCustomer("Customer 3");
    emailService.subscribe("ProductLaunch", customer3);

    marketing.launchCampaign("Update");

    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }

    emailService.shutdown();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Launching campaign: New Product
Launching campaign: Update
Customer 2 is receiving email async: New Product
Customer 3 is receiving email async: Update
Customer 2 is receiving email async: Update
Customer 1 is receiving email async: New Product
Customer 1 is receiving email async: Update
Customer 3 finished reading email: Update
Customer 1 finished reading email: Update
Customer 2 finished reading email: Update
Customer 2 finished reading email: New Product
Customer 1 finished reading email: New Product

상태 패턴 (State)

상태 패턴 예시 1

public interface State {
  void open(Door door);
  void close(Door door);
}

public class ClosedState implements State {
  @Override
  public void open(Door door) {
    System.out.println("Door is now Open.");
    door.setState(new OpenState());
  }

  @Override
  public void close(Door door) {
    System.out.println("Door is already Closed.");
  }
}

public class OpenState implements State {
  @Override
  public void open(Door door) {
    System.out.println("Door is already Open.");
  }

  @Override
  public void close(Door door) {
    System.out.println("Door is now Closed.");
    door.setState(new ClosedState());
  }
}

public class Door {
  private State state;
  public Door() {
    this.state = new ClosedState();
  }

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

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

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

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Door door = new Door();
    
    door.open();  // "Door is now Open."
    door.open();  // "Door is already Open."
    door.close(); // "Door is now Closed."
    door.close(); // "Door is already Closed."
  }
}

상태 패턴 예시 2

public interface State {
  void play(VideoPlayer player);
  void stop(VideoPlayer player);
}

public class StoppedState implements State {
  @Override
  public void play(VideoPlayer player) {
    System.out.println("Starting the video.");
    player.setState(new PlayingState());
  }

  @Override
  public void stop(VideoPlayer player) {
    System.out.println("Video is already stopped.");
  }
}

public class PlayingState implements State {
  @Override
  public void play(VideoPlayer player) {
    System.out.println("Video is already playing.");
  }

  @Override
  public void stop(VideoPlayer player) {
    System.out.println("Pausing the video.");
    player.setState(new PausedState());
  }
}

public class PausedState implements State {
  @Override
  public void play(VideoPlayer player) {
    System.out.println("Resuming the video.");
    player.setState(new PlayingState());
  }

  @Override
  public void stop(VideoPlayer player) {
    System.out.println("Stopping the video.");
    player.setState(new StoppedState());
  }
}

public class VideoPlayer {
  private State state;

  public VideoPlayer() {
    // Set initial state Stopped
    this.state = new StoppedState();
  }

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

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

  public void stop() {
    state.stop(this);
  }
}public interface State {
  void play(VideoPlayer player);
  void stop(VideoPlayer player);
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    VideoPlayer player = new VideoPlayer();
    
    player.play();   // "Starting the video."
    player.play();   // "Video is already playing."
    player.stop();   // "Pausing the video."
    player.play();   // "Resuming the video."
    player.stop();   // "Pausing the video."
    player.stop();   // "Stopping the video."
    player.stop();   // "Video is already stopped."
  }
}

상태 패턴이 적용되지 않은, 조건문 방식 예시

public class VideoPlayer {
  private String state;

  public VideoPlayer() {
    this.state = "Stopped";
  }

  public void play() {
    if (state.equals("Stopped")) {
      System.out.println("Starting the video.");
      state = "Playing";
    } else if (state.equals("Playing")) {
      System.out.println("Video is already playing.");
    } else if (state.equals("Paused")) {
      System.out.println("Resuming the video.");
      state = "Playing";
    }
  }

  public void stop() {
    if (state.equals("Playing")) {
      System.out.println("Pausing the video.");
      state = "Paused";
    } else if (state.equals("Paused")) {
      System.out.println("Stopping the video.");
      state = "Stopped";
    } else if (state.equals("Stopped")) {
      System.out.println("Video is already stopped.");
    }
  }

  public static void main(String[] args) {
    VideoPlayer player = new VideoPlayer();
    
    player.play();   // "Starting the video."
    player.play();   // "Video is already playing."
    player.stop();   // "Pausing the video."
    player.play();   // "Resuming the video."
    player.stop();   // "Pausing the video."
    player.stop();   // "Stopping the video."
    player.stop();   // "Video is already stopped."
  }
}

전략 패턴 (Strategy)

특정 인터페이스를 구현하는 여러 전략 클래스들을 두고, 런타임 시 필요에 따라 갈아끼우는 패턴입니다.

중간 다리인 Context 클래스는 실제 기능 로직을 구현하지 않고 인터페이스에만 의존합니다.
상위 모듈이 하위 모듈의 구현이 아니라 추상화에 의존해야 하는 DIP(의존 역전 원칙)을 충족하게 됩니다.

코드 변경 시 전략 구현체만 수정 및 추가하면 되어서, SOLID 원칙의 개방-폐쇄 원칙도 준수하는 코드입니다.

전략 패턴 예시 1 : 결제 방식

// 결제 전략 인터페이스
interface PaymentStrategy {
  // 결제 전략 함수 정의
  void pay(int amount);
}

// 결제 전략 클래스 1 : 신용카드
class CreditCardPayment implements PaymentStrategy {
  private String name;
  private String cardNumber;

  public CreditCardPayment(String name, String cardNumber) {
    this.name = name;
    this.cardNumber = cardNumber;
  }

  // 결제 전략 함수 구현
  @Override
  public void pay(int amount) {
    System.out.println(amount + " paid with credit card");
  }
}

// 결제 전략 클래스 2 : 페이팔
class PayPalPayment implements PaymentStrategy {
  private String email;

  public PayPalPayment(String email) {
    this.email = email;
  }

  // 결제 전략 함수 구현
  @Override
  public void pay(int amount) {
    System.out.println(amount + " paid using PayPal");
  }
}

// 중간 Context 클래스 : 쇼핑 카트
class ShoppingCart {
  // 결제 전략 인터페이스 필드
  private PaymentStrategy paymentStrategy;

  // 결제 전략 인터페이스 변경 함수 (파라미터로 인터페이스를 구현한 클래스가 올 수 있음)
  public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  // 쇼핑 카트 나갈 때 실행되는 함수
  public void checkout(int amount) {
    // 결제 전략에 따른 계산 기능 수행
    paymentStrategy.pay(amount);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Context 클래스 생성
    ShoppingCart cart = new ShoppingCart();

    // 결제 전략 클래스 교체
    cart.setPaymentStrategy(new CreditCardPayment("John Doe", "1234567890123456"));

    // 신용카드로 100원 결제
    cart.checkout(100);

    // 결제 전략 클래스 교체
    cart.setPaymentStrategy(new PayPalPayment("johndoe@example.com"));

    // 페이팔로 200원 결제
    cart.checkout(200);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

100 paid with credit card
200 paid using PayPal

전략 패턴 예시 2 : 문자열 인코딩 알고리즘

// 문자열 인코딩 전략 인터페이스
interface EncodingStrategy {
  // 인코딩 전략 함수 정의
  String encode(String data);
}

// 인코딩 전략 클래스 1 : 반복되는 문자 횟수 인코딩
class RunLengthEncoding implements EncodingStrategy {

  // 인코딩 전략 함수 구현
  @Override
  public String encode(String data) {
    StringBuilder encoded = new StringBuilder();
    int count = 1;
    for (int i = 1; i <= data.length(); i++) {
      if (i < data.length() && data.charAt(i) == data.charAt(i - 1)) {
        count++;
      } else {
        encoded.append(data.charAt(i - 1));
        encoded.append(count);
        count = 1;
      }
    }
    return encoded.toString();
  }
}

// 인코딩 전략 클래스 2 : 단순 치환 인코딩
class SimpleReplacementEncoding implements EncodingStrategy {

  // 인코딩 전략 함수 구현
  @Override
  public String encode(String data) {
    return data.replace("a", "1")
               .replace("e", "2")
               .replace("i", "3")
               .replace("o", "4")
               .replace("u", "5");
  }
}

// 중간 Context 클래스
class Encoder {
  private EncodingStrategy strategy;

  // 인코딩 전략 교체 함수
  public void setEncodingStrategy(EncodingStrategy strategy) {
    this.strategy = strategy;
  }

  public String encode(String data) {
    // 전략에 따른 인코딩 기능 수행
    return strategy.encode(data);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Context 클래스 생성
    Encoder encoder = new Encoder();

    String data = "aabcccccaaa";

    // 인코딩 전략 교체
    encoder.setEncodingStrategy(new RunLengthEncoding());

    // 문자열 인코딩 : 반복되는 문자 횟수 인코딩
    System.out.println("Run-Length Encoding : " + encoder.encode(data));

    // 인코딩 전략 교체
    encoder.setEncodingStrategy(new SimpleReplacementEncoding());

    // 문자열 인코딩 : 단순 치환 인코딩
    System.out.println("Simple Replacement Encoding : " + encoder.encode(data));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Run-Length Encoding : a2b1c5a3

Simple Replacement Encoding : 11bccccc111

템플릿 메서드 패턴 (Template Method)

전체 과정이 정해진 순서에 따라 실행되어야 하는 경우 사용되는 패턴입니다.

상위 추상 클래스에 전체 순서 템플릿 함수를 정의하고,
각 하위 클래스들에서 추상 메서드를 구현하여 세부 단계를 정의합니다.

템플릿 메서드 패턴 예시 1

// 상위 추상 클래스 : 음료 추상 클래스
abstract class Beverage {

  // 템플릿 함수 정의 (알고리즘 순서 고정)
  final void prepareRecipe() {
    boilWater();
    brew();
    pourInCup();
    addCondiments();
  }

  // 공통 함수 구현 : 물 끓이기
  void boilWater() {
    System.out.println("Boiling water");
  }

  // 공통 함수 구현 : 음료를 컵에 따르기
  void pourInCup() {
    System.out.println("Pouring into cup");
  }

  // 음료 제조 함수 선언
  abstract void brew();

  // 첨가물 넣기 함수 선언
  abstract void addCondiments();
}

// 하위 클래스 1 : 티 클래스
class Tea extends Beverage {
  // 음료 제조 함수 구현
  void brew() {
    System.out.println("Steeping the tea");
  }

  // 첨가물 넣기 함수 구현
  void addCondiments() {
    System.out.println("Adding lemon");
  }
}

// 하위 클래스 2 : 커피 클래스
class Coffee extends Beverage {
  // 음료 제조 함수 구현
  void brew() {
    System.out.println("Dripping coffee through filter");
  }

  // 첨가물 넣기 함수 구현
  void addCondiments() {
    System.out.println("Adding sugar and milk");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 템플릿 추상 클래스에 하위클래스 객체 저장
    Beverage tea = new Tea();
    Beverage coffee = new Coffee();

    // 티 객체에서 템플릿 함수 호출
    tea.prepareRecipe();

    System.out.println();

    // 커피 객체에서 템플릿 함수 호출
    coffee.prepareRecipe();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Boiling water
Steeping the tea
Pouring into cup
Adding lemon

Boiling water
Dripping coffee through filter
Pouring into cup
Adding sugar and milk

템플릿 메서드 패턴 예시 2

// 상위 추상 클래스 : 데이터 처리 클래스
abstract class DataProcessor {

  // 템플릿 함수 정의 (알고리즘 순서 고정)
  public final void process(String data) {
    // 파일 로드
    loadData(data);

    // 파일 유효성 확인
    if (isValidData(data)) {
      // 정상 파일 : 데이터 처리 후 저장
      processData(data);
      saveData(data);
    } else {
      // 비정상 파일 : 오류메시지 출력
      System.out.println("Data is invalid, processing aborted.");
    }
  }

  // 파일 데이터 로드 함수 선언
  protected abstract void loadData(String data);

  // 파일 유효성 확인 함수 선언
  protected abstract boolean isValidData(String data);

  // 데이터 처리 함수 선언
  protected abstract void processData(String data);

  // 데이터 저장 함수 선언
  protected abstract void saveData(String data);
}

// 하위 클래스 1 : CSV 데이터 처리 클래스
class CSVDataProcessor extends DataProcessor {
  // 파일 데이터 로드 함수 구현
  @Override
  protected void loadData(String data) {
    System.out.println("Loading data from CSV file: " + data);
  }

  // 파일 유효성 확인 함수 구현
  @Override
  protected boolean isValidData(String data) {
    return data != null && data.contains("CSV");
  }

  // 데이터 처리 함수 구현
  @Override
  protected void processData(String data) {
    System.out.println("Processing CSV data");
  }

  // 데이터 저장 함수 구현
  @Override
  protected void saveData(String data) {
    System.out.println("Saving CSV data to database");
  }
}

// 하위 클래스 2 : JSON 데이터 처리 클래스
class JSONDataProcessor extends DataProcessor {
  // 파일 데이터 로드 함수 구현
  @Override
  protected void loadData(String data) {
    System.out.println("Loading data from JSON file: " + data);
  }

  // 파일 유효성 확인 함수 구현
  @Override
  protected boolean isValidData(String data) {
    return data != null&& data.contains("JSON");
  }

  // 데이터 처리 함수 구현
  @Override
  protected void processData(String data) {
    System.out.println("Processing JSON data");
  }

  // 데이터 저장 함수 구현
  @Override
  protected void saveData(String data) {
    System.out.println("Saving JSON data to database");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 템플릿 추상 클래스에 하위클래스 객체 저장
    DataProcessor csvProcessor = new CSVDataProcessor();
    DataProcessor jsonProcessor = new JSONDataProcessor();

    // CSV 데이터 처리 객체에서 템플릿 함수 호출
    csvProcessor.process("CSV data");

    System.out.println();

    // JSON 데이터 처리 객체에서 템플릿 함수 호출
    jsonProcessor.process("XML data");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Loading data from CSV file: CSV data
Processing CSV data
Saving CSV data to database

Loading data from JSON file: XML data
Data is invalid, processing aborted.

방문자 패턴 (Visitor)

방문자 패턴 예시 1

// Element interface
interface Shape {
  void accept(Visitor visitor);
}

// Concrete elements
class Circle implements Shape {
  double radius;

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

  public double getRadius() {
    return radius;
  }

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

class Rectangle implements Shape {
  double width, height;

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

  public double getWidth() {
    return width;
  }

  public double getHeight() {
    return height;
  }

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

// Visitor interface
interface Visitor {
  void visit(Circle circle);
  void visit(Rectangle rectangle);
}

// Concrete Visitor
class AreaVisitor implements Visitor {
  @Override
  public void visit(Circle circle) {
    double area = Math.PI * circle.getRadius() * circle.getRadius();
    System.out.println("Circle Area: " + area);
  }

  @Override
  public void visit(Rectangle rectangle) {
    double area = rectangle.getWidth() * rectangle.getHeight();
    System.out.println("Rectangle Area: " + area);
  }
}

class PerimeterVisitor implements Visitor {
  @Override
  public void visit(Circle circle) {
    double perimeter = 2 * Math.PI * circle.getRadius();
    System.out.println("Circle Perimeter: " + perimeter);
  }

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

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Shape circle = new Circle(5);
    Shape rectangle = new Rectangle(4, 6);

    Visitor areaVisitor = new AreaVisitor();
    Visitor perimeterVisitor = new PerimeterVisitor();

    System.out.println("Calculating Area:");
    circle.accept(areaVisitor);
    rectangle.accept(areaVisitor);

    System.out.println("\nCalculating Perimeter:");
    circle.accept(perimeterVisitor);
    rectangle.accept(perimeterVisitor);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Calculating Area:
Circle Area: 78.53981633974483
Rectangle Area: 24.0

Calculating Perimeter:
Circle Perimeter: 31.41592653589793
Rectangle Perimeter: 20.0

방문자 패턴 예시 2

// FileSystemElement interface
interface FileSystemElement {
  void accept(Visitor visitor);
}

// File class
class File implements FileSystemElement {
  private String name;
  private long size;

  public File(String name, long size) {
    this.name = name;
    this.size = size;
  }

  public String getName() { return name; }
  public long getSize() { return size; }

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

// Directory class
class Directory implements FileSystemElement {
  private String name;
  private List<FileSystemElement> elements;

  public Directory(String name) {
    this.name = name;
    this.elements = new ArrayList<>();
  }

  public String getName() { return name; }

  public void addElement(FileSystemElement element) {
    elements.add(element);
  }
  public List<FileSystemElement> getElements() {
    return elements;
  }

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

// Visitor interface
interface Visitor {
  void visit(File file);
  void visit(Directory directory);
}

// Concrete visitors
class SizeCalculatorVisitor implements Visitor {
  private long totalSize = 0;

  @Override
  public void visit(File file) {
    totalSize += file.getSize();
  }

  @Override
  public void visit(Directory directory) {
    for (FileSystemElement element : directory.getElements()) {
      element.accept(this);
    }
  }

  public long getTotalSize() {
    return totalSize;
  }
}

class FileSearchVisitor implements Visitor {
  private String searchFileName;
  private File foundFile;

  public FileSearchVisitor(String searchFileName) {
    this.searchFileName = searchFileName;
  }

  @Override
  public void visit(File file) {
    if (file.getName().equals(searchFileName)) {
      foundFile = file;
    }
  }

  @Override
  public void visit(Directory directory) {
    for (FileSystemElement element : directory.getElements()) {
      element.accept(this);
    }
  }

  public File getFoundFile() {
    return foundFile;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Create files
    File file1 = new File("file1.txt", 100);
    File file2 = new File("file2.txt", 200);
    File file3 = new File("file3.txt", 300);

    // Create directories and add files to them
    Directory dir1 = new Directory("Folder1");
    dir1.addElement(file1);
    dir1.addElement(file2);

    Directory dir2 = new Directory("Folder2");
    dir2.addElement(file3);

    Directory rootDir = new Directory("Root");
    rootDir.addElement(dir1);
    rootDir.addElement(dir2);

    SizeCalculatorVisitor sizeVisitor = new SizeCalculatorVisitor();
    rootDir.accept(sizeVisitor);
    System.out.println("Total size of file system: " + sizeVisitor.getTotalSize() + " bytes");

    FileSearchVisitor searchVisitor = new FileSearchVisitor("file3.txt");
    rootDir.accept(searchVisitor);
    File foundFile = searchVisitor.getFoundFile();
    if (foundFile != null) {
      System.out.println("File found: " + foundFile.getName() + ", Size: " + foundFile.getSize() + " bytes");
    } else {
      System.out.println("File not found.");
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Total size of file system: 600 bytes
File found: file3.txt, Size: 300 bytes

도메인 주도 설계 (DDD)

명세 패턴 (Specification)

명세 패턴 예시 1

public interface Specification {
  boolean isSatisfiedBy(int number);

  default Specification and(Specification other) {
    return number -> this.isSatisfiedBy(number) && other.isSatisfiedBy(number);
  }
}

class EvenSpecification implements Specification {
  @Override
  public boolean isSatisfiedBy(int number) {
    return number % 2 == 0;
  }
}

class RangeSpecification implements Specification {
  private int min;
  private int max;

  public RangeSpecification(int min, int max) {
    this.min = min;
    this.max = max;
  }

  @Override
  public boolean isSatisfiedBy(int number) {
    return number >= min && number <= max;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Specification evenSpec = new EvenSpecification();
    Specification rangeSpec = new RangeSpecification(10, 20);

    Specification evenAndInRangeSpec = evenSpec.and(rangeSpec);

    int number = 24;

    System.out.println("Even: " + evenSpec.isSatisfiedBy(number));
        
    System.out.println("In range 10-20: " + rangeSpec.isSatisfiedBy(number));
        
    System.out.println("Even and in range 10-20: " + evenAndInRangeSpec.isSatisfiedBy(number));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Even: true
In range 10-20: false
Even and in range 10-20: false

명세 패턴 예시 2

public class Product {
  private String name;
  private String category;
  private int price;
  private int stock;

  public Product(String name, String category, int price, int stock) {
    this.name = name;
    this.category = category;
    this.price = price;
    this.stock = stock;
  }

  public String getName() { return name; }
  public String getCategory() { return category; }
  public double getPrice() { return price; }
  public int getStock() { return stock; }
}

public class PriceSpec implements Specification {
  private int maxPrice;

  public PriceSpec(int maxPrice) {
    this.maxPrice = maxPrice;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return item.getPrice() <= maxPrice;
  }
}

public class InStockSpec implements Specification {
  @Override
  public boolean isSatisfiedBy(Product item) {
    return item.getStock() > 0;
  }
}

public class AndSpec implements Specification {
  private Specification spec1;
  private Specification spec2;

  public AndSpec(Specification spec1, Specification spec2) {
    this.spec1 = spec1; this.spec2 = spec2;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return spec1.isSatisfiedBy(item) && spec2.isSatisfiedBy(item);
  }
}

public class OrSpec implements Specification {
  private Specification spec1;
  private Specification spec2;

  public OrSpec(Specification spec1, Specification spec2) {
    this.spec1 = spec1; this.spec2 = spec2;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return spec1.isSatisfiedBy(item) || spec2.isSatisfiedBy(item);
  }
}

public class NotSpec implements Specification {
  private Specification spec;

  public NotSpec(Specification spec) {
    this.spec = spec;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return !spec.isSatisfiedBy(item);
  }
}

public class ProductFilter {
  public static List<Product> filter(List<Product> items, Specification spec) {
    return items.stream()
                .filter(spec::isSatisfiedBy)
                .collect(Collectors.toList());
  }

  public static void printProducts(List<Product> products) {
    products.forEach(
      p -> System.out.println(p.getName() + " - " + p.getCategory() + " - $" + p.getPrice() + " - Stock: " + p.getStock())
    );
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    List<Product> products = Arrays.asList(
      new Product("Laptop", "Electronics", 1200, 5),
      new Product("Smartphone", "Electronics", 800, 0),
      new Product("Headphones", "Electronics", 200, 10),
      new Product("Book", "Literature", 20, 50)
    );

    Specification electronicsSpec = new CategorySpec("Electronics");
    Specification inStockSpec = new InStockSpec();
    Specification expensiveSpec = new PriceSpec(500);

    Specification electronicInStock = new AndSpec(electronicsSpec, inStockSpec);
    Specification electronicOrInStock = new OrSpec(electronicsSpec, inStockSpec);
    Specification notExpensive = new NotSpec(expensiveSpec);

    System.out.println("Electronics in stock:");
    ProductFilter.printProducts(ProductFilter.filter(products, electronicInStock));

    System.out.println("\nElectronics or items in stock:");
    ProductFilter.printProducts(ProductFilter.filter(products, electronicOrInStock));

    System.out.println("\nNot expensive items:");
    ProductFilter.printProducts(ProductFilter.filter(products, notExpensive));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Electronics in stock:
Laptop - Electronics - $1200.0 - Stock: 5
Headphones - Electronics - $200.0 - Stock: 10

Electronics or items in stock:
Laptop - Electronics - $1200.0 - Stock: 5
Smartphone - Electronics - $800.0 - Stock: 0
Headphones - Electronics - $200.0 - Stock: 10
Book - Literature - $20.0 - Stock: 50

Not expensive items:
Laptop - Electronics - $1200.0 - Stock: 5
Smartphone - Electronics - $800.0 - Stock: 0