[클린코드] 3장 함수

JUN·2024년 7월 15일
0

클린코드

목록 보기
3/14

💡 클린 코드 3장 함수를 읽고 정리한 페이지입니다.

어떤 프로그램이든 가장 기본단위가 함수이기에 함수를 잘 만드는 법이 중요하다.

1. 함수를 최대한 작게 만들어라.

모니터와 ide의 발달로 한 화면에 세로 100줄도 들어가지만 우리는 함수를 더 작게, 짧게 유지해야만 한다.

블록과 들여쓰기

if-else 문, while 문에 들어가는 블록은 한줄이여야 한다.

즉, 중첩구조는 함수가 커지는 지름길이다. 함수의 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다.

2. 함수는 한가지 일을 해야한다.

2-1. 의미 있는 표현으로 다른 함수를 추출할 수 있다면 추출할것.

제곧네. 단순히 다른 표현이 아니라, 의미 있는 이름으로 함수 안에 다른 함수를 추출할 수 있다면 추출하여 함수를 한가지 일을 하도록 만들것.

2-2. 함수 당 추상화 수준은 하나로.

함수가 한가지 작업만 수행하려면 함수 내 모든 문장의 추상화 수준이 동일해야함.

추상화 수준이 같다는게 뭐지?

  • "차를 운전한다"라는 함수의 문장들 ("열쇠를 찾는다", "시동을 건다", "기어를 바꾼다", "방향을 조절한다")은 같은 추상화 수준에서 작동한다. 이들은 모두 "차를 운전한다"라는 한 가지 일을 수행하기 위한 구체적인 단계들이다.
  • "열쇠를 찾는다"라는 함수의 문장들 ("가방을 뒤진다", "키 홀더를 확인한다")은 더 낮은 수준의 추상화를 나타낸다. 이들은 "열쇠를 찾는다"라는 한 가지 작업을 수행하기 위한 구체적인 단계들이다.
  • 함수의 추상화 수준을 판단하는 것은 그 함수 내의 문장들이 어떤 수준의 작업을 나타내는지 확인하는 것이다.
  • 함수 내의 모든 문장이 같은 수준의 작업을 나타낸다면, 그 함수는 한 가지 일을 하는 것으로 본다.

2-3. 내려가기 규칙 - 추상화 수준으로 내림차순 정렬하기

위에서 아래로 프로그램을 읽을 때 추상화 수준이 한번에 한단계씩 낮아지도록 할 것.

Switch문

  • Switch 문은 태생적으로 작게, 한가지 작업만 하게 만들기 힘듦.
  • 다형성을 이용해서 Switch문을 한번만 사용하거나 사용하지 않을 수 있음.
  • 동물의 종류에 따라 다른 행동을 하는 경우의 예시.
    class Animal {
        String type;
    
        void act() {
            switch (type) {
                case "Dog":
                    // Dog의 행동을 정의
                    break;
                case "Cat":
                    // Cat의 행동을 정의
                    break;
                // ... 추가적인 동물 종류
                default:
                    // 기본 행동
                    break;
            }
        }
    }
    
    이런 방식으로 코드를 작성하면, 새로운 동물 종류가 추가될 때마다 switch 문을 수정해야하고, 동일한 행동 코드가 여러 곳에서 반복될 수 있음.

다형성을 이용한 switch 문 숨기기

다형성을 이용하면 switch 문을 저차원 클래스에 숨길 수 있음.
이 방법은 코드 반복을 방지하고, switch 문이 한 가지 작업만 하게 만들 수 있음.

예를 들면, 동물의 종류에 따라 다른 행동을 하는 경우를 생각해보자.
동물 클래스를 만들고, 각 동물 종류를 나타내는 서브클래스를 만들 수 있다. 각 서브클래스는 동물 클래스의 '행동' 메소드를 오버라이드하여 해당 종류의 동물이 특정 행동을 어떻게 하는지 정의할 수 있다.

```java
abstract class Animal {
    abstract void act();
}

class Dog extends Animal {
    void act() {
        // Dog의 행동을 정의
    }
}

class Cat extends Animal {
    void act() {
        // Cat의 행동을 정의
    }
}

```

이렇게 하면, Animal 타입의 객체를 사용할 때는 각 동물의 특정 행동을 알 필요 없이 'act' 메소드를 호출하면 된다.

```java
Animal myPet = new Dog();
myPet.act(); // Dog의 행동을 실행

```

이런 방식으로 switch 문을 다형성을 이용해 숨기면, 각 동물 종류에 따른 행동 코드를 한 곳에서 관리할 수 있고, 코드의 반복을 줄일 수 있다.
  • 추상팩토리 패턴을 사용해서 switch문 숨기기

    💡 해당 부분은 필자의 지식 부족으로 gpt의 도움을 받았습니다 🥲

    1. 제품 인터페이스 정의

    먼저 제품 인터페이스를 정의합니다.
    java코드 복사
    public interface Animal {
        void act();
    }
    

    2. 구체적인 제품 클래스들 정의

    구체적인 제품 클래스들을 정의합니다.
    java코드 복사
    public class Dog implements Animal {
        @Override
        public void act() {
            System.out.println("Dog is acting.");
        }
    }
    
    public class Cat implements Animal {
        @Override
        public void act() {
            System.out.println("Cat is acting.");
        }
    }
    

    3. 추상 팩토리 인터페이스 정의

    추상 팩토리 인터페이스를 정의합니다.
    java코드 복사
    public interface AnimalFactory {
        Animal createAnimal();
    }
    

    4. 구체적인 팩토리 클래스 정의

    구체적인 팩토리 클래스들을 정의합니다.
    java코드 복사
    public class DogFactory implements AnimalFactory {
        @Override
        public Animal createAnimal() {
            return new Dog();
        }
    }
    
    public class CatFactory implements AnimalFactory {
        @Override
        public Animal createAnimal() {
            return new Cat();
        }
    }
    

    5. 클라이언트 코드

    클라이언트 코드는 추상 팩토리 인터페이스를 통해 동물을 생성합니다.
    java코드 복사
    public class Client {
        public static void main(String[] args) {
            AnimalFactory dogFactory = new DogFactory();
            Animal dog = dogFactory.createAnimal();
            dog.act(); // Output: Dog is acting.
    
            AnimalFactory catFactory = new CatFactory();
            Animal cat = catFactory.createAnimal();
            cat.act(); // Output: Cat is acting.
        }
    }
    

    6. 팩토리 제공 메서드 정의

    동물 종류에 따라 팩토리를 제공하는 메서드를 정의하여 클라이언트 코드에서 팩토리를 선택할 수 있도록 합니다.
    java코드 복사
    public class FactoryProvider {
        public static AnimalFactory getFactory(String type) {
            switch (type) {
                case "Dog":
                    return new DogFactory();
                case "Cat":
                    return new CatFactory();
                default:
                    throw new IllegalArgumentException("Unknown animal type: " + type);
            }
        }
    }
    

    7. 팩토리 제공 메서드를 사용하는 클라이언트 코드

    클라이언트 코드는 팩토리 제공 메서드를 통해 팩토리를 선택합니다.
    java코드 복사
    public class Client {
        public static void main(String[] args) {
            AnimalFactory factory = FactoryProvider.getFactory("Dog");
            Animal animal = factory.createAnimal();
            animal.act(); // Output: Dog is acting.
    
            factory = FactoryProvider.getFactory("Cat");
            animal = factory.createAnimal();
            animal.act(); // Output: Cat is acting.
        }
    }
    
    이 예제에서는 FactoryProvider 클래스의 getFactory 메서드가 switch 문을 사용하여 적절한 팩토리를 반환합니다. 하지만 클라이언트 코드에서는 switch 문을 보지 않고, 추상 팩토리 인터페이스를 통해 다형성을 활용하여 동물을 생성하고 행동을 수행합니다. 이를 통해 switch 문을 효과적으로 숨기고, 추상 팩토리 패턴과 다형성을 결합하여 코드의 유연성과 유지보수성을 높일 수 있습니다. switch 문 숨기기

3. 함수의 인수

  • 함수에서 가장 이상적인 인수의 개수는 0개! 다음 이상적인 것은 1개, 다음은 2개이고 3개 이상은 가능한 피하는 것이 좋다. 4개 이상은 안된다.
  • 테스트 관점에서 봐도 여러개의 인수를 유효한 값으로 조합을 구성해야하기에 테스트하기 부담스러워진다.
  • 출력 인수는 입력 인수보다 이해하기 어려우므로 지양할 것.

    출력인수란?

    출력 인수는 함수전달받은 값바꿔서 그 바뀐 값을 함수 밖에서도 사용할 수 있게 하는 것

    public void 추가하기(List<String> list, String data) {
    list.add(data); // 전달받은 list에 data를 추가합니다.
    }
    
    ...
    추가하기(myList, "안녕");

    여기서 list 가 출력 인수이다.

    → 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택할 것

    ex) appendFooter(StringBuffer report)report.appendFooter()

  • 인스턴스 변수를 사용하여 함수의 인수를 관리할것.

3-1. 단항 인수

  1. 인수에 질문을 던지는 경우

    ex) boolean fileExists(”Myfile”);

  2. 인수로 뭔가를 변환해 결과를 반환하는 경우.

    ex) InputStream fileOpen(”MyFile”) : String의 파일 이름을 InputStream으로 변환하는 함수

→ 단, 함수의 이름을 지을 때는 두 경우를 분명히 구문하게 하여 짓는다.

  1. 이벤트 함수

    • 입력 인수만 있음. 출력 인수는 없음, 입력 인수로 시스템을 바꿈

    → 단, 이벤트라는 사실이 함수 이름에 명확히 드러나야 함.

    ex) passwordAttempFailedNtimes(int attempts)

  • 이름 형식 : 동사(명사) 형식으로 쌍을 이룰 것. ex) write(name)

3-2. 이항 함수

  • 가능하면 단항 함수로 바꿀 것.

단 가능한 경우가 몇가지 있음

  • 한 값을 표현하는 두 값을 받을 경우 ex) Point p = new Point(0,0) : 직교 좌표계의 점은 한값을 표현하는 두 요소가 필요.
  • assertEquals(expected, actual) : 해당 함수는 두 인수의 순서가 인위적이기 때문에 assertExpectedEqualsActual(expected, actual) 로 인수의 순서를 알리기.

3-3. 삼항 함수

  • 마찬가지, 가능하면 단항함수로 바꿀 것.
  • 용도 파악이 가능하도록 연관성 있는 세개의 매개변수를 사용할 것.

3-4. 인수 객체화

  • 인수가 여러개 필요할 경우 클래스 변수로 선언을 고려할 것.
    • Point p = new Point(0,0) 의 경우 3차원일 경우 3개 4차원일 경우 4개 이상의 인수가 필요해진다. 그럴 경우에는 vector 클래스를 따로 생성해서 관리하는 것이 나을 수 있음.

4. 부수효과를 방지할 것.

  • 함수로 넘어온 인수를 수정하거나(출력인수) 시스템 전역 변수, 클래스 변수 등을 수정하면 안된다.

이런 짓은 시간적인 결합, 순서 종속성을 초래함.

5. 명령과 조회를 분리할 것.

뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 할 것.

6. 오류코드보다 예외를 사용할 것.


if (deletePage(page) = E_OK) {
    if (registry.deleteReference(page.name) = E_OK)
        if (configkeys.deleteKey(page.name.makeKey()) logger.log("page deleted");
        }
    else {
        logger.log("configkey not deleted");
    } {
        = =
    } else {
        logger.log("deleteReference from registry failed");
    }
} else {
    logger.log("delete failed");
    return E_ERROR;
}

try-catch 문으로 오류 처리를 원래 코드에서 분리하면 코드를 깔끔하게 할 수 있음.

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
    logger.log(e.getMessage());
}

또한 try catch 구문을 별도 함수로 뽑아낸다면 더욱 좋음 → 오류처리도 한가지 일이기에 오류 처리를 별도로 처리하는 함수를 오류만 처리하게 나눠야함.

Error.java 와 같이 에러코드를 정의하는 enum 등을 사용하는것은 지양.

  • 다른 클래스에서 import해서 사용해야 하므로 enum이 변하면 다른 클래스 모두 다시 배치하고 컴파일해야함.
  • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되기에 새 예외 추가가 더욱 쉬워짐.
profile
순간은 기록하고 반복은 단순화하자 🚀

0개의 댓글