생성패턴은 인스턴스를 만드는 절차를 추상화하는 패턴.
객체의 표현방법과 시스템을 분리해주는 것
클래스 생성패턴이 인스턴스로 만들 클래스를 다양하게 만들기 위한 용도로 상속을 하는반면, 객채생성패턴 은 인스턴스화 작업을 다른 객체에게 떠넘길 수 있다.
생성패턴을 사용하면 다음과 같은 장점을 가질수 있습니다.
즉, 무엇이 생성되고 누가 만들며, 어떻게 생성되고 언제 생성되어지는지를 결정하는데 유연성을 제공한다.
enum Direction {North, South, East, West};
public abstract class MapSite{
public abstract void Enter();
}
public class Room extends MapSite{
private final int _roomNumber;
private MapSite[] _sides;
public Room(int roomNumber){ this._roomNumber = roomNumber; }
public abstract void Enter();
public abstract void setSide();
public abstract void getSide();
}
public class Wall extends MapSite{
public Wall(){ };
public abstract void Enter();
}
public class Door extends MapSite {
private final Room _room1,_room2;
private boolean _isOpen;
public Door(Room room1, Room room2){
this._room1 = room1;
this._room2 = room2;
}
public abstract void Enter();
public abstract otherSideFrom(Room room);
}
public class Maze {
public Maze(){};
public abstract void addRoom(Room room);
public abstract Room roomNo(int roomNumber);
}
라고 가정했을 경우 다음과 같은 코드를 제시할 수 있습니다.
class MazeGame{
public Maze createMaze() {
Maze maze = new Maze();
Room r1 = new Room(1);
Room r2 = new Room(2);
Door theDoor = new Room(r1,r2);
maze.addRoom(r1);
maze.addRoom(r2);
r1.setSide(North, new Wall());
r1.setSide(East,theDoor);
r1.setSide(South, new Wall());
r1.setside(West, new Wall());
r2.setSide(North, new Wall());
r2.setSide(East, new Wall());
r2.setSide(South, new Wall());
r2.setSide(West, theDoor);
return maze;
}
}
여기서 미로하나를 생성하는데 있어서
r1.setSide(North, new Wall());
r1.setSide(East,theDoor);
r1.setSide(South, new Wall());
r1.setside(West, new Wall());
r2.setSide(North, new Wall());
r2.setSide(East, new Wall());
r2.setSide(South, new Wall());
r2.setSide(West, theDoor);
총 8줄의 코드를 일일이 생성해줘야 된다는 단점이 있습니다. 이러한 코드를 일일이 각 벽마다 설정해줄 필요 없고, 단지 연결되는 부분
r1.setSide(East, theDoor);
r2.setSide(West, theDoor);
정도만 있으면 되는 것이죠.
생성패턴은 이런상황에서 유연한 설계 를 할 수 있도록 제공해줍니다.
의도 : 상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공.
동기 : 여러 응용 프로그램을 보면 서로다른 표준 락앤필을 가지는 경우가 많다. 스크롤바, 윈도우 버튼 제각각 모양도 다르고, 동작방식도 다르게 되는 경우가 많다. 이런 응용 프로그램이 서로 다른 록앤필 표준에 상관없이 이식성을 가지려면 각 사용자 인터페이스 툴킷에서 제공하는 위젯을 직접 사용하지 못하도록 해야 합니다. (// 이부분 추후 보완할 것)
- 룩앤필(look and feel)이란?
소프트웨어 디자인에서 룩앤필은 GUI(Graphical User Interface)측면과 디자인의 형태 구성에서 사용되는 용어로 색상, 모양, 레이아웃, 서체(look)와 같은 요소와 버튼, 박스, 메뉴(feel)와 같은 동적 요소의 행위를 포함한다.
구성요소
장점
구체적인 클래스를 분리
응용프로그램이 생성할 객체의 클래스를, 팩토리의 클래스로 한정지을 수 있습니다. 즉, 제품 객체를 생성하는 과정과 책임을 캡슐화 한 것이기 때문에, 구체적인 구현 클래스가 사용자에게서 분리 됩니다.
높은 이식성
팩토리 클래스는 응용 프로그램에서 한번만 나타나기 때문에 응용 프로그램이 사용할 팩토리를 변경하기 쉽습니다.
일관성 유지
팩토리 클래스 내에서 객체의 생성을 관리하고, 그 객체들은 추상 팩토리에서 제시한 인터페이스 내에서 구현하도록 설계 되어야 하기 때문에 프로그램이 일관성을 가질 수 있도록 합니다.
단점
낮은 확장성
추상 팩토리가 제공하고 있는 인터페이스의 변경시, 추상팩토리가 제공하고 있는 모든 서브클래스에서 해당 코드가 구현되어져야함.
다음은 위 미로코드를 추상 팩토리 패턴으로 관리했을때의 예시입니다.
abstract class MazeFactory {
public MazeFactory(){};
public abstract Maze makeMaze();
public abstract Wall makeWall();
public abstract Room makeRoom();
public abstract Door makeDoor(r1,r2);
}
라는 클래스가 존재한다면, 다음과 같이 구현할 수 있습니다.
class MazeGame{
void Maze createMaze(MazeFactory factory){
Maze maze = factory.makeMaze();
Room r1 = factory.makeRoom(1);
Room r2 = factory.makeRoom(2);
Door door = factory.makeDoor(r1,r2);
maze.addRoom(r1);
maze.addRoom(r2);
r1.setSide(North, factory.makeWall());
r1.setSide(East,door);
r1.setSide(South, factory.makeWall());
r1.setside(West, factory.makeWall());
r2.setSide(North, factory.makeWall());
r2.setSide(East, factory.makeWall());
r2.setSide(South, factory.makeWall());
r2.setSide(West, door);
}
}
라고 구현을 할 수 있으며, 각각의 기능에 맞는 MazeFactory만 구현함으로써 내부가 어떻게 구현되어있는지 상관없이 미로를 변경할수 있습니다.
동기 : 복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리
즉 생성하는 방법과 구현하는 부의 책임을 서로 분리해서 간결한 코드를 제작하겠다는 것.
표현하는 곳에서는 해당 객체가 어떻게 생성됐는지에 대한 관심은 없고, 객체를 어떻게 이용하겠다만 서술
반대로 생성하는 곳에서는 어떻게 이용하는지는 관심없고, 객체를 어떻게 생성하겠다만을 서술
위와같이 결합을 느슨하게 함으로써, 클래스 생성방법이 변경되더라도 이를 이용하는 나머지 클래스에는 변경없이 코드를 작성할 수 있다.
장점
낮은 결합성(Loose Coupling)
클래스 내부를 알고있는 것은 Builder 객체 밖에 없습니다. 따라서 클래스 내부를 어떻게 표현하고 싶은지에 대해서 사용자들은 몰라도 됩니다. 즉 빌더가 어떤 제품을 만드는지
만 알면 됩니다.
생성과 표현에 필요한 코드를 분리
추후 클래스 내부의 변경이 필요하다면, 클래스 전체를 변경할 필요없이 생성부와 관련된 클래스만 변경하면 되기때문에, 변경에 대한 Side Effect를 최소화할 수 있습니다.
생성하는 절차를 세밀하게 나눌 수 있음
abstract class MazeBuilder{
public abstract void buildMaze();
public abstract void buildRoom(int room);
public abstract void buildDoor(int roomFrom, int roomTo);
public abstract Maze getMaze();
}
//다음과 같이 이용할 수 있음
Maze createMaze(MazeBuilder builder){
builder.buildMaze();
builder.buildRoom(1);
builder.buildRoom(2);
builder.buildDoor(1,2);
return builder.getMaze();
}
사실 빌더 패턴을 사용하는 예로는, 객체 생성을 유연하게 하기위함이 가장 크다라고 본다.
쉬운예제, (자바의 StringBuilder)
class Member(){
private final String name;
private final int age;
private final Gender gender;
private Member(Builder builder){
this.name = builder.name;
this.age = builder.age;
this.gender = builder.gender;
}
public static class Builder{
private String name;
private int age;
private Gender gender;
public Builder name(String name){
this.name = name;
}
public Builder age(int age){
this.age = age;
}
public Builder gender(Gender gender){
this.gender = gender;
}
public Member build(){
return new Member(this);
}
}
}
//이렇게 함으로써 객체 생성에 대한 변화에 유연하게 대처가능.
Member member = new Member.Builder()
.name("김동근")
.age(28)
.gender(Gender.Man)
.build();
의도 : 객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브 클래스가 한다.
활용성
구성요소
장점
서브클래스에 대한 훅 메소드를 제공(높은 이식성)
클래스 내부에서 객체를 생성하는 것이 객체를 직접생성하는 것보다 훨씬 응용성이 높아짐
간단한 예로, 로그인이라는 클래스가 있다면 이 클래스를 상속해서 네이버 로그인, 카카오 로그인으로 분리해서 생각할 수 있다는 것.
병렬적인 클래스 계통을 연결하는 역할을 담당
특정 클래스만이 팩토리 메소드를 호출하게 되어있습니다. 하지만 꼭 이럴때만 유용한 것이 아니고, 클래스가 자신의 책임을 분리되니 다른 클래스에 위임할때, 유연하게 대처할 수 있음
단점
ConcreteProduct 객체 하나만 만들려 할 때에도 Creator 클래스를 서브클래싱해야 할지 모른다는점
(즉, 단일 상속 개체에도 굳이 책임을 분리해야하나 말아야하나)
public abstract class MazeGame{
abstract public Maze makeMaze();
abstract public Room makeRoom(int n);
abstract public Wall makeWall();
abstract public Door makeDoor(Room r1, Room r2);
public void Maze createMaze(MazeFactory factory){
Maze maze = makeMaze();
Room r1 = makeRoom(1);
Room r2 = makeRoom(2);
Door door = makeDoor(r1,r2);
maze.addRoom(r1);
maze.addRoom(r2);
r1.setSide(North, makeWall());
r1.setSide(East,door);
r1.setSide(South, makeWall());
r1.setside(West, makeWall());
r2.setSide(North, makeWall());
r2.setSide(East, makeWall());
r2.setSide(South, makeWall());
r2.setSide(West, door);
}
}
여기서 다른 게임을 만든다면 다음과 같이 구현하면됨
public class FiredWallMazeGame(){
@Override
public Maze makeMaze(){
return new FireWall();
}
@Override
public Room makeRoom(int n){
return new FireRoom(n);
}
...
}
오직 미로가 어떻게 동작하는지에 대한 매커니즘은 그대로 두고, 필요한 부분의 메소드만 변경함으로써 원하는 동작을 구현할 수 있습니다.
다만 클래스를 상속해서 책임을 분리해야 한다는 과정때문에 번거로움이 많이 발생하고 자바는 interface를 이용한 Strategy 패턴을 주로 사용
의도 : 원형이 되는 인스턴스를 사용하여 생성할 객체의 종률 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성.
활용성
구조
추상팩토리
및 빌더
와 비슷한 결과를 가짐.장점
런타임에 새로운 제품을 추가하고 삭제할 수 있음
원형패턴을 이용하면 사용자에게 원형으로 생성되는 인스턴스
를 등록하는 것만으로도 시스템에 새로운 클래스를 추가하는 것과 같은 효과를 가짐
값들을 다양화함으로써 새로운 객체를 명세
고도로 동적화된 시스템에서는 새로운 클래스를 생성할 필요없이 인스턴스의 조합만으로 새로운 객체를 정의
구조를 다양화 함으로써 새로운 객체를 명세
서브클래스의 수를 줄임
클래스에 따라 응용프로그램을 설정할 수 있음.
class MazePrototypeFactory extends MazeFactory {
private Maze prototypeMaze;
private Room prototypeRoom;
private Wall prototypeWall;
private Door prototypeDoor;
public MazePrototypeFactory(Maze m, Wall w, Room r, Door d){
this.prototypeMaze = m;
this.prototypeWall = w;
this.prototypeRoom = r;
this.prototypeDoor = d;
}
public Wall makeWall(){
return prototypeWall.clone();
}
public Door(room r1, room r2){
Door door = porototypeDoor.clone();
door.initialize(r1,r2);
return door;
}
}
//다음과 같이 사용가능합니다.
public class Main(){
public static void main(String ...args){
MazeGame game = new MazeGame();
MazePrototypeFactory simpleMazeFactory =
new MazePrototypeFactory(new Maze(), new Wall(), new room(), new Door());
Maze maze = game.createMaze(simpleMazeFactory);
//만약 게임을 변경하고 싶다.
maze = game.createMaze(new Maze(), new FireWall(), new FireRoom(), newFireDoor());
}
}
사용자는 Clone() 연산의 반환값을 자신이 원하는 타입으로 다운캐스트할 필요가 절대로 없어야 함.
abstract class Car implements Cloneable {
public Frame frame;
public Wheel wheel;
public Car(Frame frame, Wheel wheel) {
this.frame = frame;
this.wheel = wheel;
}
@Override
public String toString() {
return "Car{" +
"frame=" + frame +
", wheel=" + wheel +
'}';
}
@Override
protected Object clone() {
Object obj = null;
try {
obj = super.clone();
}catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
class Avante extends Car{
public Avante(Frame frame, Wheel wheel) {
super(frame, wheel);
}
public void changeFrame(Frame frame) {
this.frame = frame;
}
@Override
public String toString() {
return "Avante{" +
"frame=" + frame +
", wheel=" + wheel +
'}';
}
}
class Frame {
private String name;
private String color;
public Frame(String name, String color) {
this.name = name;
this.color = color;
}
@Override
public String toString() {
return "Frame{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
class Wheel {
private String name;
private int size;
public Wheel(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public String toString() {
return "Wheel{" +
"name='" + name + '\'' +
", size=" + size +
'}';
}
}
public class Main {
public static void main(String[] args) {
Frame frame = new Frame("avante V1", "red");
Wheel wheel = new Wheel("avante V1", 18);
Avante redAvante = new Avante(frame, wheel);
Avante newAvante = (Avante) redAvante.clone(); // <- 객체 복사
Frame newFrame = new Frame("avante V2", "white");
newAvante.changeFrame(newFrame);
System.out.println(redAvante);
System.out.println(newAvante);
}
}
//출처 : https://github.com/sup2is/study/tree/master/design-pattern/prototype-pattern
의도 : 오직 하나의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근을 제공
활용성 :
장점
유일하게 존재하는 인스턴스로 접근 통제
Singleton
클래스 자체가 인스턴스를 캡슐화 하기 때문에, 이 클래스에서 사용자가 언제, 어떻게 인스턴스로 접근할 수 있을지 제어.
네임스페이스 절약
전역변수를 사용하기 때문에 namepsace
를 따로 망치는 일을 없애준다.
나머지는 장점인지는 잘...
구현
인스턴스가 유일해야함을 보장
이것을 보장할 수없기때문에 안티패턴으로 분류
class MazeFactory {
private MazeFactory insatnace;
private MazeFactory(){}
public static MazeFactory getInstance(){
if(instance == null)
this.instance = new MazeFactory();
return this.instance;
}
}
싱글톤 패턴은 일반적으로 안티패턴으로 분류
그 이유는 싱글코어, 단일쓰레드로 동작하는 시대가 아니기때문에, 인스턴스가 유일함을 보장할 수 없음.
또한 간단한 코드인 경우 다음과 같이 대응할 수 있는데, 이 또한 코드가 복잡해질 경우 Deadlock
을 발생시킬 우려가 있다.
class MazeFactory {
private MazeFactory insatnace;
private MazeFactory(){}
//동기화하여 유일 객체 생성 보장
public static syncronized MazeFactory getInstance(){
if(instance == null)
this.instance = new MazeFactory();
return this.instance;
}
}
또한 private한 생성자를 가지고 있기 때문 상속을 사용할 수 없음.
따라서 싱글톤 패턴은 그 값이 변하지 않는 읽기전용
의 경우에만 사용하는 것이 적절.