πŸ§‘β€πŸŽ¨ Design Patterns

GunhoΒ·2024λ…„ 11μ›” 9일
1

Object Oriented Programming (OOP) & Java

λͺ©λ‘ 보기
15/29

πŸ§‘β€πŸŽ¨ Design Patterns

πŸ§‘β€πŸŽ¨ A Design Pattern is a general and repeatable solution to the commonly occurring problems. A Design Pattern can be also referred to as the best practices from the experiences.

In Software Designs, there are considerable design patterns that are commonly applied by developers such as:

  • Factory Method
  • Singleton
  • Adapter
  • Composite
  • Facade Design
  • Visitor

...

where these can be largely divided into three design types:

  • Creational Design Patterns
  • Structural Design Patterns
  • Behavioural Design Patterns

Over the following sections, due to the time constraints, only a few patterns specifically Singleton, Factory, and Adapter, will be thoroughly examined.


🐈 Singleton

🐈 Singleton is a pattern that limits the instantiation of a class to a single instance.

Refactoring Guru (1) Available at here


Singleton implies all threads if under multi-threading context, sharing a single instance of a class and hence:

  • πŸ’Ύ prevents the unnecessary memory allocations
    • leading to the efficient resource management
  • 🌞 improves instance consistency

Singleton can be implemented via a few Java components such as a private access modifier, a static keyword, and so on:

Singleton

public class Singleton {

  private static Singleton singleton; // static

  private Singleton() {  // private constructor
  }

  public static Singleton getInstance() { // static method
    if (singleton == null) {
    	sychronized (Singleton.class) {
        	if (singleton == null)
        		singleton = new Singleton();
        } 
	}

        

    return singleton;
  }
}

Above Singleton Class is a conventional approach to create an instance in the context of Singleton Pattern, where in the client code:

Client Code

public static void main(String[] args) {

	Singleton singleton1 = Singleton.getInstance();
	Singleton singleton2 = new Singleton(); // prohibited
}
public static void main(String[] args) {

	Singleton singleton1 = Singleton.getInstance();
	Singleton singleton2 = Singleton.getInstance();
    
    System.out.println("singleton1 = " + singleton1); // singleton1 = com.example.demo.Singleton@1921994e
	System.out.println("singleton2 = " + singleton2); // singleton2 = com.example.demo.Singleton@1921994e
}

its access to the Singleton instance is only viable through the getInstance() method.

Above Singleton pattern, however, heavily relies on the concrete class instantiation so that it has a high likelihood of violating OCP and DIP from the SOLID principles.

Furthermore, followed by its innate nature, where the identical class instance is shared under the multi-threading context, careful control over a class to ensure the thread-safe becomes necessary. Hence, Singleton incurs:

  • πŸ§ͺ difficulties in tests (no constructor)
  • βš™οΈ high dependency on a concrete class
  • πŸͺ‘ thread-unsafety under multi-threading environment

These can be almost all addressed followed by a new Singleton Pattern implemented via Map or HashTable data structure, where it saves an instance and returns the instance whenever necessary. Popular usecases of this improved Singleton are Threadpool, Database Connection Pool, Spring DI or Bean Container, and etc.

Singleton (HashMap)

public class Container {
  
  private static Map<String, Object> map = new HashMap<>(); 
  
  public Object get(String key) {
    return map.get(key);
  }
  
  public void put(String key, Object value) {
    map.put(key, value);
  }
}

🏭 Factory

🏭 Factory Method is a pattern that encapsulates the instance creational logic.

Refactoring Guru (2) Available at here


In Factory Method Pattern, methods specifically create instances, where it replaces the direct new object calls to the method invockation and thereby becoming the Factory Method Pattern. The pattern extensively applies abstraction or interfaces strictly related to the instance creational logics, where the actual implementation is preserved for the concrete classes.

Factory Method Pattern:

  • ♻️ increases code reusability
  • πŸ’ decreases couplings
  • 🐣 maintains OCP and SRP

Factory Method Pattern can be implemented via abstract classes and interfaces, abstract methods, and concrete classes:

Factory Method

// Logistics class
public abstract class Logistics {
	public final Transport planDelivery() {
    	Transport transport = createTransport();
        transport.deliver();
        return transport;
    }
    
	protected abstract Transport createTransport() {
    }
}

// Transport interface
public interface Transport {
	void deliver();
}

In the above Logistics class, a creational method, createTransport() method is declared as protected abstract, where it implies that the actual implementation falls to the concrete classes. Client, simply by calling planDelivery() can acquire the Transport instances.

Transport interface is the interface for the Trasport classes where these classes implement this interface. The interface is replaceable with an abstract class if necessary.

Concrete Class

// Lorry class
public class Lorry implements Transport {
	public void deliver() {
    	System.out.println("DELIVERING BY A LORRY");
    }
}

// Ship class
public class Ship implements Transport {
	public void deliver() {
    	System.out.println("DELIVERING BY A SHIP");
    }
}

// LandLogistics class
public class LandLogistics extends Logistics {
	public Transport createTransport() {
    	return new Lorry();
    }
}

// SeaLogistics class
public class SeaLogistics extends Logistics { 
	public Transport createTransport() {
    	return new Ship();
    }
}

Client

public class Client {
	public static void main(String[] args) {
    	Logistics seaLogistics = new SeaLogistics();
        Logistics landLogistics = new LandLogistics();
        
        seaLogistics.planDelivery();  // "DELIVERING BY A SHIP"
        landLogistics.planDelivery(); // "DELIVERING BY A LORRY"
    }
}

Finally at the client-level, just simply calling on planDelivery(), all the instance creational logic becomes executed behind the scene, followed by the instantiation encapsulation.

Factory Method, however, can generate these negative impacts to the code:

  • πŸ€– increase in the number of factory classes
    • leading to an explosion of subclasses
  • πŸ˜΅β€πŸ’« increase in the code complexity

πŸ”Œ Adapter

πŸ”Œ Adapter is a structural design pattern that enables collaboration between objects of incompatible types.

Refactoring Guru (3) Available at here


Adapter pattern works by wrapping an object as an instance variable and inheriting or implementing the class or interface that needs compatibility. The Adapter then implements the logic needed to make the wrapped object compatible with this class or interface.

Adapter maintains:

  • SRP

    • interface or data conversion code can be separated from the core business logic of the programme.
  • OCP

    • it works with the adapter through the client interface without modifying existing class code.

Adapter can be implemented as below:

Adapter

// Adapter
public class AAdapter extends A {
	private B b;
    
    public AAdapter(B b) {
    	this.b = b;
    }
    
    @Override
 	public int multiply() {
    	return 3 * b.getB();
    }  
}

// Class A
public class A {
	private int a;
    private int b;
    
    public A(int a, int b) {
    	this.a = a;
        this.b = b;
    }
    
    public int multiply() {
    	return a * b;
    }
}

// Class B
public class B {
    private int b;
    
    public B(int b) {
        this.b = b;
    }
    
    public int getB() {
    	return b;
    }
}

class extension can be replaced with interface implementation.

Client

public class Client {
	public static void main(String[] args) {
		A a = new A(3, 5);
        AAdapter b = new AAdapter(new B(5));
        
        System.out.println(a.multiply());
        System.out.println(b.multiply());
    }
}

class B which was not compatible to class A is now compatible via AAdapter class where it implements all the relevant business logic that class A performs.

Good use cases of Adapter in the practice could be JDBC and JRE where given the interfaces, the actual implementation falls to the concrete classes and these implementations always run.

Adapter, however, can result in:

  • πŸ˜΅β€πŸ’« the increasing code complexity
    • followed by the increasing number of classes.

where its application may have to be carefully examined.


πŸ“š References

Refactoring Guru (1)
Refactoring Guru (2)
Refactoring Guru (3)
μŠ€ν”„λ§ μž…λ¬Έμ„ μœ„ν•œ μžλ°” 객체 μ§€ν–₯의 원리와 이해

profile
Hello

0개의 λŒ“κΈ€