17장. 냄새와 휴리스틱
Overview
휴리스틱
Heutiskein → '찾아내다', '발견하다’
🌱 발견법 - 간편 추론, 경험을 기반으로 **그때그때 상황과 직관에 따라 행동**하여 결론을 도출하는 것
휴리스틱 알고리즘
최적 해를 보장하지 않는다.
Greedy
Travelling Salesman Problem
저자의 휴리스틱
주석
🌱 주석은 코드만으로 다하지 못하는 설명을 부언한다.
부적절한 정보
코드만으로 충분한데 구구절절 설명하는 주석은 없어야 한다.
i++ // i 증가
function signature만 달랑 기술하는 Javadoc은 사용하지 않아야한다.
/** * @param sellRequest * @return * @throws ManagedComponentException */ public SellResponse beginSellItem(SellRequest sellRequest) throws ManagedComponentException
주석 처리된 코드
주석 처리된 코드를 발견하면 즉각 지워버려야 한다.
함수
너무 많은 parameter
parameter 개수는 작을수록 좋다. 아예 없는게 가장 좋다!
넷 이상은 최대한 피해야한다.
fun testEffect(key1: Any?, key2: Any?, key3: Any?, key4: Any?) { } fun testEffect( key1: Any?, key2: Any?, key3: Any?, key4: Any? ) { }
flag parameter ✨
boolean, int, enum 등 플래그 파라미터는 함수가 내부적으로 여러 기능을 수행한다는 뜻
플래그를 넘겨 제어하는 대신, 새로운 함수를 만들자.
죽은 함수
아무도 호출하지 않는 함수는 삭제해야 한다.
일반
한 소스 파일에 여러 언어를 사용하는 것
이상적으로는 소스 파일 하나 당, 하나의 언어만 사용해야 한다.
불가피한 경우에 최대한 언어 범위를 줄여야 한다.
당연한 기능을 구현해라
함수나 클래스는 프로그래머가 당연히 받아들일 수 있는 기능을 제공해야 한다.
Day day = DayDate.StringToDay(String dayName); // Monday -> Day.MONDAY
중복✨
DRY (Don’t Repeat Yourself)
코드에서 중복을 발견하면, 추상화의 기회이다!
중복 코드 → 하위 루틴이나 클래스로 분리 가능
올바른 추상화 수준을 설정하기 ✨
high level - 집의 형태, 외관, 공간, 방의 배치 (정책)
low level - 콘센트, 전등의 위치, 지붕의 크기 등의 기초공사 수준 (세부사항)
의존성 역전 법칙 (DIP) → 집의 형태가 전등의 위치나 색깔에 의해 바뀌어선 안된다. low level이 high level에 의존
기초 클래스 (interface, abstract) 에서 파생 클래스에 의존해서는 안된다.
기능 욕심
클래스 메서드는 자기 클래스의 상태에 관심을 가져야지, 다른 객체를 조작해서는 안된다.
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); } }
부적절한 static 함수
Math.max(double a, double b) 는 좋은 static 함수. 모든 정보를 parameter를 통해 가져온다.
재정의 할 가능성이 있는 함수를 static으로 만들어선 안된다. ✨
시급을 계산하는 방법은 언제든 바뀔 수 있다.
HourlyPayCalculator.calculatePay(employee, overtimeRate);
서술적 변수 (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
(부정 조건을 피하자)
명명된 상수로 교체해라! ✨
코드에서 숫자를 사용하지 마라 (숫자는 명명된 상수 뒤로 숨기자)
ex) 책의 한 페이지당, 55줄을 인쇄한다 →
LINES_PER_PAGE
double circleArea = radius * radius * **Math.PI**
조건을 캡슐화 하라
if (timer.hasExpired() && !timer.isRecurrent())
보다는
if (shouldBeDeleted(timer))
가 더 좋다.
함수는 한 가지 일만 한다.
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);
}
시간적인 결합을 드러내라
함수 파라미터를 통해 함수가 호출되는 순서를 드러낼 수 있다.
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**); } ... }
경계 조건은 캡슐화 하라 ✨
경계 조건은 한 곳에서 설정해두고 사용한다.
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
긴 import 목록을 피해라
패키지에서 클래스 둘 이상을 사용한다면 와일드 카드를 사용한다.
import package.*;
상수는 상속하지 않는다.
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) ); } ... }
상수 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);**
}
}
}
이름
서술적인 이름을 사용하라
이렇게는 쓰지마라,,
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; }
적절한 추상화 수준에서 이름을 선택하라
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();** }
이름과 범위는 비례한다
짧은 범위에는 짧은 이름을 사용해도 괜찮다.
private void rollMany(int n, int pins) { for (**int i = 0**; i < n; i++) g.roll(pins); }
이름에 부수효과를 설명하라
함수, 변수, 클래스가 하는 모든 일을 기술해야 한다.
public ObjectOutputStream **getOos()** throws IOException { if (m_oos == null) { m_oos = new ObjectOutputStream(m_socket.getOutputStream()); } return m_oos; } -> **createOrReturnOos()**
테스트
잠재적으로 불안정한 모든 부분을 테스트 해야한다.
사소한 테스트를 건너뛰지 마라
@Ignore
를 붙여 불분명한 요구사항을 표시하라경계 조건을 테스트 하라
버그 주변은 철저히 테스트하라
주소 값 비교
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
Untitled 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(); } } }
익셉션!
Untitled 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