클래스와 인터페이스 - 규칙 20 태그 달린 클래스 대신 클래스 계층을 사용하라.

JAVA/Effective Java|2018. 5. 29. 22:39

클래스에 상황에 따라서 역할을 변경하는 

태그 달린 클래스를 볼 수 있다.

예를 들어, 아래 유형과 같이 탈것을 나타내는 클래스는
어떤 형태로 사용하느냐에 따라서 자동차가 될수도, 자전거가 될 수도 있다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Vehicle {
  enum Type { CAR, BICYCLE };
  final Type type;
  
  public Vehicle (double pedalCount) {
   this.type = Type.BICYCLE;
  }
 
  public Vehicle (double license, int sheetCount) {
    this.type = Type.CAR;
  }
 
  // 바퀴수를 반환하는 메서드
  public int getWheelCount() {
   switch (type) {
    case CAR:
       return 4;
       break;
    case BICYCLE:
       return 2;
       break;
   }
  }
 
}
cs




하지만 이럴경우, 종류에 따라 Vehicle 클래스에서
다루어주어야 할 부분이 너무 많다. 

예를 들면,
위에 보면 Vehicle클래스 타입에 따라 getWheelCount 메서드를 사용할 때, switch문을 사용해야 하고 굉장히 번잡스럽다.

이러한 이유들로
관리도 힘들고 버그가 발생할 위험도 있다.

또한 이런 경우 단일 책임 원칙 (Single Responsibility Principle) SRP를 위해하는 행위이다.

이런 경우 태그 기반 클래스(tagged class)를 사용하지 말고 계층기반으로 변경한다.



1
2
3
public abstract class Vehicle {
 abstract int getWheelCount();
}
cs




Vehicle을 추상클래스로 생성하고, 이를 Car, Bicycle클래스에서 상속받아 구현하여 

getWheelCount를 자신의 클래스에 맞게 재정의 하여 사용하도록 처리한다.

그럴 경우, 처리도 단순해지고 유지보수에도 수월해진다.


결론을 말하자면,

태그 기반 클래스를 피하고 클래스 계층을 사용한다.



출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙20인용.



댓글()

클래스와 인터페이스 - 규칙 19 인터페이스는 자료형을 정의할 때만 사용하라.

JAVA/Effective Java|2018. 5. 29. 22:38

인터페이스를 구현하는 클래스를 만들경우,

그 인터페이스는 해당 클래스의 객체를 참조할 수 있는 자료형 역할을 하게 된다. 

상수 인터페이스
=> 메서드가 없고 static final 필드만 있어, 상수 이름 앞에 클래스 이름을 붙이는 번거로움을 피하기 위해서이다.



1
2
3
public interface PhysicalConstants {
  static final double AVOGADROS_NUMBER = 6.02312312323;
}
cs




이런 상수 인터페이스 패턴은 인터페이스를 잘못 사용한 것이다.
=> 인터페이스는 사용자에게 이러한 기능을 구현한 클래스라는 명세를 알려주는거와 같은데 상수 인터페이스는 기능 명세를 제공하는 것이 아니라 상수값을 사용하기 위한 것으로 좋은 방식이 아니다.

또한 

추후에 이런 상수 인터페이스에 정의된 상수를 사용하지 않게 되더라도 하위호완성 때문에 제거를 하지 못하는 문제가 발생한다.

이런 상수들이 필요한 경우에는 상속이 불가능하고, 객체 생성이 불가능 하도록 private 생성자를 가지고 있는 유틸 클래스에 정의해서 사용하는 것이다.

만약 유틸리티 클래스에 정의된 상수 값을 자주 사용하게 된다면, 
JDK 1.5부터 도입된 정적 임포트(static import) 기능을 사용하여 클래스 이름을 제거하여 사용할 수 있다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Constant {
  private Constant() {}
  
  public static BABO = 2412.424;
}
 
 
 
import static com.wedul.wedulpos.Constant;
 
public Class WedulPosDto {
 double getData() {
  return BABO;
 }
}
cs



정리하면 인터페이스는 클래스의 명세로서 자료형을 정의할 때만 사용하고, 그 특정 목적을 벗어난 특정 상수와 같은 상수 인터페이스는 사용을 자제해야 한다.

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙19 인용.

댓글()

클래스와 인터페이스 - 규칙 18 추상 클래스 대신 인터페이스를 사용하라.

JAVA/Effective Java|2018. 5. 29. 22:36

자바는 다중 상속이 되지 않기 때문에, 추상 클래스 보다 인터페이스를 사용하는 것이 좋다.

믹스인
인터페이스는 믹스인을 정의하는 데 이상적이다.
=> 믹스인은 클래스가 주 자료형 이외에 추가로 구현할 수 있는 자료형으로 어떤 선택적 기능을 제공한다는 사실을 선언하기 위해 쓰인다.
=> 예를 들면, Comparable은 어떤 클래스가 자기 객체를 다른 객체와의 비교 결과에 따른 순서를 갖는다고 선언할 때 쓰는 인터페이스이다.

이런 믹스인 기능을 추상클래스에 할 수 없다. 
=> 클래스가 가질 수 있는 상위 클래스는 하나 이기 때문에 좋은 방법이 아니다.

인터페이스는 여러 속성을 합쳐서 새로운 속성을 만들 수 있다.
=> singer와 SongWriter 속성을 합쳐서 새로운 인터페이스를 만들 수 있다.



1
2
3
4
5
6
7
8
9
10
public interface singer {
}
 
public interface SongWriter {
 
}
 
public interface SingerSongWriter extends Singer, SongWriter {
 
}
cs




하지만 이런 조합을 인터페이스가 아닌 클래스를 이용한다면 별도의 클래스를

계속 만들어 주어야 한다.

필요한 속성이 n개가 있을 때 조합의 가짓수는 2의 n승으로 이것을 조합 폭증이라고 한다.

추상 골격 구현 (Abstract skeletal implemetation)
해당 추상 골격 구현 클래스를 중요 인터페이스마다 두면, 인터페이스의 장점과 추상 클래스의 장점을 결합 할 수 있다.

방법
=> 인터페이스로는 자료형을 정의하고, 구현하는 일은 골격 구현 클래스에 맡기면 된다.

Ex) List



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 골격 구현 위에서 만들어진 완전한 List 구현
static List<Integer> intArrayAsList(final int[] a) {
  if (a == null
     throw new NullPointerExeception();
 
  return new AbstractList<Integer>() {
     public Integer get(int i) {
        return null;
     }
 
     @Oveerride
     public Integer set(int i, Integer val) {
         int oldVal = a[i];
         a[i] = val;
         return oldVal;
      }
 
      public int size() {
         return a.length;
      }
  };
}
cs



인터페이스 보다 추상 클래스가 좋은 경우는
새로운 기능이 추가되었을 때, 인터페이스는 새로운 메서드를 추가하더라도 새로 모두 구현해 주어야 하지만 추상클래스의 경우에는 그대로 하위클래스에서 사용할 수 있다는 것이다.
=> 하지만 Java 8에서 default 메소드를 통해 이 단점 또한 해결이 되었다.

또한 
public 인터페이스의 경우 공개되고 난 다음에는, 인터페이스 수정이 거의 불가능 하다. 그러므로 처음 설계부터 잘 구현해야 한다는 단점이 있다.

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙18 인용.



댓글()

클래스와 인터페이스 - 규칙 17 계승을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 계승을 금지하라.

JAVA/Effective Java|2018. 5. 29. 22:34


16번에서 정의한 것처럼 계승 (이하 상속)을 받을 경우 문제를 야기할 수 있다.

그렇기에 이를 예방하기 위해서 문서를 잘 갖추고 있어야 하는데 
문서를 갖춘다는 것은 어떤 의미일까?

이는 재정의 가능 메서드를 내부적으로 어떻게 사용하는지(self-use)반드시 남기라는 것이다.
-> 어떤 재정의 가능 메서드를 어떤 순서로 호출하는지, 그리고 호출결과가 추후 어떤 영향을 미치는지 문서로 남기라는 의미이다.

여러 문제를 야기할 수 있기에, 계승에 맞도록 설계하고 문서화하지 않은 클래스에 대한 하위 클래스는 만들지 않아야 한다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙17 인용.

댓글()

클래스와 인터페이스 - 규칙 16 계승하는 대신 구성하라.

JAVA/Effective Java|2018. 5. 29. 22:33

상속은 코드를 재사용 할 수 있도록 돕는 강력한 도구이지만, 항상 최선이라 할 수 없다.

제대로 구현하지 못한 경우 문제를 유발할 가능성이 크기 때문이다.

그리고 같은 패키지 내부에 존재하는 클래스를 상속받아야한다.

상속은 일반적으로 캡슐화 원칙을 위반한다.
그 이유는 상위클래스의 일부 개념이 변경될 경우, 하위클래스는 그대로 영향을 받을 수 밖에 없기 때문이다.

이를 예방하기 위해서는 문서를 제대로 구현해 놓아야 한다.

문제가 야기될 수 있는 상황은 다음과 같다.
1. 기존의 재정의 하여 사용하였을 경우.
-> 문제가 될 수 있는 상황을 예를 들면, HashSet을 상속하고 add관련 메서드를 재정의하여 객체를 추가한 횟수를 확인하는 로직을 추가하는 로직을 만들어보자



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Effective16<E> extends HashSet<E> {
    private int addCount = 0;
    
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    
    @Override
    public boolean addAll(Collection<extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    
    public int getCount() {
        return addCount;
    }
}
cs



다음과 같이 HashSet<E>을 상속받아 기존의 add 관련 함수를 재정의 한다음 다음과 같이 호출하였을 때, 출력되는 count값은 어떻게 될까?


1
2
3
4
5
6
7
public static void main(String args[]) {
    Effective16<String> set = new Effective16<>();
        
    set.addAll(Arrays.asList("babo""cjung"));
        
    System.out.println(set.getCount());
}
cs



위의 로직을 보면 당연히 count가 2가 나올 것 같지만 4가 나온다. 

그 이유는 HashSet에서 제공하는 addAll은 내부적으로 all을 사용하기 때문에 재정의한 all에 접근하여 카운트가 더 증가되는 것이다.

2. 다른 이름을 가진 새로운 메서드를 사용한다.
-> 1번의 방법보다 조금더 안전하긴 하지만 만약 상위 HashSet 클래스에 동일한 메서드나 반환형만 다른 메서드 타입이 생성될 경우 문제를 야기 할 수 있기 때문에 문제가 될 수 있다.


해결 방법
기존 클래스를 상속하는 대신 새로운 클래스에 기존 클래스 객체를 참조하는 private필드를 하나 두는 composition이라는 기법을 사용한다. (Has a 기법)

이렇게 가지고 있는 필드에 접근하여 필요한 메서드만 호출하여 사용하는 방법이다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ForwardingSet<E> implements Set<E> {
 
    private final Set<E> s;
    public ForwardingSet(Set<E> s) {
        this.s = s;
    }
    
    @Override
    public boolean add(E arg0) {
        return s.add(arg0);
    }
 
    @Override
    public boolean addAll(Collection<extends E> arg0) {
        return s.addAll(arg0);
    }
}
 
public class Effective16<E> extends ForwardingSet<E> {
    
    public Effective16(Set<E> s) {
        super(s);
    }
 
    private int addCount = 0;
    
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    
    @Override
    public boolean addAll(Collection<extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    
    public int getCount() {
        return addCount;
    }
}
cs




정리하면
상속은 캡슐화의 원칙을 침해 하므로 문제를 발생시킬 소지가 있다는 것을 잊지말아야한다. 그러므로 상속은 하위 클래스가 상위 클래스의 하위 자료형이 확실하고, IS-A관계가 성립되는 경우에만 사용한다.

그렇기에 해당 기능을 사용해야 하는 경우에는 has a 관계를 사용하면 안되는지 다시 한번 확인 해 보는 것이 좋다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙16 인용.



댓글()

클래스와 인터페이스 - 규칙 15 변경 가능성을 최소화하라

JAVA/Effective Java|2018. 5. 29. 22:31

immutable 클래스는 그 객체를 수정할 수 없는 클래스이다. 

객체 내부의 정보는 객체가 생성될 때 주어진 것이며, 객체가 살아 있는 동안 그대로 보존된다.

이러한 변경 불가능 클래스를 만드는 이유는 다양하다. 
- 설계가 쉽다.
- 오류 가능성이 적고 안전하다.

생성 규칙
- 객체 상태를 변경하는 메서드를 제공하지 않는다. (setter)
- 상속할 수 없도록 진행한다. 
  => 하위 클래스가 객체 상태가 변경된 것처럼 동작할 수 있다.
  => class에 final을 붙힌다. 
- 모든 필드를 final로 생성한다.
  => 이렇게 생성할 경우 생성된 객체에 대한 참조가 동기화(Synchronization)없이 다른 스레드로 전달
   되어도 안전하다.
- 모든 필드를 private로 선언한다. 
  => 모든 필드를 직접 접근해서 수정하는 경우를 막을 수 있다.
  => public으로 선언하고 final로 지정할 수 있지만 이럴 경우 나중에 클래스의 내부 표현을 변경할 수 
   없게 되기 때문에 좋지 않다.
- 다음과 같은 방식으로 변경 가능 컴포넌트에 대한 독점적 접근법을 보장한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Test {
  private final String alias;
  private final String name;
 
  public Test(String alias, String name) {
    this.alias = alias;
    this.name = name;
  } 
 
  
  //  방어적 복사본을 만들어 기존의 값을 변경하는 것이 아닌 변경된 새로운 값을 만든다.
  public Test addPrifix(String prefix, Test te) {
    return new Test(prefix + te.getAlias(), prefix + te.getName());
  }
 
}
cs



또한 이렇게 방어적 복사본을 계속해서 만들지 않고도 공통으로 사용하는 항목에 대해서 다음과 같이
생성해 두어 공유할 수 있다.


1
2
public static final Test PNP = new Test("pnp" , "inc");
public static final Korea KOR = new Test("kor""public");
cs



이렇게 만들어 놓으면 계속해서 객체를 생성할 이유가 없어서 좋다. 


또한 이런 클래스는 어차피 변경이 되지 않기때문에
clone 메서드나 복사 생성자를 만들 필요도 없고 만들어서도 안된다.

이러한 별경 불가능한 객체의 단점은 위에서 보는거와 같이 새로운 값이 필요할 때마다 객체를 만들어야 한다는 것이다.

그렇지만 이런 문제는 자주 사용하는 연산을 다른 동료 클래스들에서 제공하는 기본연산등을 이용하여 어느정도 완충할 수 있다.

정리하자면.
변경 가능한 클래스로 만들어야할 이유가 없는 클래스의 경우 모드 변경불가능한 클래스로 만들고
만약 그럴 수 엇다면 변경 가능성을 제한하고 특별한 이유가 없는 경우는 모든 필드를 final로 선언한다.

 출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙15 인용.


댓글()