17장. 냄새와 휴리스틱

Overview

  • 휴리스틱

    • Heutiskein → '찾아내다', '발견하다’

🌱 발견법 - 간편 추론, 경험을 기반으로 **그때그때 상황과 직관에 따라 행동**하여 결론을 도출하는 것

  • 휴리스틱 알고리즘

    • 최적 해를 보장하지 않는다.

    • Greedy

    • Travelling Salesman Problem


저자의 휴리스틱

주석

🌱 주석은 코드만으로 다하지 못하는 설명을 부언한다.

  1. 부적절한 정보

    • 코드만으로 충분한데 구구절절 설명하는 주석은 없어야 한다.

    i++ // i 증가
    • function signature만 달랑 기술하는 Javadoc은 사용하지 않아야한다.

    /** 
     * @param sellRequest
     * @return
     * @throws ManagedComponentException
     */
    public SellResponse beginSellItem(SellRequest sellRequest)
    throws ManagedComponentException
  2. 주석 처리된 코드

    • 주석 처리된 코드를 발견하면 즉각 지워버려야 한다.

함수

  1. 너무 많은 parameter

    • parameter 개수는 작을수록 좋다. 아예 없는게 가장 좋다!

    • 넷 이상은 최대한 피해야한다.

    fun testEffect(key1: Any?, key2: Any?, key3: Any?, key4: Any?) { }
    
    fun testEffect(
        key1: Any?, 
        key2: Any?, 
        key3: Any?, 
        key4: Any?
    ) { }
  2. flag parameter ✨

    • boolean, int, enum 등 플래그 파라미터는 함수가 내부적으로 여러 기능을 수행한다는 뜻

    • 플래그를 넘겨 제어하는 대신, 새로운 함수를 만들자.

  3. 죽은 함수

    • 아무도 호출하지 않는 함수는 삭제해야 한다.


일반

  1. 한 소스 파일에 여러 언어를 사용하는 것

    • 이상적으로는 소스 파일 하나 당, 하나의 언어만 사용해야 한다.

    • 불가피한 경우에 최대한 언어 범위를 줄여야 한다.

  2. 당연한 기능을 구현해라

    • 함수나 클래스는 프로그래머가 당연히 받아들일 수 있는 기능을 제공해야 한다.

    Day day = DayDate.StringToDay(String dayName);
    
    // Monday -> Day.MONDAY
  3. 중복✨

    • DRY (Don’t Repeat Yourself)

    • 코드에서 중복을 발견하면, 추상화의 기회이다!

    • 중복 코드 → 하위 루틴이나 클래스로 분리 가능

  4. 올바른 추상화 수준을 설정하기 ✨

    • high level - 집의 형태, 외관, 공간, 방의 배치 (정책)

    • low level - 콘센트, 전등의 위치, 지붕의 크기 등의 기초공사 수준 (세부사항)

    • 의존성 역전 법칙 (DIP) → 집의 형태가 전등의 위치나 색깔에 의해 바뀌어선 안된다. low level이 high level에 의존

    • 기초 클래스 (interface, abstract) 에서 파생 클래스에 의존해서는 안된다.

  5. 기능 욕심

    • 클래스 메서드는 자기 클래스의 상태에 관심을 가져야지, 다른 객체를 조작해서는 안된다.

    public class HourlyPayCalculator {
        public Money calculateWeeklyPay(**HourlyEmployee e**) {
            int tenthRate = e.getTenthRate().getPennies();
            int tenthsWorked = e.getTenthsWorked();
            int straightTime = Math.min(400, tenthWorked);
            int overTime = Math.max(0, tenthsWorked - straightTime);
            int straightPay = straightTime * tenthRate;
            int overtimePay = (int)Math.round(overTime * tenthRate * 1.5);
            return new Money(straightPay + overtimePay);
        }
    }
  6. 부적절한 static 함수

    • Math.max(double a, double b) 는 좋은 static 함수. 모든 정보를 parameter를 통해 가져온다.

    • 재정의 할 가능성이 있는 함수를 static으로 만들어선 안된다. ✨

    • 시급을 계산하는 방법은 언제든 바뀔 수 있다.

    HourlyPayCalculator.calculatePay(employee, overtimeRate);
  7. 서술적 변수 (Descriptive Variable)

    • 서술적 변수 이름을 사용해서 가독성을 높이자.

    Matcher match = headerPattern.matcher(line);
    if(match.find())
    {
      String **key** = match.group(1);
      String **value** = match.group(2);
      headers.put(key.toLowerCase(), value);
    }
    • 변수 이름을 지어보자

      • The total cost of a purchase (totalCost)

      • The total cost of a purchase, including tax (totalCostWithTax)

      • The number of columns in a grid (colNum)

      • The number of rows in a grid (rowNum)

      • The width of a rectangle (rectWidth)

    • 플래그 변수

      • isRaining , isTooHot , hasPassedExam → is , has 또는 contains 와 같은 동사로 시작

      • 변수 이름을 positive 하게 → isNotFull 대신 isEmpty (부정 조건을 피하자)

  8. 명명된 상수로 교체해라! ✨

    • 코드에서 숫자를 사용하지 마라 (숫자는 명명된 상수 뒤로 숨기자)

    • ex) 책의 한 페이지당, 55줄을 인쇄한다 → LINES_PER_PAGE

    double circleArea = radius * radius * **Math.PI**
  9. 조건을 캡슐화 하라

if (timer.hasExpired() && !timer.isRecurrent())

보다는

if (shouldBeDeleted(timer))

가 더 좋다.
  1. 함수는 한 가지 일만 한다.

public void pay(){
	for (Employee e : employees) { // 직원 목록을 돌며
    	if (e.isPaypay()) { // 월급일이라면
        	Money pay = e.calculatePay(); // 급여를 계산해서
          e.deliverPay(pay); // 급여 지급
        }
    }
  • 세개로 나눌 수 있다.

public void pay(){
	for (Employee e : employees) 
    	payIfNecessary(e);
}

private void payIfNecessary(Employee e) {
	if(e.isPayday()){
    	calculateAndDeliverPay(e)
    }
}

private void calculateAndDeliverPay(Employee e) {
	Money pay = e.calculatePay();
    e.deliverPay(pay);
}
  1. 시간적인 결합을 드러내라

    • 함수 파라미터를 통해 함수가 호출되는 순서를 드러낼 수 있다.

    public class MoogDiver {
        Gradient gradient;
        List<Spline> splines;
    
        public void dive(String reason) {
          saturateGradient(); ...1
          reticulateSplines(); ...2
          diveForMoog(reason); ...3
        }
        ...
      }
    
    // 프로그래머가 reticulateSplines 를 먼저 호출한다면?
    • 파라미터를 사용!

    public class MoogDiver {
        Gradient gradient;
        List<Spline> splines;
    
        public void dive(String reason) {
          Gradient gradient = saturateGradient();
          List<Spline> splines = reticulateSplines(**gradient**);
          diveForMoog(**splines, reason**);
        }
        ...
      }
  2. 경계 조건은 캡슐화 하라 ✨

    • 경계 조건은 한 곳에서 설정해두고 사용한다.

    if (**level + 1** < tags.length) {
        parts = new Parse(body, tags, **level + 1**, offset + endTag;
        body = null;
    }
    • 변수로 캡슐화

    int nextLevel = level + 1;
    if (nextLevel < tags.length)
    {
        parts = new Parse(body, tags, nextLevel, offset + endTag;
        body = null;
    }

Java

  1. 긴 import 목록을 피해라

    • 패키지에서 클래스 둘 이상을 사용한다면 와일드 카드를 사용한다.

    import package.*;
  2. 상수는 상속하지 않는다.

    • TENTHS_PER_WEEK, OVERTIME_RATE 상수의 출처는?

    public class HourlyEmployee extends Employee {
        private int tenthsWorked;
        private double hourlyRate;
    
        public Money calculatePay() {
          int straightTime = Math.min(tenthsWorked, **TENTHS_PER_WEEK**);
          int overTime = tenthsWorked - straightTime;
          return new Money(
            hourlyRate * (tenthsWorked + **OVERTIME_RATE** * overTime)
          );
        }
        ...
      }
    • 부모 클래스?

    public abstract class Employee implements PayrollConstants {
        public abstract boolean isPayday();
        public abstract Money calculatePay();
        public abstract void deliverPay(Money pay);
    }
    • 인터페이스?

      • 계층의 가장 상위에 선언되어 있다.

    public interface PayrollConstants {
       public static final int **TENTHS_PER_WEEK = 400;** 
       public static final double **OVERTIME_RATE = 1.5;** 
    }
    • static import 사용 ✨

    import static PayrollConstants.*;
    
    public class HourlyEmployee extends Employee {
        private int tenthsWorked;
        private double hourlyRate;
    
        public Money calculatePay() {
          int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
          int overTime = tenthsWorked - straightTime;
          return new Money(
            hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime)
          );
        }
        ...
    }
  3. 상수 vs Enum

    • 주석으로 상수의 의미를 전달,,?

    public class EnumExample {
    		public static void main(String[] args) {
    			/*
    			* 월요일 == 1
    			* 화요일 == 2
    			* 수요일 == 3
    			* 목요일 == 4
    			* 금요일 == 5
    			* 토요일 == 6
    			* 일요일 == 7
    			*/
    			int day = 1;
    			
    			switch (day) {
    					**case 1:
    						System.out.println("월요일");**
    						break;
    					case 2:
    						// ...
    			}
    		}
    }ㅇ
  • 상수들이 많아진다면? 집합을 만들고 싶다.

public class EnumExample {
		private val static int MONDAY = 1;
		private val static int TUESDAY = 2;
		private val static int WENDESDAY = 3;
		private val static int THURSDAY = 4;
		private val static int FRIDAY = 5;
		private val static int SATURDAY = 6;
		private val static int SUNDAY = 7;

		public static void main(String[] args) {

			int day = MONDAY;
			
			switch (day) {
					**case MONDAY:
						System.out.println("월요일");**
						break;
					case TUESDAY:
						// ...
			}
		}
}
  • Enum을 활용, 서로 연관된 상수들의 집합

**enum Day {
		MONDAY,
		TUESDAY,
		WENDESDAY,
		THURSDAY,
		FRIDAY,
		SATURDAY,
		SUNDAY
}**

public class EnumExample {
		public static void main(String[] args) {

			int day = Day.MONDAY;
			
			switch (day) {
					**case MONDAY:
						System.out.println("월요일");**
						break;
					case TUESDAY:
						// ...
			}
		}
}
  • 추가 속성 부여 가능

**enum Day(String korDay) {
		MONDAY("월요일"),
		TUESDAY("화요일"),
		WENDESDAY("수요일"),
		THURSDAY("목요일"),
		FRIDAY("금요일"),
		SATURDAY("토요일"),
		SUNDAY("일요일")
}**

public class EnumExample {
		public static void main(String[] args) {

			int day = Day.MONDAY;
			
			**System.out.println(day.korDay);**
			}
		}
}

이름

  1. 서술적인 이름을 사용하라

    • 이렇게는 쓰지마라,,

      public int x() {
        int q = 0;
        int z = 0;
        for (int kk = 0; kk < 10; kk++) {
          if (l[z] == 10)
          {
            q += 10 + (l[z + 1] + l[z + 2]);
            z += 1;
          }
          else if (l[z] + l[z + 1] == 10)
          {
            q += 10 + l[z + 2];
            z += 2;
          } else {
            q += l[z] + l[z + 1];
            z +=2;
          }
        }
        return q;
    }
    • 명명법에 신경쓰기

    public int score() {
        int score = 0;
        int frame = 0;
        for (int frameNumber = 0; frameNumber < 10; frameNumber++) {
          if (isStrike(frame)) {
            score += 10 + nextTwoBallsForStrike(frame);
            frame += 1;
          }
          else if (isSpare(frame)) {
            score += 10 + nextBallForSpare(frame);
            frame += 2;
          } else {
            score += twoBallsInFrame(frame);
            frame += 2;
          }
        }
        return score;
    }
  2. 적절한 추상화 수준에서 이름을 선택하라

    • detail한 구현을 드러내는 이름을 피하라

    public interface Modem {
        boolean **dial(String phoneNumber**);
        boolean disconnect();
        boolean send(char c);
        char recv();
        String **getConnectedPhoneNumber();**
      }
      
    // Refactor
    public interface Modem {
        boolean **connect(String connectionLocator);**
        boolean disconnect();
        boolean send(char c);
        char recv();
        String **getConnectedLocator();**
    }
  3. 이름과 범위는 비례한다

    • 짧은 범위에는 짧은 이름을 사용해도 괜찮다.

    private void rollMany(int n, int pins) {
        for (**int i = 0**; i < n; i++)
          g.roll(pins);
    }
  4. 이름에 부수효과를 설명하라

    • 함수, 변수, 클래스가 하는 모든 일을 기술해야 한다.

    public ObjectOutputStream **getOos()** throws IOException {
        if (m_oos == null) {
          m_oos = new ObjectOutputStream(m_socket.getOutputStream());
        }
        return m_oos;
    }
    
    -> **createOrReturnOos()**

테스트

  1. 잠재적으로 불안정한 모든 부분을 테스트 해야한다.

  2. 사소한 테스트를 건너뛰지 마라

  3. @Ignore 를 붙여 불분명한 요구사항을 표시하라

  4. 경계 조건을 테스트 하라

  5. 버그 주변은 철저히 테스트하라


  • 주소 값 비교

    • Int

    val a: Int = 1
    val b: Int = 1
    
    println(a === b) // true
    • Int?

    val a: Int? = 1
    val b: Int? = 1
    
    println(a === b) // true
    • ~128 - 127 사이만 캐싱

    val a: Int? = 128
    val b: Int? = 128
    
    println(a === b) // false
    • String

    val a: String = "1"
    val b: String = "1"
    
    println(a === b) // true
  • Marker Interface

    data class User(val name: String): Serializable
    • Serializable

    import java.io.*;
    
    public class SerializableTester {
    
        public void serializableTest() throws IOException, ClassNotFoundException {
            File f = new File("user.txt");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f));
            objectOutputStream.writeObject(new User("sujin", "sujin@gmail.com"));
        }
    
        class User {
            private String name;
            private String email;
    
            public User(String name, String email) {
                this.name = name;
                this.email = email;
            }
    
    				// ...
        }
    
        public static void main(String[] args) {
            serializableTest serializableTester = new SerializableTester();
            try {
                **serializableTester.serializableTest();**
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
        }
    }
    • 익셉션!

    • Serializable 인터페이스를 구현함으로써 해결

       class User implements Serializable {
            private String name;
            private String email;
    
            public User(String name, String email) {
                this.name = name;
                this.email = email;
            }
       // ...
    • writeObject()를 살펴보자

    private void writeObject0(Object obj, boolean unshared)
            throws IOException
        {
    			... 
    
                if (obj instanceof String) {
                    writeString((String) obj, unshared);
                } else if (cl.isArray()) {
                    writeArray(obj, desc, unshared);
                } else if (obj instanceof Enum) {
                    writeEnum((Enum<?>) obj, desc, unshared);
                } **else if (obj instanceof Serializable) {**
                    writeOrdinaryObject(obj, desc, unshared);
                } else {
                    if (extendedDebugInfo) {
                        throw new NotSerializableException(
                            cl.getName() + "\n" + debugInfoStack.toString());
                    } else {
                        throw new NotSerializableException(cl.getName());
                    }
                }
            } finally {
                depth--;
                bout.setBlockDataMode(oldMode);
            }
        }

Last updated