티스토리 뷰

🐶prototype

 

Prototype

/ Design Patterns / Creational Patterns Prototype Also known as: Clone Intent Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their classes. Problem Say you have an object, and you want to

refactoring.guru

 

📝한 줄 정리

클라이언트 코드에서는 클래스를 신경 쓸 필요 없이 객체에 내장된 clone 인스턴스 메소드를 호출하여 해당 객체를 손쉽게 클론한다.

clone 메소드 동작 방식 : 자기 클래스의 생성자로 자신을 인자로 해서 넘김 -> 생성자가 새로운 객체 생성, 모든 필드 copy한 후 리턴

 

 

 

 

🤔해결할 문제 상황

당신은 어떤 객체의 완전 동일한 copy본을 만들고 싶다.

동일한 클래스의 객체를 생성한 뒤에 original 객체 필드의 값들을 새 객체로 일일히 복사하는 작업을 거친다고 하더라도 private한, original 객체 외부에서 접근 불가능한 것들은 copy할 방법이 없다.

또, 당신의 코드는 그 original 객체의 클래스에 dependent 해지고 때로는 original 객체의 concrete 클래스가 아닌 인터페이스 밖에 알아내지 못할 수도 있다.

 

 

🧐해결 방법

클론될 실제 객체에게 cloning process를 위임한다. (내부적으로 객체 자신을 생성자에 인자로 넘기는 식으로 클로닝 하는 인스턴스 메서드를 통해). 클로닝을 위한 공통 인터페이스를 정의하고 사용함으로써 당신의 코드가 original 객체의 클래스와 decoupling된 채 클론할 수 있도록 한다. 대개 그 인터페이스는 "clone"이라는 메소드를 포함하고 있을 뿐이다.

 

이 "clone" 메소드를 구현하는 방식은 모든 클래스에서 대개 비슷하다. 이 메소드는 현재 클래스의 새로운 객체를 생성한뒤 old 객체의 모든 필드를 새로운 객체로 copy함.

당연히 이때 private 필드도 모두 copy 가능한데, 대부분의 프로그래밍 언어에서 같은 클래스에 속한다면 한 객체에서 다른 객체의 private 필드로의 접근을 허용하기 때문이다.

 

cloning을 지원하는 객체를 프로토타입이라고 부른다. 만약 필드가 엄청 많고 수백가지의 가능한 설정이 있다면 cloning이 subclassing의 좋은 대체제가 될 수 있다.

이런식으로 동작 : config 방식이 여러가지인 객체 set이 있을 때, 이것들과 같은 객체를 만들려고 한다면 밑바닥 부터 새로운 객체를 생성하는 대신 프로토타입을 클로닝 하면 된다.

 

 

 

 

🍎현실 세계 비유

세포분열이랑 비슷하다. 완전 동일한 cell 쌍이 생기는 것 같음. original cell(= clone될 인스턴스)이 프로토타입 같이 작용하면서 클로닝에 active하게 참여한다.

 

 

 

 

 

🏗구조

 

 

 

👩‍💻pseudo 코드

// Base 프로토타입
abstract class Shape is
    field X: int
    field Y: int
    field color: string

    // 일반적인 생성자
    constructor Shape() is
        // ...

    // 프로토타입 생성자. 기존 객체의 값을 가지고 새로운 객체를 초기화 한다.
    constructor Shape(source: Shape) is
        this()
        this.X = source.X
        this.Y = source.Y
        this.color = source.color

    // clone 메소드는 Shape를 상속받은 자식 클래스의 객체를 리턴할 것임.
    abstract method clone():Shape


// Concrete 프로토타입. Base 프로토타입 상속 받음.
class Rectangle extends Shape is
    field width: int
    field height: int

    constructor Rectangle(source: Rectangle) is
        // 부모 클래스에 정의된 private 필드도 copy 하기 위해 부모 생성자도 호출해야 함.
        super(source)
        this.width = source.width
        this.height = source.height

    method clone():Shape is
        return new Rectangle(this)


class Circle extends Shape is
    field radius: int

    constructor Circle(source: Circle) is
        super(source)
        this.radius = source.radius

    method clone():Shape is
        return new Circle(this)


// 다른 클라이언트 코드 예시
class Application is
    field shapes: array of Shape

    constructor Application() is
        Circle circle = new Circle()
        circle.X = 10
        circle.Y = 10
        circle.radius = 20
        shapes.add(circle)

        Circle anotherCircle = circle.clone()
        shapes.add(anotherCircle)

        Rectangle rectangle = new Rectangle()
        rectangle.width = 10
        rectangle.height = 20
        shapes.add(rectangle)

    method businessLogic() is
        // 프로토타입의 멋진 점은 해당 객체의 타입을 모르고도 copy를 생성할 수 있다는 점이다
        Array shapesCopy = new Array of Shapes.

 
        // 예를 들어 shapes 배열의 element에 대해 clone 메소드를 구현한 shapes라는 것 외에
        // 아무것도 모르더라도 이 배열의 완전한 copy를 구할 수 있음
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

 

 

 

🍟적용 분야

- 당신의 코드가 copy할 객체의 클래스에 의존적이면 안되는 상황이라면 prototype 패턴을 도입해라.

=> 이런 상황은 주로 3rd-party 객체를 가지고 코드를 짜야할 때 발생한다. 이런 객체의 concrete 클래스는 알 수가 없다. 그리고 이런 클래스에 의존적이게 코드를 짜서도 안된다.

프로토타입 패턴은 클라이언트 코드에 어떤 객체의 cloning이든 지원하는 general한 인터페이스를 제공할 수 있다. 이 방식을 쓰면 copy할 클래스와 독립적인 클라이언트를 작성할 수 있음.

 

- 객체 초기화 방식만 좀 다른 자식 클래스 (특정 config를 가능하기 위해 생성했을..) 개수를 줄이고 싶다면 프로토타입 패턴을 사용할 것. 

=> 미리 여러 방식으로 config 된 pre-built 객체 set을 여러개의 자식 클래스 대신 제공한다음, 클라이언트 코드가 적당한 객체(프로토타입)을 골라 cloning하도록 한다.

 

 

 

 

🚀구현 방법

  1. 프로토타입 인터페이스를 생성하고 clone 메소드를 선언한다. 아니면 hierarchy 따라서 존재하는 모든 클래스에 clone 인스턴스 메소드를 추가한다.

  2. 프로토타입 클래스는 해당 클래스의 객체를 인자로 받는 생성자를 추가로 정의해야 한다. 이 생성자는 인자로 받은 객체의 모든 필드의 value를 새로 생성할 인스턴스에 복사해야 한다. subclass라면 super로 부모 생성자도 호출해서 부모 클래스 private 필드도 copy 해야함.
    만약 당신이 쓰는 프로그래밍 언어가 메소드 오버로딩을 지원하지 않는다면 객체 데이터를 copy할 특별한 메소드를 정의해야함. (new operator로 바로 클론된 새 객체를 받을 수 있으니 생성자를 쓰는게 가장 편하긴 하지만...)

  3. cloning 메소드는 주로 한 줄로 되어있다 : 2에서 정의한 프로토타입틱 버전의 생성자를 new operator로 호출하는 코드. 모든 클래스는 명시적으로 부모 클래스로 부터 clone 메소드를 오버로딩 해서 인자로 자기 클래스의 객체를 생성자에 넘겨야 한다. 안그러면 clone 메소드를 호출했을 때 부모 클래스의 객체가 리턴될 수 있음.

  4. 자주 사용되는 프로토타입(clone 할 수 있는 인스턴스 객체)를 카탈로그 처럼 저장하고 있기 위해 중앙화된 프로토타입 registry를 운영하는 것도 괜찮은 방법이다.
    방법은 다양한데, 이 registry를 새로운 팩토리 클래스 처럼 구현하거나,
    일반적인 base 프로토타입 클래스에 이런 registry를 static 메소드로 정의해두고 프로토타입 fetch할 수 도 있다. (클라이언트 코드에서 메소드로 넘긴 search 기준에 따라 프로토타입을 찾은 다음 클론한 객체를 리턴함)
    (그리고 클라이언트 코드에서 subclass들의 생성자로의 직접 호출을 프로토타입 registry의 팩토리 메소드 호출로 교체할 것)

 

 

 

🔮장점

- 실제 클래스와 coupling 되지 않은 채 객체를 클론할 수 있다.

- clone 메소드가 pre-built된 프로토타입 객체를 사용하면 반복적인 초기화 코드를 제거 할 수 있다.

- 복잡한 객체를 더 편하게 생성할 수 있다.

- 복잡한 객체의 config 세팅 방식을 고민하고 있다면 subclassing의 대체제로 프로토타입을 사용할 수도 있다. (prototype registry 활용하여..?)

 

 

 

🧸단점

- circular reference를 가진 복잡한 객체를 cloning하는 것은 매우 까다롭다.

 

 

 

 

🏀루비 코드

class Prototype
    # primitive 필드 => 원시 타입
    # component 필드 => 참조 타입
    # circular_refence => 자신을 가리키는 포인터 객체
    attr_accessor :primitive, :component, :circular_reference
  
    def initialize
      @primitive = nil
      @component = nil
      @circular_reference = nil
    end
  
    def clone
      p2 = deep_copy(self)
    end
  
    # deepy copy 효과를 내기 위해 마샬링 라이브러리를 썼는데, 느리고 비효율적이니까 
    # 실제 어플리케이션에서는 이것을 위한 gem을 사용할 것을 권장.
    private def deep_copy(object)
      Marshal.load(Marshal.dump(object))
    end
  end
  
  class ComponentWithBackReference
    attr_accessor :prototype
  
    def initialize(prototype)
      @prototype = prototype
    end
  end
  
  ##### 클라이언트 코드
  p1 = Prototype.new
  p1.primitive = 245
  p1.component = Time.now
  p1.circular_reference = ComponentWithBackReference.new(p1)
  p2 = p1.clone
  
  if p1.primitive == p2.primitive
    puts 'Primitive field values have been carried over to a clone. Yay!'
  else
    puts 'Primitive field values have not been copied. Booo!'
  end
  
  if p1.component.equal?(p2.component)
    puts 'Simple component has not been cloned. Booo!'
  else
    puts 'Simple component has been cloned. Yay!'
  end
  
  if p1.circular_reference.equal?(p2.circular_reference)
    puts 'Component with back reference has not been cloned. Booo!'
  else
    puts 'Component with back reference has been cloned. Yay!'
  end
  
  if p1.circular_reference.prototype.equal?(p2.circular_reference.prototype)
    print 'Component with back reference is linked to original object. Booo!'
  else
    print 'Component with back reference is linked to the clone. Yay!'
  end


p ''
p "#{p1.object_id} / #{p1.circular_reference.prototype.object_id}"
p "#{p2.object_id} / #{p2.circular_reference.prototype.object_id}"

*The marshaling library converts collections of Ruby objects into a byte stream

dump : Ruby 객체 -> serialized data

load : serialized data -> Ruby 객체

 

 

 

 

 

🦄자바스크립트의 prototype pattern

* 자바스크립트의 객체가 프로토타입 기반이다. 상속 개념 => 프로토타입 체이닝으로 구현.

https://www.patterns.dev/posts/prototype-pattern/

Object.create() 메소드는 기존에 이미 있는 객체를 프로토타입으로 하여 새로운 객체를 생성한다. 

 

댓글
공지사항
최근에 올라온 글