Factory Method Pattern
Why to use
- 클라이언트가 특정 클래스의 생성 방법을 모른 체로 사용할 수 있게하고 OCP 를 지키며 클래스 종류를 쉽게 확장할 수 있게 하기위함.
Example
Client.java
@Getter
public class Client {
private String name;
private String email;
public Client(String name, String email) {
this.name = name;
this.email = email;
}
}
Ship.java
@Getter
public class Ship {
private String name;
private String color;
private String logo;
public Ship(String name) {
this.name = name;
}
public Ship(String name, String color, String logo) {
this.name = name;
this.color = color;
this.logo = logo;
}
}
ShipFactory.java
public class ShipFactory {
public static Ship orderShip(Client client, String shipName) {
if (client.getName() == null) {
throw new MissingFormatArgumentException("주문 고객명을 입력해주세요");
} else if (client.getEmail() == null) {
throw new MissingFormatArgumentException("이메일을 입력해주세요");
}
// 💡 Ship 의 종류나 속성이 변경된다면?
// 아래 코드가 변경될 수 밖에 없다.
// 이는 Open-Closed Principal (OCP) 에 위배된다.
// 확장에 열려있고, 변경에 닫혀있어야하는데
// 확장과 변경 모두 열려있다.
if (shipName == "white-ship") {
// ~~
} else if (shipName == "black-ship") {
} else {
throw new IllegalArgumentException("유효하지 않은 배 이름입니다.");
}
return new Ship(shipName);
}
}
위의 코드는 OCP 에 위반한다.
팩토리 메소드 패턴은 클래스 변경에 독립적인 클래스 생성을 위해 필요하다.
다양한 구현체가 존재하고, 그중에 특정 구현체를 만들 수 있는 다양한 팩토리를 제공하는 패턴
👉 팩토리 패턴은 위 예제에서 ShipFactory 클래스를 확장함으로써 다양한 Ship 을 커버할 수 있게한다.
When to use
- 특정 클래스를 생성하는 방식을 OCP 원칙을 지키며 변화에 독립적으로 확장하고 싶을 때
- 특정 클래스의 상태 관리를
static
키워드 없이 하고 싶을 때
How to use
Factory 를 Interface 및 구현체 추상 클래스 - 클래스 간 계층 구조를 갖게 하는게 일반적
Ship.java
여기서는 Interface 대신 Abstract class 를 사용했다.
@Getter
abstract class Ship {
private String name;
private String color;
private String logo;
protected Ship(String name) {
this.name = name;
}
protected Ship(String name, String color, String logo) {
this.name = name;
this.color = color;
this.logo = logo;
}
void changeShipInfo(String name, String color, String logo) {
this.name = name;
this.color = color;
this.logo = logo;
}
}
WhiteShip.java
public class WhiteShip extends Ship {
protected WhiteShip(String name, String color, String logo) {
super(name, color, logo);
}
}
BlackShip.java
public class BlackShip extends Ship {
protected BlackShip(String name, String color, String logo) {
super(name, color, logo);
}
}
WhiteShip, BlackShip 등 기존 코드 변경에 무관하게
클래스를 확장시킬 수 있다.
⚠️ 이 때, 클래스 확장시마다 각각 Factory 를 구성해야한다.
ShipFactory.java
public interface ShipFactory {
default Ship orderShip(Client client, String orderShipName) {
validateClient(client);
validateShipName(orderShipName);
Ship ship = createShip();
sendEmailToClient(client.getEmail(), ship);
return ship;
}
Ship createShip();
private void validateClient(Client client) {
if (client.getName() == null || client.getName().isBlank()) {
throw new IllegalArgumentException("주문 고객 이름을 입력해주세요");
}
if (client.getEmail() == null || client.getEmail().isBlank()) {
throw new IllegalArgumentException("주문 고객 이메일을 입력해주세요");
}
}
private void validateShipName(String orderShipName) {
if (orderShipName.equalsIgnoreCase("white-ship")) {
} else if (orderShipName.equalsIgnoreCase("black-ship")) {
} else {
throw new IllegalArgumentException("주문할 수 없는 배압니다.");
}
}
private void sendEmailToClient(String clientEmail, Ship ship) {
System.out.println(clientEmail + "로 주문 정보 발송되었습니다.");
}
}
WhiteShipFactory.java
public class WhiteShipFactory implements ShipFactory {
// orderShip 은 default 메소드라 따로 구현 필요 X
@Override
public WhiteShip createShip() {
return new WhiteShip("white-ship", "white", "⇯");
}
}
BlackShipFactory.java
public class BlackShipFactory implements ShipFactory {
@Override
public Ship createShip() {
return new BlackShip("black-ship", "black", "⚓");
}
}
Factory 와 Class 에 각각계층 구조가 있어 구현체 팩토리 내에서 원하는 클래스를 만들어내는 것이다.
결과적으로 아래와 같은 구조를 갖게된다.
Pros and Cons
장점
- OCP 원칙을 지키며 인스턴스 생성 로직을 전혀 건드리지 않고 별도의 클래스를 확장할 수 있다.
Product - Factory 간 Loosely coupled 로 구현되었기 때문이다.
변경에 닫혀있다: 기존 코드를 변경할 필요가 없다.
확장에 열려있다: 새로운 코드를 얼마든지 생성할 수 있다.
이 예제에서는 WhiteShip, BlackShip 을 정의하며 동시에 팩토리도 추가 구현하여 유연하게 확장.
default 키워드
interface 에서 구현체를 만들 수 있게하는 키워드
기존에 abstract class 에서만 지원되던 메소드 구현부를 interface 에서도 지원한다.
단점
- 역할을 나누면서 클래스가 늘어나 복잡도가 증가한다.