계속 업데이트 중입니다
SOLID 원칙을 정확하게 지키며 설계하기란 쉬운일이 아니다
원칙들로만 이뤄져 있기 때문에 틀리게 해석할 위험성이 있고, 객체 작성마다 각 원칙을 고려하기는 어렵다
여러 SW 엔지니어들이 SOLID 원칙에 따라 OOP를 설계 했고, 이런 설계들에서 나타나는 공통점들을 정리한 것이 바로 디자인 패턴이다
SOLID 원칙이 추상체라면, 디자인 패턴은 구상체라고 생각할 수 있다
객체 생성을 위한 메커니즘을 제공하는 패턴들
객체 생성을 위한 인터페이스를 제공하는 패턴
객체 생성을 직접 호출하는 것이 아니라, 팩토리 메소드를 호출해 팩토리 내부에서 객체를 생성한다
단순히 생성 위치를 옮긴 것으로 보일 수도 있지만, 팩토리 메소드를 통하게 하면서 자식 클래스에서 팩토리 메소드를 오버라이딩해 생성되는 객체를 바꿀 수 있다
interface Button {
void render();
void click();
}
class LinuxButton implements Button {
@Override
public void render() {
/* 리눅스 버튼 렌더 구현 */
}
@Override
public void click() {
/* 리눅스 버튼 클릭 구현 */
}
}
class WindowsButton implements Button {
@Override
public void render() {
/* 윈도우 버튼 렌더 구현 */
}
@Override
public void click() {
/* 윈도우 버튼 클릭 구현 */
}
}
import Button;
import LinuxButton;
import WindowsButton;
abstract class Dialog {
public void renderWindow() {
Button yesButton = createButton();
yesButton.render();
}
// 팩토리 메소드는 하위 클래스에서 구현
public abstract Button createButton();
}
class LinuxDialog extends Dialog {
@Override
public Button createButton() {
return new LinuxButton();
}
}
class WindowsDialog extends Dialog {
@Override
public Button createButton() {
return new WindowsButton();
}
}
// 호스트 코드는 프로덕트를 처음 생성할때만 어떤 팩토리를 사용하는지 선택하고
// 이후로는 베이스 클래스로 프로덕트를 다룬다
Dialog dia = new WindwosDialog();
dia.renderWindow();
연관된 객체의 그룹을 생산하기 위한 패턴
다양한 종류의 프로덕트가 있고 각 프로덕트들은 하나의 그룹에 속할 때
새로운 프로덕트 또는 그룹의 프로덕트를 추가할 때 기존 코드를 수정해야한다
interface ProductA {
void someMethod();
}
interface ProductB {
void otherMethod();
}
class ProductA1 implements ProductA {
@Override
void someMethod() {
/* 구현 */
}
}
class ProductA2 implements ProductA {
@Override
void someMethod() {
/* 구현 */
}
}
class ProductB1 implements ProductB {
@Override
void otherMethod() {
/* 구현 */
}
}
class ProductB2 implements ProductB {
@Override
void otherMethod() {
/* 구현 */
}
}
public interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
class ConcreteFactory1 impelemnts AbtractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA1;
}
@Override
public ProductB createProductB() {
return new ConcreteProductB1;
}
}
class ConcreteFactory2 impelemnts AbtractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA2;
}
@Override
public ProductB createProductB() {
return new ConcreteProductB2;
}
}
AbstractFactory factory = new ConcreteFactory1();
ProductA pa = factory.createProductA();
동일한 생성코드로 다양한 객체를 생산 가능하게 하는 패턴
하나의 추상체에서 매우 다양한 하위 구상체가 나오고, 계속 추가해야한다면 어떻게 관리해야 효율적일까?
모든 것이 가능하도록 추상체의 생성자에 많은 데이터를 넣을 수도 있지만, 그렇게 하면 말도 안되게 거대한 생성자가 생길 것이다
객체 생성코드를 클래스 밖으로 빼내어 별도 클래스인 builder를 만든다
각 빌더는 객체를 생성하기 위한 다양한 메소드들을 가지고, 필요한 메소드만 호출해 객체를 생성할 수 있다
director 클래스를 통해 호스트 코드에서 일일히 메소드를 호출하지 않고 관리할 수도 있다
class Car {}
class Manual {}
interface Builder {
void reset();
void setSeats(int n);
void setEngine(Engine e);
void setTripComputer();
void setGPS();
}
class CarBuilder implements Builder {
private Car car;
CarBuilder() {
this.reset();
}
@Override
public void reset() {
this.car = new Car();
}
@Override
public void setSeats(int n) {}
@Override
public void setEngine(Engine e) {}
@Override
public void setTripComputer() {}
@Override
public void setGPS() {}
// 빌더는 다양한 프로덕트를 생산할 수 있기 때문에
// 빌더 인터페이스에서 getResult()를 정의하지 않고
// 각 구상체에서 정의한다
// 빌더는 프로덕트 반환 후 생산 준비가 되어야하기 때문에
// reset 해줘야한다
public Car getResult() {
Car product = this.car;
this.reset();
return product;
}
}
class CarManualBuilder implements Builder {
private Manual man;
CarManualBuilder() {
this.reset();
}
@Override
public void reset() {
this.man = new Manual();
}
@Override
public void setSeats(int n) {}
@Override
public void setEngine(Engine e) {}
@Override
public void setTripComputer() {}
@Override
public void setGPS() {}
public Manual getResult() {
Manual product = this.man;
this.reset();
return product;
}
}
public class Director {
private Builder builder;
public void setBuilder (Builder builder) {
this.builder = builder;
}
public void makeSportsCar(Builder builder) {
builder.reset();
builder.setSeats(2);
builder.setEngine(new SportEngine());
builder.setTripComputer(true);
builder.setGPS(true);
}
}
Director director = new Director();
CarBuilder builder = new CarBuilder();
director.makeSportsCar(builder);
Car car = builder.getResult();
클래스에 대한 의존 없이 기존의 객체를 복사하기 위한 패턴
복사 과정을 실제 복사되는 객체에 위임한다
복사가 가능한 객체에 클래스에 coupling될 필요 없이 복사할 수 있는 인터페이스를 clone()
을 선언한다
clone()
은 보통 동일한 클래스의 인스턴스를 생성하고 복사하는 객체의 정보를 새로운 객체에 옮겨 담아 구현한다
clone()
인터페이스를 구현한 객체를 프로토타입이라 부른다
public abstract class Shape {
int x;
int y;
String color;
public Shape() {}
// 프로토타입 생성자
public Shape(Shape source) {
this.x = source.x
this.y = source.y
this.color = source.color
}
abstract Shape clone();
}
class Rectangle extends Shape {
int width;
int height;
public Rectangle(Rectangle source) {
super(source);
this.width = source.width;
this.height = source.height;
}
@Override
Shape clone() {
return new Rectangle(this);
}
}
Shape shape = new Rectangle();
Shape shapeCopy = shape.clone();
클래스의 인스턴스가 오직 하나만 존재하는 것을 보장하고 글로벌 접근을 제공하기 위한 패턴
클래스가 하나의 인스턴스만 가지도록 보장하기 어렵다. 인스턴스의 수를 관리하는 가장 기본적인 예시는 공유 자원에 대해 접근을 제어하는 경우이다. RDB의 경우 어디에서 접근하던 해당 객체는 하나여야만 한다. 일반적인 생성자는 항상 새로운 인스턴스를 반환하기 때문에 이를 보장하기는 어렵다.
전역에서 인스턴스를 안전하게 접근 가능하게 하기 어렵다. 전역변수가 위험성이 많은 것처럼 전역 인스턴스는 어디에서도 바꿀 위험이 있어 조심해야한다.
기본 생성자를 private으로 만들어 새로운 인스턴스 생성을 막는다
static한 생성 메소드를 만든다. 내부적으로 이 생성 메소드를 호출 시 private 생성자를 호출하고 이를 static 필드에 저장한다. 이후 이 생성자로의 접근은 static 필드를 반환한다
사실 요즘에는 둘 중 하나만 해결해도 싱글톤 패턴이라고 한다
public final class Singleton {
// double check lock이 정상적으로 작동하도록 volatile로 선언
private static volatile Singleton instance;
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance(String value) {
Singleton result = instance;
if (result != null) {
return result;
}
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
}
Singleton instance = Singleton.getInstance("싱글턴");
var value = instance.value;
객체들를 합쳐 구조를 이루면서, 유연하고 효율적인 구조를 유지하기 위한 패턴들
호환되지 않는 인터페이스를 가진 객체끼리 협력할 수 있게 하기 위한 패턴
어플리케이션에서 xml을 json로 변환해 라이브러리에 전달하도록한다
이런 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 해주는 객체(위 경우 변환기)를 어답터라고 한다
@AllArgsConstructor
@Getter
public class RoundHole {
private int radius
public boolean fits(RoundPeg peg) {
return this.getRadius() >= peg.getRadius();
}
}
@AllArgsConstructor
@Getter
public class RoundPeg {
private int radius;
}
@AllArgsConstructor
@Getter
public class SquarePeg {
private int width;
}
@AllArgsConstructor
public class SquarePegAdapter extends RoundPeg {
private SquarePeg peg;
public int getRadius() {
return this.peg.getWidth() * Math.sqrt(2) / 2
}
}
// 클라이언트 코드
// 컴파일 불가한 코드
var hole = new RoundHole(5);
var squarePeg = new SquarePeg(5);
hole.fits(squarePeg);
// 어답터 사용
var hole = new RoundHole(5);
var squarePegAdapter = new SquarePegAdapter(new SquarePeg(5));
hole.fits(squarePegAdapter);
하나의 거대한 클래스 또는 깊게 연관된 클래스들을 두개의 별도 계층으로 분리하기 위한 패턴
객체들을 트리 구조들로 구성하고 마치 트리 구조의 부분부분이 각각의 객체인것처럼 처리하기 위한 패턴
여러 제품이 담긴 주문품의 총 가격을 어떻게 알 수 있을까?
위 트리 구조처럼 주문품을 나타낸다면 루프를 돌며 상자인지, 제품인지 확인하고 제품인 경우에 대해서만 가격을 구하고, 모든 제품의 가격을 더할 방법이 있어야하고 등등 복잡하다
컴포지트 패턴은 제품과 상자를 동일한 인터페이스를 통해 총 가격을 계산하도록 한다
만약 제품이라면 그냥 가격을 반환하면 되고, 상자라면 하위의 모든 제품을 찾아 값을 더해 가격을 반환한다
이때 만약 상자 아래에 또 다른 상자가 있다면, 마찬가지로 모든 제품을 찾게 될것이다
클라이언트는 트리의 구성을 명확히 알 필요 없이, 똑같이 인터페이스를 통해 사용한다
abstract public class Component {
private String name;
public Component(String name) {
this.name = name;
}
abstract public int getSize();
}
// 리프
public class File extends Component {
private Object data;
private int size;
public File(String name) {
super(name);
}
@Override
public int getSize() {
return this.size;
}
}
// 컴포지트
public class Folder extends Component {
List<Component> children = new ArrayList<>();
public Folder(String name) {
super(name);
}
public boolean addComponent(Component component) {
return children.add(component);
}
public boolean removeComponent(Component component) {
return children.remove(component);
}
@Override
public int getSize() {
return children.stream()
.mapToInt(Component::getSize)
.sum();
}
}
객체를 새로운 행동을 지닌 wrapper 객체로 감싸므로서 행동을 추가하기 위한 패턴.
wrapper 객체는 타겟 객체와 동일한 인터페이스를 가진다. 메소드에 요청이 들어올 시 타겟 객체에 위임한다. 이때 wrapper 객체는 타겟 객체에게 전달하기전, 또는 이후 다른 작업을 해서 결과를 바꿀 수 있다
즉 클라이언트 입장에서 wrapper 객체와 타겟 객체는 동일하다
wrapper 객체가 특정 인터페이스를 구현한 객체를 필드로 가질 수 있게 한다면, wrapper를 wrapper로 감싸며 모든 wrapper들의 행동을 추가하는 것이 가능해진다
// DataSource.java
public interface DataSource {
void writeData(Data data);
Data readData();
}
// FileDataSource.java
public class FileDataSource implements DataSource {
private final String filename;
public FileDataSource(String filename) {
this.filename = filename;
}
@Override
public void writeData(Data data) {
// 파일에 데이터 작성
}
@Override
public Data readData() {
// 파일에서 데이터를 읽는다
return data;
}
}
// DataSourceDecorator.java
public class DataSourceDecorator implements DataSource {
private final DataSource wrappee;
public DataSourceDecorator(DataSource dataSource) {
this.wrappee = dataSource;
}
@Override
public void writeData(Data data) {
this.wrappee.writeData(data);
}
@Override
public Data readData() {
return this.wrappee.readData();
}
}
// EncryptionDecorator.java
public class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource dataSource) {
super(dataSource);
}
@Override
public void writeData(Data data) {
super.writeData(encrypt(data));
}
@Override
public Data readData() {
Data data = super.readData();
if (data.isEncrpyted)
return decrypt(data);
return data;
}
}
// CompressionDecorator.java
public class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource dataSource) {
super(dataSource);
}
@Override
public void writeData(Data data) {
super.writeData(compress(data));
}
@Override
public Data readData() {
Data data = super.readData();
if (data.isCompressed)
return decompress(data);
return data;
}
}
// 클라이언트 코드
// file.dat에 평문 데이터 추가
DataSource dataSource = new FileDataSource("file.dat");
dataSource.writeData(someData);
// file.dat에 압축된 데이터 추가
dataSource = new CompressionDecorator(dataSource);
dataSource.writeData(someData);
// file.dat에 암호화되고 압축된 데이터 추가
dataSource = new EncryptionDecorator(dataSource);
source.writeData(someData);
라이브러리, 프레임워크 등 복잡한 클래스들의 집합에 단순화된 인터페이스를 제공하기 위한 패턴
복잡한 라이브러리와 동작하는 코드를 작성해야한다면, 객체 생성, 의존석 관리, 메소드 실행을 올바른 순서로 하려고 해야하고 복잡하다
코드의 비즈니스 로직이 라이브러리와 결합도가 높아지게 된다. 코드를 이해, 유지 보수하기 어려워진다
public class VideoFile{}
public class OggCompressionCodec{}
public class MPEG4CompressionCodec{}
public class CodecFactory{}
public class BitrateReader{}
public class AudioMixer{}
public class VideoConverter {
public File convert(String filename, Format format) {
File file = new VideoFile(filename);
srcCodec == new CodecFactory.extract(file);
...
if (format.equals("mp4")
destCodec = new MPEG4CompressionCodec();
else
...
return new File(...);
}
}
// 클라이언트 코드
var converter = new VideoConverter();
var mp4File = converter.convert("/file/dir/namme", Format.MP4);
객체간 공통 부분을 공유해 더 많은 객체를 RAM에 담기 위한 패턴
객체의 대체로 다른 객체를 두어 프록시 객체가 원본 객체로의 접근을 관리하게 하는 패턴
public interface ThridPartyYoutubeLib {
List<Video> listVideos();
Video getVideoInfo(Long id);
void downloadVideo(Long id);
}
public class ThridPartyYouTubeClass implements ThridPartyYoutubeLib {
@Override
public List<Video> listVideos() {}
@Override
public Video getVideoInfo(Long id) {}
@Override
public video downloadVideo(Long id) {}
}
public class CachedYoutubeClass implements ThirdPartyYoutubeLib {
private ThridPartyYouTubeLib service;
private List<Video> listCache;
private Video videoCache;
boolean needReset;
Public CachedYoutubeClass(ThridPartyYouTubeLib service) {
this.service = service
}
@Override
public List<Video> listVideos() {
if (listCache == null || needReset)
listCache = service.listVideos();
return listCache;
}
@Override
public Video getVideoInfo(Long id) {
if (videoCache == null || needReset)
videoCache = service.getVideoInfo(id);
return videoCache;
}
@Override
public void downloadVideo(Long id) {
}
}
객체간 알고리즘과 책임의 할당에 대한 패턴들
여러 handler가 이루는 체인에 요청을 보내고, 각 handler가 요청을 처리할지 또는 다음 handler로 전달할지로 구성된 패턴
요청을 독립적인 객체로 만들어 요청을 메소드의 인자로 사용할 수도 있고, 요청을 처리를 지연시킬 수도 있게 하는 패턴
public abstract class Command {
protected Application app;
protected Editor editor;
protected String backup;
public Command(Application app, Editor editor) {
this.app = app;
this.editor = editor;
}
public void saveBackup() {
backup = editor.getText();
}
public void undo() {
editor.setText() = backup;
}
public abstract boolean execute();
}
public class CopyCommand extends Command {
@Override
public boolean execute() {
app.clipboard = editor.getSelection()
return false;
}
}
public class CutCommand extends Command {
@Override
public boolean execute() {
saveBackup();
app.clipboard = editor.getSelection();
editor.deleteSelection();
return true;
}
}
public class PasteCommand extends Command {
@Override
public boolean execute() {
saveBackup();
editor.replaceSelection(app.clipboard);
return true;
}
}
public class PasteCommand extends Command {
@Override
public boolean execute() {
saveBackup();
editor.replaceSelection(app.clipboard);
return true;
}
}
public class UndoCommand extends Command {
@Override
public boolean execute() {
app.undo()
return false;
}
}
public class CommandHistory {
private Stack<Command> history;
public void push(Command command) {
history.push(command);
}
public Command pop() {
return history.pop();
}
}
public class Editor {
private String text;
public String getSelection() {}
public void deleteSelection() {}
public void replaceSelection() {}
}
컬랙션의 요소를 자료구조의 노출 없이 탐색하기 위한 패턴
public interface SocialNetwork {
ProfileIterator createFriendsIterator(Long profileId);
ProfileIterator createCoworkersIterator(Long profileId);
}
public class Facebook implements SocialNetwork {
@Override
ProfileIterator createFriendsIterator(Long profileId) {
return new FacebookIterator(this, profileId, "friends");
}
@Override
ProfileIterator createCoworkersIterator(Long profileId) {
return new FacebookIterator(this, profileId, "coworkers);
}
}
public interface ProfileIterator {
Profile getNext();
boolean hasMore();
}
public class FacebookIterator implements ProfileIterator {
private Facebook facebook;
private Long profileId;
private String type;
private Long currentPosition;
private Profile[] cache;
public FacebookIterator(Facebook facebook, Long profileId, String type) {
this.facebook = facebook;
this.profileId = profileId;
this.type = type;
}
private void lazyInit() {
if (cache == null) {
cache = facebook.socialGraphRequest(profileId, type);
}
}
public Profile getNext() {
if (hasMore()) {
currentPositon++;
return cache[currentPosition];
}
}
public hasMore() {
lazyInit()
return currentPositon < cache.length;
}
}
public class SocialSpammer {
public void sned(ProfileIterator iterator, String message) {
while (iterator.hasMore()) {
profile = iterator.getNext();
System.sendEmail(profile.getEmail(), message);
}
}
}
public class Application {
private SocialNetwork network;
private SocialSpammer spammer;
public void config() {
if (working with Facebook)
this.network = new Facebook();
if (working with LinkedIn)
this.network = new LinkedIn();
this.spammer = new SocialSpammer();
}
public void sendSpamToFriends(Profile profile) {
iterator = network.createFirendsIterator(profile.getId());
spammer.send(iterator, "hi");
}
public void snedSpamToCoworkers(Profile profile) {
iterator = network.createCoworkersIterator(profile.getId());
spammer.send(iterator, "hello");
}
}
객체들이 mediator 객체를 통해 협동하도록 강제하여 의존성을 줄이는 패턴
객체 구현에 대한 상세 정보 없이 객체의 상태를 저장하고 이전 상태로 복구 시키기 위한 패턴
= Listener
객체가 다른 객체를 구독하는 장치를 정의해 객체에서 발생하는 이벤트를 구독하는 객체에게 알리는 패턴
상태를 주기적으로 확인하는 폴링 방식으로 다른 객체의 변화를 감지하려고 한다면, 아직 변화가 일어나지 않았음에도 확인하는 것은 자원의 낭비이다
반대로 모든 변화에 대해 전달해도 필요없는 데이터의 전달이 될것이다. 어떻게 하면 원하는 변화에 대해서만 알 수 있을까?
상태를 가지고, 이를 다른 객체에게 알리는 객체를 publisher
라고 한다. 알림을 받는 객체를 subscriber
라고 한다
퍼블리셔는 구독자의 배열을 필드로 가지고, 배열에 구독자를 추가하거나 제거할 수 있는 인터페이스를 제공한다
퍼블리셔는 어떤 상태가 변화하거나 무엇인가 실행했을때 구독자들에게 알림을 보낸다
// EventManager.java
public class EventManager {
private Map<String, List<Listener>> listeners = new HashMap<>();
public EventManager(String... operations) {
for (String operation : operations)
this.listeners.put(operation, new ArrayList<>());
}
public void subscribe(String eventType, Listener listener) {
List<Listener> users = listeners.get(eventType);
users.add(listener);
}
public void unsubscribe(String eventType, Listener listener) {
List<Listener> users = listeners.get(eventType);
users.remove(listener);
}
public void notify(String eventType, File file) {
List<Listener> users = listeners.get(eventType);
for (Listener listener : users) {
listener.update(eventType, file);
}
}
}
// Editor.java
public class Editor {
public EventManager events = new EventManager("open", "save");
private File file;
public void openFile(String filePath) {
this.file = new File(filePath);
events.notify("open", file);
}
public void saveFile() throws Exception {
if (this.file == null)
throw new Exception("Must open file before save");
events.notify("save", file);
}
}
// Listener.java
public interface Listener {
void update(String eventType, File file);
}
// EmailAlertsListener.java
// 파일이 저장되는 이벤트 구독
public class EmailAlertsListener implements Listener {
private String email;
public EmailAlertsListener(String email) {
this.email = email;
}
@Override
public void update(String eventType, File file) {
System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
}
}
// LoggingListener.java
// 파일이 열리는 이벤트 구독
public class LoggingListener implements Listener {
private File log;
public LoggingListener(String fileName) {
this.log = new File(fileName);
}
@Override
public void update(String eventType, File file) {
System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
}
}
// 클라이언트 코드
Editor editor = new Editor();
editor.events.subscribe("open", new LoggingListener("some.log"));
editor.events.subscribe("save", new LoggingListener("some.log"));
try {
editor.openFile("test.txt");
editor.saveFile();
} catch (Exception e) {
e.printStackTrace();
}
객체 내부의 상태가 변하면 객체의 행동을 변경하게해 마치 클래스가 변한것처럼 작동하는 패턴
객체에는 다양한 상태가 있을 수 있다. 이때 각 상태에 따라 다른 상태로 변화가 가능할 수도 가능하지 않을 수도 있다
예를 들어 위 그림에서 객체가 A 상태에 있다면 B 상태로는 변할 수 있지만, B 상태를 거치지 않고 다른 상태가 될 수는 없다
이런 상태의 제약을 어떻게 코드로 표현할 수 있을까?
public enum PaymentStatus {
WAITING {
@Override
public PaymentStatus cancel() {
return PaymentStatus.CANCELED;
}
@Override
public PaymentStatus complete() {
return PaymentStatus.COMPLETED;
}
},
COMPLETED {
@Override
public PaymentStatus cancel() {
return PaymentStatus.CANCELED;
}
@Override
public PaymentStatus complete() {
throw new StatusConflictException("payment status is already competed");
}
},
CANCELED {
@Override
public PaymentStatus cancel() {
throw new StatusConflictException("payment status is already canceled");
}
@Override
public PaymentStatus complete() {
throw new StatusConflictException("canceled payment cannot be completed");
}
};
abstract public PaymentStatus cancel();
abstract public PaymentStatus complete();
}
@Entity
@Getter
@NoArgsConstructor
public class Ticket {
...
public void cancel() {
LocalDateTime cancelableUntil = round.getTicketCancelableUntil();
if (LocalDateTime.now().isAfter(cancelableUntil)) {
throw new TimeExceedException(
MessageFormat.format("ticket was cancelable until {0}", cancelableUntil)
);
}
paymentStatus = paymentStatus.cancel();
}
public void complete() {
paymentStatus = paymentStatus.complete();
}
}
일련의 알고리즘들을 정의하고, 이 알고리즘들을 하나의 클래스로 담아 이런 클래스들이 상호 교환 가능하게 만드는 패턴
하나의 동작을 여러가지 방식으로 실행할 수 있는 경우를 생각하자. 예를들어 출발 지점부터 도착 지점까지 경로를 추천해주는 네비게이션 앱을 만든다고 하자. 처음에는 자동차를 위한 경로 추천 기능만 추가했지만, 이후에는 자전거, 보행자, 여행객 등 다양한 경로 추천 기능을 추가하고 싶다. 이를 하나의 객체 내에서 구현한다면 다음과 같은 문제들이 발생한다
객체가 너무 커진다. 하나의 경로 추천 알고리즘의 수정이 객체 전체에 영향을 끼친다. 에러가 발생할 확률이 높아진다
협업이 어려워진다. 형상 관리 툴을 통해 협업한다면, 동일한 객체를 수정하다보니 팀원들이 계속 머지를 하고 충돌을 해결해야한다.
하나의 동작을 여러가지 방식으로 할 수 있는 객체에 알고리즘을 별도 객체로 분리한다. 이때 동작을 가진 객체를 context
라 하고, 분리된 알고리즘을 strategy
라고 한다
context
는 strategy
객체를 필드로 가진다. 하지만 context
가 전략을 선택하지는 않는다. 전략을 클라이언트에게 주입 받는 것으로 컨택스트는 단순히 특정 동작에 대한 인터페이스만 제공한다
컨택스트 객체의 수정 없이 전략을 추가할 수 있게 된다. 또한 런타임에 동작의 실행 방식을 바꿀 수 있다
// Strategy.java
public interface Strategy {
int execute(int a, int b);
}
// ConcreteStrategyAdd.java
public class ConcreteStrategyAdd implements Strategy {
@Override
int execute(int a, int b) {
return a + b;
}
}
// ConcreteStrategySubtract.java
public class ConcreteStrategySubtract implements Strategy {
@Override
int execute(int a, int b) {
return a - b;
}
}
// Context.java
public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
void changeStrategy(Strategy strategy) {
this.strategy = strategy;
}
int executeStrategy(int a, int b) {
return this.strategy.execute(a, b);
}
}
// 클라이언트 코드
int num1 = 10;
int num2 = 5;
Strategy strategy = new ConcreteStrategyAdd();
Context context = new Context(strategy);
context.executeStrategy(num1, num2);
알고리즘이 몇개 없고, 자주 바뀌지 않는다면 전략 패턴을 적용하는 것은 오히려 프로그램을 복잡하게 한다
전략이 간단하다면 그냥 익명 함수를 전달하는 것이 더 깔끔할 수 있다
알고리즘의 기본 구조를 부모 클래스에 정의하고 자세한 실행은 자식 클래스에서 구현하도록하는 패턴
문서 파일에서 정보를 읽어와 저장하는 프로그램을 만들고 싶다. 처음에는 Doc 파일만 지원했지만, 나중에는 CSV, PDF 파일까지 지원하게 된다. 여러 파일을 지원하고 보니 파일을 읽어오는 객체 간에 중복된 코드가 많다는 것을 알게 된다.
클라이언트 코드에서 위 객체들을 사용하려고 보니, 어떤 포멧의 파일인지에 따라 조건문을 통해 실행해야한다. 세 객체가 동일한 인터페이스를 가졌다면 조건문에 의한 분기 없이 다형성으로 해결할 수 있지 않을까?
public abstract class GameAI {
...
public void takeTurn() {
collectResources();
buildStructures();
buildUnits();
attack();
}
protected void collectResources() {
for (Structure s : this.builtStructures) {
s.collect();
}
}
protected abstract void buildStructures();
protected abstract void buildUnits();
protected void attack() {
enemy = closestEnemy();
if (enemy == null)
sendScouts(map.center);
else
sendWarrirors(enemy.position)
}
protected abstract void sendScouts(Position position);
protected abstract void sendWarrirors(Positon position);
}
public class OrcsAI extends GameAI {
@Override
protected void buildStrctures() {
if (자원이 있으면)
// 건물들을 지음
}
@Override
protected void buildUnits() {
if (자원이 충분하면)
// 피온을 만들고 정찰 그룹에 포함
else
// 그런트를 만들고 전사 그룹에 포함
}
@Overide
protected void sendScouts(Position positon) {
if (scouts.length > 0)
// 위치로 정찰을 보냄
}
@Override
protected void sendWarriors(Position position) {
if (warriors.length > 5)
// 위치로 전사를 보냄
}
}
public class extends GameAI {
@Override
protected void collectResources {}
@Override
protected void buildStructures() {}
@Override
protected void buildUnits() {}
}
알고리즘이 작동하는 객체에서 분리할 수 있게 하는 패턴
문법 규칙을 클래스화 한 구조로, 일련의 규칙으로 정의된 문법적 언어를 해석하는 패턴