제 개인적으로는 구간을 알아보기 쉽도록 블록 내 한 줄이더라도 대괄호
{}
와 줄바꿈을 입력하고 있습니다.
함수는 한 가지를 해야 한다.
지정한 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈.
어떤 작업을 수행하는데 여러 단계와 여러 객체를 거쳐야 동작하는 코드라면 단위테스트를 작성할 때 단위로 쪼깨는 과정부터 너무 힘듭니다.
getHtml()
: 추상화 수준이 아주 높음String pagePathName = PathParser.render(pagepath)
: 추상화 수준 중간.append("\n")
: 추상화 수준 아주 낮음구분이 힘들고 이해가 어려워지면 결국 다른 사람들이 함수에 세부사항을 점점 추가해갈 것입니다.
본질적으로 switch 문은 n 가지 일을 처리한다.
각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법이 존재. (다형성 polymorphism 이용)
switch 사용 예 - 직원 유형에 따라 다른 값을 계산, 반환하는 함수
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
} }
switch 문 문제 해결한 코드
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactorylmpl implements EmployeeFactory {
public Employee makeEmployee(EniployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
다형성을 위한 코드 (다형적 객체 생성) 안에서는 swtich 문을 활용할 수 있다고 나와있지만 가급적 적게 쓰도록 의식해볼 생각입니다.
함수가 하는 일을 좀 더 잘 표현하는 이름으로
좋은 이름이 주는 가치
코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다. - word
함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워짐
이름이 길어도 괜찮음
이름을 정하는데 시간을 들여도 ok. 최대한 서술적인 이름을 골라보자
명명에는 일관성이 있어야 함
모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용
includeSetupAndTeardownPages();
includeSetupPages();
includeSuiteSetupPage();
includeSetupPage();
명사의 경우 복수형을 써야하는지 단수를 써야하는지에 대한 판단 기준을 아직 잘 모르겠습니다만 일단은 복수형을 기본으로 생각하고 있습니다.
includeSetupAndTeardownPages() 의 경우는 하나의 함수가 두가지의 책임을 맡고 있으니 단위로 분리하는 작업이 필요할 것 같습니다.
이상적인 인수 개수는 0개 (무항)
인수는 개념을 이해하기 어렵게 만듦
// newPageContent 구성 시 문자열이 들어감
// 현 시점에서 StringBuffer라는 별로 중요하지 않은 세부사항을 알아야 함...)
includeSetupPageInto(newPageContent) // 함수 이름과 인수 사이 추상화 수준이 다름
includeSetupPage() // 이해하기 쉬움
테스트 관점에서 인수가 없다면 간단하겠지만
출력 인수는 입력 인수보다 이해하기 어려움
최선은 입력 인수가 없는 경우 이고
차선은 입력 인수가 1개뿐인 경우
pageDataSetupTeardownIncluder.render(pageData); // pageData 객체 내용을 rendering 하겠다는 뜻
여러 개의 parameter를 사용해야 하는 상황이라면 class를 생성해 관련 객체를 구성하는 것도 방법이지 않을까 생각됩니다.
함수에 인수 1개를 넘기는 이유
인수에 질문을 던지는 경우
boolean fileExists("myFile");
인수를 무언가로 변환해 결과를 반환하는 경우
InputStream fileOpen("myFile"); // String형 파일 이름을 inputStream으로 변환
함수의 이름을 지을 때는 질의 / 변환(조회 / 명령) 의 두 경우를 분명히 구분한다.
이벤트 함수 (입력 인수만 존재)
pwAttemptFailedNtimes(int attempts);
위의 경우를 제외하면 가급적 단항 함수는 피하자 (void인 단항 함수)
변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.
void includeSetupPageInto(StringBuffer pageText); // StringBuffer라는 출력 인수...
StringBuffer transform(StringBuffer in) // 변환 함수 형식이므로 아래 사용보다 좋다.
// void transform(StringBuffer out)
// render(boolean isSuite);
renderForSuite();
renderForSingleTest();
불필요한 T/F를 넘기지 않도록 앞으로 유의해야겠습니다.
인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다
이항 함수가 적절한 경우
직교 좌표계 객체
Point p = new Point(0, 0);
이항 함수가 무조건 나쁘다는 것은 아지만 (불가피한 경우도 있지만) 가능하면 단항 함수로 바꾸는게 좋음
outputStream.writeField(name); // writeField 메서드를 outputStream 클래스 구성원으로 생성
// 또는 outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않거나...
// 또는 FieldWriter 클래스를 생성하여 구성자에서 outputStream을 받고 write method를 구현하거나...
assertEquals(message, expected, actual); // ??
assertEquals(1.0, amount, .001) // 부동소수점 비교가 상대적
intelliJ에서 다음의 옵션을 켜두면 들어갈 parameter의 힌트를 확인할 수 있습니다. (단, 코드 길이가 길어져서 가독성을 해칠순 있습니다.)
IntelliJ IDEA - Inlay hints
인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다
// Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point 센터, double radius); // x, y 좌표를 변수로 묶어 넘겨 개념을 표현
인수 개수가 가변적인 함수도 필요
// public String format(String format, Object... args)
String.format("%s worked %.2f hours.", name, hours);
가변 인수를 취하는 모든 함수는 단항, 이항, 삼항 함수로 취급할 수 있음
...
로 표기함수의 의도, 인수의 순서 및 의도를 제대로 표현하려면 좋은 함수 이름이 필수
write(name); // name이 무엇이든 write한다
writeField(name); // name이 field 라는 사실을 확인 가능
assertExpectedEqualsActual(expected, actual); // 함수 이름에 키워드를 추가
부수효과는 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency) 초래
부수효과의 예 - UserVaildator
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
예제 함수를 수정할 수 없다면 명칭이라도
checkPasswordAndInitalizeSession
같이 명확히 표기되면 혼란이 줄어들 것 같습니다. (함수가 한가지 기능만 행하지 않지만)
this
public void appendFooter(StringBuffer report){...}
// 함수 선언부를 확인해야 s의 의미를 알 수 있음
appendFooter(s);
// 출력 인수로 사용하는 대신 해당 객체의 method로 호출하는 방식으로 변경
report.appendFooter();
// attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true, 실패 시 false 반환
public boolean set(String attribute, String value);
// "username"이 "unclebob"으로 설정되어 있는지 확인?
// "username"을 "unclebob"으로 설정하는 코드?
if (set("username", "unclebob")){...}
// 명령과 조회를 분리하여 혼란을 일으키지 말것
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
if 문에서 명령을 표현식으로 사용하기 쉬운 탓...
if (deletePage(page) == E_OK) // 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기
if (deletePage(page) = E_OK) {
if (reg1st ry.deleteReference(page.name) = E_OK){
if (configKeys.deleteKey(page.name.makeKey()) = E_0K) {
logger.log("page deleted");
} else {
logger.log("configKey not deleted");
}
} else{
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERR0R;
}
오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되기 때문에 코드가 깔끔해짐
try {
deletePage(page);
registry.deleteReference(page, name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}
// 정상 동작과 오류 처리 동작의 분리
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAHReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
아마도 처음부터 정상 기능과 오류 기능을 분리할 수는 없을 것이기 때문에 우선은
Exception이 발생되도록 수정 -> Exception 발생 시 동작하는 기능 분리
식으로 단계적으로 고칠 수 있을 것 같습니다.
public enum Error {
OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT;
}
COP에 대해서 명확히 알지 못하고 있어 추후 내용을 알아보고 정리해둘 예정!
goto
는 조건에 맞으면 지정된 라인으로 이동하도록 처리하는 용도로 이해했습니다.
엑셀 자동화 개발 시에 GoTo 문을 사용했었는데 소스가 긴 분기문에서 코드 로직을 수정하는데 처리 순서를 명확히 파악하기 힘들어서 정말 애먹었던 기억이 있습니다.