자바/자바 코딩의 기술

자바 코딩의 기술 - 7장 ( 객체 디자인 )

끄적끄적 2022. 4. 9. 15:05

컴퓨터 과학의 모든 문제는 또 다른 간접 게층을 추가해 풀 수 있다. 하지만 대부분 또 다른 문제를 양산한다. 
- 데이비드 휠러

 

7-1. boolean 매개변수로 메서드 분할

void log(String message, boolean classified) throws IOException {
    if (classified) {
        writeMessage(message, CAPTAIN_LOG);
    } else {
        writeMessage(message, CREW_LOG);
    }
}

위는 기장(Captain)과 승무원(Crew)로그 처리 부분과 같이 서로 연관 없는 부분을 메서드를 각각 분리하는게 좋다.

void writeToCaptainLog(String message) throws IOException {
    writeMessage(message, CAPTAIN_LOG);
}

void writeToCrewLog(String message) throws IOException {
    writeMessage(message, CREW_LOG);
}

7-2. 옵션 매개변수로 메서드 분할

class Logbook {

    static final Path CREW_LOG = Paths.get("/var/log/crew.log");

    List<String> readEntries(LocalDate date) throws IOException {
        final List<String> entries = Files.readAllLines(CREW_LOG,
                StandardCharsets.UTF_8);
        if (date == null) {
            return entries;
        }

        List<String> result = new LinkedList<>();
        for (String entry : entries) {
            if (entry.startsWith(date.toString())) {
                result.add(entry);
            }
        }
        return result;
    }
}

파라미터 date가 null 일 경우, 전체 항목을 리턴하는 기능이 포함되어 있다. 이럴 거면 별도 메서드(readAllEntries)로 분리하는게 낫다.

class Logbook {

    static final Path CREW_LOG = Paths.get("/var/log/crew.log");

    List<String> readEntries(LocalDate date) throws IOException {
        Objects.requireNonNull(date);
        
        List<String> result = new LinkedList<>();
        for (String entry : readAllEntries()) {
            if (entry.startsWith(date.toString())) {
                result.add(entry);
            }
        }
        return result;
    }

    List<String> readAllEntries() throws IOException {
        return Files.readAllLines(CREW_LOG, StandardCharsets.UTF_8);
    }
}

7-3. 구체 타입보다 추상 타입

class Inventory {
    LinkedList<Supply> supplies = new LinkedList();

    void stockUp(ArrayList<Supply> delivery) {
        supplies.addAll(delivery);
    }

    LinkedList<Supply> getContaminatedSupplies() {
        LinkedList<Supply> contaminatedSupplies = new LinkedList<>();
        for (Supply supply : supplies) {
            if (supply.isContaminated()) {
                contaminatedSupplies.add(supply);
            }
        }
        return contaminatedSupplies;
    }
}

위 소스는 ArrayList 를 다시 LinkedList로 넣는데, 다른 Collection까지 지원하려면 추상적인 Collection을 쓰는게 확장성 면에서 유리하다.  getContaminatedSupplies 메서드에서도 LinkList 반환대신 List 로 반환한다.

class Inventory {
    List<Supply> supplies = new LinkedList();

    void stockUp(Collection<Supply> delivery) {
        supplies.addAll(delivery);
    }

    List<Supply> getContaminatedSupplies() {
        List<Supply> contaminatedSupplies = new LinkedList<>();
        for (Supply supply : supplies) {
            if (supply.isContaminated()) {
                contaminatedSupplies.add(supply);
            }
        }
        return contaminatedSupplies;
    }
}

7-4. 가변 상태보다 불변 상태 사용하기

class Distance {
    DistanceUnit unit;
    double value;

    Distance(DistanceUnit unit, double value) {
        this.unit = unit;
        this.value = value;
    }

    static Distance km(double value) {
        return new Distance(DistanceUnit.KILOMETERS, value);
    }

    void add(Distance distance) {
        distance.convertTo(unit);
        value += distance.value;
    }

    void convertTo(DistanceUnit otherUnit) {
        double conversionRate = unit.getConversionRate(otherUnit);
        unit = otherUnit;
        value = conversionRate * value;
    }
}


class Main {
    static void usage() {
        Distance toMars = new Distance(DistanceUnit.KILOMETERS, 56_000_000);
        Distance marsToVenus = new Distance(DistanceUnit.LIGHTYEARS, 0.000012656528);
        Distance toVenusViaMars = toMars;
        toVenusViaMars.add(marsToVenus);
    }
}

변수가 final이 아니다보니, toVenusViaMars = toMars; 를 해도 오류가 발생하지 않는다. final 키워드를 이용해서 잘못 사용할 경우를 차단하라.

final class Distance {
    final DistanceUnit unit;
    final double value;


    Distance(DistanceUnit unit, double value) {
        this.unit = unit;
        this.value = value;
    }

    Distance add(Distance distance) {
        return new Distance(unit, value + distance.convertTo(unit).value);
    }

    Distance convertTo(DistanceUnit otherUnit) {
        double conversionRate = unit.getConversionRate(otherUnit);
        return new Distance(otherUnit, conversionRate * value);
    }
}

class Main {
    static void usage() {
        Distance toMars = new Distance(DistanceUnit.KILOMETERS, 56_000_000);
        Distance marsToVenus = new Distance(DistanceUnit.LIGHTYEARS, 0.000012656528);
        Distance toVenusViaMars = toMars.add(marsToVenus)
                                        .convertTo(DistanceUnit.MILES);
    }
}

7-5. 상태와 동작 결합하기

class Hull {
    int holes;
}


class HullRepairUnit {

    void repairHole(Hull hull) {
        if (isIntact(hull)) {
            return;
        }
        hull.holes--;
    }

    boolean isIntact(Hull hull) {
        return hull.holes == 0;
    }
}

상태를 나타내는 Hull 클래스와 동작을 하는 HullRepairUnit이 분리되어 Hull은 불필요하게 Getter, Setter를 제공해야 하고, 다른 객체가 잘못 수정하는 방법을 제공한다.  아래는 하나의 클래스안에 상태와 동작이 결합되어 외부에 불필요한 정보를 제공하지 않는다.

class Hull {
    int holes;

    void repairHole() {
        if (isIntact()) {
            return;
        }
        holes--;
    }

    boolean isIntact() {
        return holes == 0;
    }
}

7-6. 참조 누수 피하기

class Inventory {

    private final List<Supply> supplies;

    Inventory(List<Supply> supplies) {
        this.supplies = supplies;
    }

    List<Supply> getSupplies() {
        return supplies;
    }
}

class Usage {

    static void main(String[] args) {
        List<Supply> externalSupplies = new ArrayList<>();
        Inventory inventory = new Inventory(externalSupplies);

        inventory.getSupplies().size(); // == 0
        externalSupplies.add(new Supply("Apple"));
        inventory.getSupplies().size(); // == 1

        inventory.getSupplies().add(new Supply("Banana"));
        inventory.getSupplies().size(); // == 2
    }
}

위 소스는 외부의 객체 externalSupplies 가 변경되는데, Inventory내부의 supplies도 변경된다. 외부에서 생성한 ArrayList를 그대로 사용하기 때문이다. 아래와 같이 객체 내부에 저장할때는 별도로 내부 ArrayList를 만들어서 넣으면 내부구조가 보호된다.

class Inventory {

    private final List<Supply> supplies;

    Inventory(List<Supply> supplies) {
        this.supplies = new ArrayList<>(supplies);
    }

    List<Supply> getSupplies() {
        return Collections.unmodifiableList(supplies);
    }
}

 아래처럼 외부의 변화가 내부에 영향을 주지 않으며, unmodifiableList로 get한 후에 변경을 못하게 막을 수 있다.

class Usage {

    static void main(String[] args) {
        List<Supply> externalSupplies = new ArrayList<>();
        Inventory inventory = new Inventory(externalSupplies);

        inventory.getSupplies().size(); // == 0
        externalSupplies.add(new Supply("Apple"));
        inventory.getSupplies().size(); // == 0

        // UnsupportedOperationException
        inventory.getSupplies().add(new Supply("Banana"));
    }
}

7-7. 널 반환하지 않기

static SpaceNation getByCode(String code) {
    for (SpaceNation nation : nations) {
        if (nation.getCode().equals(code)) {
            return nation;
        }
    }
    return null;
}

위 처럼 메서드에서 적절한 값이 없을 경우 null을 리턴하게 되면 아래처럼 NullPointException이 발생한다.

static void main(String[] args) {
    String us = SpaceNations.getByCode("US").getName();
    // -> "United States"
    String anguilla = SpaceNations.getByCode("AI").getName();
    // -> NullPointerException
}

아래와 같이 빈 객체를 리턴하거나, 명시적으로 IllegalArgumentException이나 NoSuchElementException같은 예외를 던져라.

static final SpaceNation UNKNOWN_NATION = new SpaceNation("", "");

static SpaceNation getByCode(String code) {
    for (SpaceNation nation : nations) {
        if (nation.getCode().equals(code)) {
            return nation;
        }
    }
    return UNKNOWN_NATION;
}

 

 

반응형