엄범

 

Effective Java : 아이템 18. 상속보다는 컴포지션을 사용하라

[Effective Java] 4장 클래스와 인터페이스

  • 상속이란?
    • extends를 말함. (implements는 아님. 이건 구현.)
  • 컴포지션이란?
  • 어떤 클래스에 기능을 추가하거나, 책임을 더해서 확장하고 싶을 때 사용한다.

 

상속의 단점?

상속과 컴포지션의 차이는 즉, @Override 하느냐 메서드를 호출해서 delegate 하느냐의 차이로 볼 수 있는데 

 

@Override를 통해 하위 클래스에서 변경한 메서드는, 상위 클래스 메서드에도 영향을 미칠 수 있기 때문에 상속이 캡슐화를 깨뜨릴 수 있다.

 

  • 그래서 #아이템 19 에서 상속을 금지하라고 가이드 하고 있는 것.
  • 보통 상속은 더 구현하기 까다롭고 복잡한 경우가 많다.
    • 상속하다 보면 부모 클래스 쪽의 구현사항에서 이것 저것 신경써야 하는 경우도 많고... 예를들면 __slots__같은 거라던가...

 

 

반면 컴포지션을 통해 delegate하면 Composition 대상 클래스의 내부 구현을 건드리는게 아니기 때문에 캡슐화를 깨뜨리지 않는다.

 

 

위임(Delegation) 이란?

Composition으로 필요한 클래스 가져오고, 새 클래스의 메서드가 Composition 클래스의 대응하는 메서드를 호출하도록 전달(Forwarding) 하는 것.

 

  • Composition + Forwarding을 위임(Delegation) 이라 한다.
    • 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임에 해당하지만, 넓은 의미로 이렇게 쓰이는 경우가 많다.
  • 상속도 super()만 호출하면 위임 비슷한거 아니냐..라고 생각할 수 있지만 Delegation은 정의 자체가 Composition을 전제로 하고 있다.
  • https://en.wikipedia.org/wiki/Delegation_pattern 여기서는 Pattern이라고 부르고 있으나, 그 자체로 패턴이라기 보다는 방법(?)이라고 보는게 더 맞는 것 같음. 정의도 그렇고.
    • Delegation을 사용해서 Decorator Pattern을 구현하게 되므로...
    • 보통 얘기할 때도 얘한테 위임한다. 라고 얘기하지 Delegation Pattern을 사용한다. 라고 얘기하지는 않는 것도 있고
  • 언어 차원에서 Delegation을 지원해주는 경우도 있다!

 

Decorator Pattern (Wrapper Class)

https://sourcemaking.com/design_patterns/decorator

 

  • 어떤 클래스를 Wrapper 클래스로 감싸는 패턴을, 기능을 덧씌운다는 의미에서 Decorator Pattern이라고 부른다.
    • Wrapper 클래스는(또는 Forwarding 클래스는) delegation을 사용할 수 밖에 없다. 원본 클래스를 wrapping 하는게 목적이므로, 원본 클래스 메서드를 호출해야 의미가 있는 경우가 대부분이니까.
  • 예시) Wrapper 클래스 (상속 대신 컴포지션) / 재사용할 수 있는 Forwarding 클래스
    • InstrumentedSet 안에 private final로 Set을 가져도 되는데 중간에 ForwardingSet을 한 번 거치는 이유는? 재활용성 때문
      • 만약 ForwardingSet이 없었다면,
        • InstrumentedSet을 만들 때 clear, isEmpty 같은 Forwarding Method를 각각 다 만들어줘야 하고,
        • anotherWrapperSet을 만들 때 또 clear, isEmpty 등을 각각 다 만들어줘야 하니까.
      • 그래서 by 같은 키워드 지원이 된다면 굳이 필요 없지 않을까 하는 생각이 든다.
      • 또는, 굳이 재활용이 필요하지 않다면 Forwarding 클래스를 건너 뛰어도 될 듯.
    • 사용은 아래와 같이 한다.

```java

Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));

Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));

```

 

 

상속과 컴포지션을 구분하는 방법?

  • 상속은 진짜 "B is A" 관계가 성립할 때만 써야 한다. 
  • 상속은 단순 기능 추가 보다는 좀 더 의미론적으로 접근하는게 맞는 것 같다.
    • 상속 계층을 따져보면 상속을 받을 수 밖에 없는 경우?
    • 타입 문제 때문에 하위 클래스로 만들어 다형성을 이용해야 하는 경우는... interface를 이용한 컴포지션으로 커버 가능하다.
      • ForwardingSet과 InstrumentedSet의 예제.
      • 굳이 class를 extends할 필요 없다는 것. 목적이 다형성이면 interface implements로 충분하다.
  • 자바 플랫폼 라이브러리에도 잘못된 예들이 있는데
    • Stack은 Vector가 아니므로, 상속 보다는 컴포지션을 사용했다면 좋았을 것이고
    • Properties도 Hashtable이 아니므로, 상속이 아니라 컴포지션이 더 좋았을 것임

 

 

np.ndarray의 경우

```python

import numpy as np

 

# sol 1.

class OperatorOverloading:

    def __init__(self):

        self.board = np.zeros((8, 8), dtype=int)

 

    def __getitem__(self, point):

        i, j = point

        return self.board[i][j]

 

    def myFunc(self):

        return "my function!"

 

A = OperatorOverloading()

print(A[1,2])

print(A.myFunc())

 

# sol 2.

class Extern(np.ndarray):

    """

    https://docs.scipy.org/doc/numpy/user/basics.subclassing.html

    type casting하는 방식 말고, 좀 더 복잡하지만 __new__ 등을 overloading해서 완전 서브클래스로 사용하는 방법도 있다.

    """

    def myFunc(self):

        return "my function!"

 

_B = np.zeros((8, 8), dtype=int)

B = _B.view(Extern)

print(B[1][2])

print(B.myFunc())

 

# 확장 함수는 지원하지 않음.

C = np.zeros((8, 8), dtype=int)

C.myFunc = lambda x: "my function!"

print(C.myFunc())    # AttributeError: 'numpy.ndarray' object has no attribute 'myFunc'

```

 

socket.socket의 경우

```python
class LogSocket(socket.socket):
    def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, fileno=None):
        socket.socket.__init__(self, family, type, proto, fileno)
        self.next_retry_time = 0
        self.retry_factor = 2
        self.retry_period = 1
        self.retry_time_max = 30
 
    def recv(self, bufsize = 4096):
        cmd_raw = super.recv(bufsize)
        cmd = cmd_raw.decode()
        return cmd
 
    def exponential_backoff_connect(self, address):
        while True:
            result = self._exponential_backoff_connect(address)
            if result in (True, False):
                return result
 
    def _exponential_backoff_connect(self, address):
        now = time.time()
 
        if now > self.next_retry_time:
            try:
                print("Try...")
                self.connect(address)
                return True
            except OSError:
                self.next_retry_time = now + self.retry_period
                self.retry_period *= self.retry_factor
                if self.retry_period > self.retry_time_max:
                    print("retry_time_max! {}".format(self.retry_time_max))
                    return False
                print("Fail : wait {}".format(self.retry_period))
        return None
```
 

```python

class SocketWrapper:

    __slots__ = ("_sock")

 

    def __init__(self, sock):

        self._sock = sock

    

    def recv(self, bufsize = 4096):

        cmd_raw = self._sock.recv(bufsize)

        cmd = cmd_raw.decode()

        return cmd

```