public class NutritionFacts {
private final int calories;
private final int fat;
private final int sodium;
public NutritionFacts(int calories) {
this(calories, 0, 0);
}
public NutritionFacts(int calories, int fat) {
this(calories, fat, 0);
}
public NutritionFacts(int calories, int fat, int sodium) {
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
}
}
문제 : 확장하기 어렵다.public class NutritionFacts {
private int calories = 0;
private int fat = 0;
private int sodium = 0;
public NutritionFacts() { } //매개변수 없는 생성자로 객체를 만든 뒤,
//setter 메소드로 값을 설정한다.
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
}
객체 하나를 만드려면 메서드를 여러개 호출해야 하고 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. 일관성이 무너지면 클래스를 불변으로 만들 수 없는 큰 단점이 존재한다public class NutritionFacts {
private final int calories;
private final int fat;
private final int sodium;
public static class Builder {
//필수 매개변수
private final int calories;
//선택 매개변수
private int fat = 0;
private int sodium = 0;
public Builder(int calories) {
this.calories = calories;
}
//setter 메소드
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
}
}
NutritionFacts cocaCola = new NutritionFacts(250).fat(30).sodium(20).build();
유연하다는 장점이 있지만 단점은 객체를 만들기 전에 빌더 부터 만들어야 한다. 생성 비용이 크진 않지만 성능에 민감한 상황에서는 문제가 될 수 있다. 하지만 계층적으로 설계된 클래스와 함께 쓰기에 빌더가 좋다public abstract class Pizza{
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>>{
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping){
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
protected abstract T self();
}
Pizza(Builder<?> builder){
toppings = builder.toppings.clone();
}
}
Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 여기에 추상 메서드인 self를 더해 하위 클래스에서 형변환하지 않고도 메서드 연쇄를 지원할 수 있다.public class NyPizza extends Pizza{
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder>{
private final Size size;
public Builder(Size size){
this.size = Objects.requireNonNull(size);
}
@Override public NyPizza build(){
return new NyPizza(this);
}
@Override protected Builder self(){
return this;
}
}
private NyPizza(Builder builder){
super(builder);
size = builder.size();
}
}
Private 생성자와 public static 필드
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis { ... }
public void leaveTheBuilding() { ... }
}
해당 클래스가 싱글턴임이 API에 명백히 드러난다. public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다.
문제 :
정적 팩터리 메서드를 public static 멤버로 제공되는 방식
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis { ... }
public static Elvis getInstance() {return INSTANCE;}
public void leaveTheBuilding() { ... }
}
장점:
하지만 여기서 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어지지 않으려면 이 코드를 추가해야한다
// 싱글턴임을 보장해주는 readResolve 메서드
private Object readResolve() {
// '진짜' Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡다.
return INSTANCE;
}
원소가 하나인 열거 타입을 선언한다
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
장점:
문제점:
·정적 메서드와 정적 필드만 담은 클래스는 객체 지향적으로 사고하지 않는 사람들이 종종 남용하는 방식이지만, 나름의 쓰임새가 있다.
java.lang.Math, java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다.
java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수 있다.
final 클래스와 관련된 메서드를 모아 놓을 때 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가기 때문이다.
// 인스턴스를 만들 수 없는 유틸리티 클래스
public class UtilityClass {
// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
// 악의적 리플렉션을 막을 수 있다.
private UtilityClass() {
throw new AssertionError();
}
}
많은 클래스가 하나 이상의 자원에 의존한다. 사용하는 자원에 따라 동작이 달라지는 클래스에 경우 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.
static 유틸리티를 잘못 사용한 예시- 유연하지 않고 테스트하기 어렵다.
public class SpellChecker {
private static final Lexicon dictionary = ...; // 의존하는 리소스 (의존성)
private SpellChecker() {} // 객체 생성 방지
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
인스턴스를 만들 필요가 없기 때문에 private한 생성자를 만들어주고, public static 메소드들이 있다.
싱글턴을 잘못 사용한 예- 유연하지 않고 테스트하기 어렵다.
public class SpellChecker {
private final Lexicon dictionary;
public boolean isValid(String word) { return true; }
public List<String> suggestions(String typo) { return null; }
}
제대로 사용하려면 의존성을 바깥으로 분리하여 외부로부터 주입받도록 해야 한다. 쉽게 말하면 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.
public class SpellChecker {
private final Lexicon dictionary;
private SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { return true; }
public List<String> suggestions(String typo) { return null; }
불변을 보장하여 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있다
생성자에 자원 팩터리를 넘겨주는 방
예시 Supplier<T>
인터페이스
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get(); // T 타입 객체를 찍어낸다
}
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
Supplier<T>
를 입력으로 받는 메서드는 한정적 와일드카드 타입을 사용해 팩터리의 타입 매개변수를 제한
public class SpellChecker {
private final Lexicon dictionary;
private SpellChecker(Supplier<Lexicon> dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { return true; }
public List<String> suggestions(String typo) { return null; }
public static void main(String[] args) {
Lexicon lexicon = new KoreanDictionary();
SpellChecker spellChecker = new SpellChecker(() -> lexicon);
spellChecker.isValid("hello");
}
}
interface Lexicon{}
class KoreanDictionary implements Lexicon {}
class TestDictionary implements Lexicon {} // test가 가능해짐
결론 : 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.
똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되다. 특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다