Search
Duplicate

3장 함수

개인적 감상평

로직들을 최대한 쪼개어 하나의 기능만 하는 메소드를 만든 뒤 그 메소드를 이어서 로직을 풀어냅니다. 그리고 2장 의미 있는 이름 에서 클래스명은 명사 메소드명은 동사로짓기를 추천합니다.
그리고 길더라도 서술형으로 작성해 개발자가 읽고 추측하기 쉽게 작성하라고 합니다.
그럼 이렇게 읽기쉬워진 작은 하나하나의 함수들을 합쳐서 하나의 로직을 만든다는것은 결국 하나의 이야기를 만들어내는 것이죠.
개발자라는 작가가 이야기를 어떻게 풀어내면 좋을지 알려주는 3장입니다.

1. 작게 만들어라.

함수를 작게만들 수록 하나의 함수를 이해하는데 필요한 해석능력이 줄어든다.
들여쓰기 수준은 1~2단을 넘지 않도록 하라.

2. 한 가지만 해라.

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

그 한가지는 무엇인가

public static String renderpageWithSetupsAndTeardowns( PageData pageData, boolean iS5uite) throws Exception { if (isTestPage(pageData)) includeSetupAndTeardownPages(pageData, isSuite); return pageData.getHtml(); }
Java
복사
이 함수내에서 수행되는 로직은 3가지 이다.
1.
페이지가 테스트 페이지인지 판단.
2.
그렇다면 설정 페이지와 해제 페이지를 넣는다.
3.
페이지를 HTML로 렌더링해 반환한다.
3가지 로직을 수행하지만 세 로직 모두 동일한 추상화 수준을 가진다. 그렇기에 한가지라 할 수 있다.
결국 한 가지만 하라는 의미는 서로다른 추상화 수준을 가진 함수를 하나의 함수안에 사용하지 말라는 의미가 된다.
메소드로 분리할 수 있는 유의미한 코드가 있다면 한 가지만 하는 함수가 아니다.

3. 함수당 추상화 수준은 하나로 맞춰라.

//높은 추상화 수준 getHtml(); //중간 추상화 수준 String pagePathName = PathParser.render(pagepath); //낮은 추상화 수준 pagePathName.append("\n");
Java
복사
하나의 함수에 추상화 수준이 다른 코드들이 뒤섞여있다면, 가독성이 떨어진다.
이렇게 근본 개념과 세부사항이 뒤섞이기 시작하면 다른 개발자들이 함수에 세부사항을 점점 더 추가한다.

위에서 아래로 코드읽기: 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 계속해서 한 단계씩 낮아지는 것을 내려가기 규칙이라 부른다.

switch문

저차원 클래스에 숨기고 다형성(phlymorphism)을 이용해 반복하지 않도록 한다.
상속 관계로 switch문을 숨긴 뒤 다른 코드로 노출하지 않는다.
불가피한 상황도 분명 있다.
Before Case
//before case 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); } }
Java
복사
문제점
1.
함수가 길며 새 유형이 추가되면 더 길어진다.
2.
SRP(Single Responsiblity Principle)위반
3.
OCP(Open Closed Principle)위반
4.
'한 가지' 작업만 수행하지 않는다.
After Case
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(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r) case HDURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmployee(r}; default : throw new InvalidEmployeeType(r.type}; } } }
Java
복사
개선점
1.
switch문을 EmplyeeFactory라는 추상 팩토리에 숨긴다.
2.
메소드의 실제 실행은 Employee 인터페이스를 거쳐 다형성으로 맞는 로직이 수행된다.
3.
팩토리는 switch문을 통해 Employee 파생클래스의 인스턴스를 생성한다.

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

: 이름이 길어진다고 걱정하지않아도 된다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
Ex)
includesetupAndTeardownPages
isTestable
makeCharacterAndinitialize
이름을 붙힐때는 일관성이 있어야 한다.

5. 함수 인수는 적을수록 좋다.

인수는 정말 '특별한' 이유가 없는한 3개 를 넘기지 않도록 하자.

단항 형식

이럴때 사용하자
인수에 질문을 던지는 경우
boolean fileExists("MyFile")
Java
복사
인수를 뭔가로 변환해 반환하는 경우
InputStream fileOpen("MyFile")
Java
복사
이럴땐 사용을 피하자
변환 함수에서 출력 인수를 사용
void includeSetupPageInto(StringBuffer pageText);
Java
복사
⇒ 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.
⇒ 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려주자.
//worst void transform(StrinfBuffer out); //best StringBuffer transform(StringBuffer in);
Java
복사

플래그 인수

인수로 논리값을 보내는 것은 함수가 한 번에 여러가지를 처리한다고 하는 셈이기에 자제하자.
대책은?
render(true) 대신 renderForSuit()renderForSingleTest()로 나눈다.

이항 함수

writeField(outputStream, name);
Java
복사
writeField(name);
Java
복사
좌측과 우측중 어느게 더 가독성이 높은가?
좌측은 첫 인수를 무시해야한다는 사실을 알기까지 약간의 시간이 소요된다.
그리고 이렇게 인수를 무시하면 그 안에 오류가 숨어들 가능성이 생긴다.
대책은?
writeField 메소드를 outputStream 클래스 구성원으로 만들어 처리한다.
outputStream.writeField(name)
Java
복사
outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않는다.
class A{ private OutputStream outputStream; ... public void logic(String name){ ... writeField(name); } }
Java
복사
FieldWrite 라는 클래스를 만들어 구성자에 outputStream을 받아서 처리한다.
class FieldWrite{ private OutputStream outputStream; public FieldWrite(OutputStream outputStream){ this.outputStream = outputStream; } public void FieldWrite(String name){...} }
Java
복사

삼항 함수

삼항 함수는 인수가 2개인 함수보다 순서, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다.

인수 객체

인수가 2개 이상일 때 일부가 따로 정보를 제공하는 Meta Class로 축약시킬 수 있을지 짚어본다.
Circle makeCircle(double x, double y, double radius);=
Java
복사
⇒ 여기서 x와 y는 평면상의 위치 좌표값을 나타내기에 Point 클래스로 묶을 수 있다.
Circle makeCircle(Point center, double radius);
Java
복사
⇒ 눈속임이라 생각할수도 있지만 하나로 묶음으로써 두 필드(x, y)를 하나의 개념으로 표현하여 가독성을 높힐 수 있다.

인수 목록

인수가 가변적인 함수도 있다(Ex: String.format) 하지만 가변적으로 추가되는 인자를 동등하게 취급하면 하나의 인수형태(List)로 취급할 수 있다.
String.format("%s worked %.2f hours.", name, hour);
Java
복사
얼핏 보면 인수는 3개지만 name과 hour는 동등하게 취급할 수 있어서 사실상 이항 함수가 된다.
public Strinf format(String format, Object... args);
Java
복사

동사와 키워드

단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
writeField(name)
Java
복사
함수 이름에 키워드를 추가한다.
//worst case assertEquals(expected, actual); //best case assertExpectedEqualsActual(expected, actual);
Java
복사

6. 부수 효과를 일으키지 마라!

함수에서 하나의 로직 외에 다른 로직을 부수적으로 수행하게 하지 말아라.
Ex) 비밀번호 확인 메소드(checkPassword)를 호출했는데, 확인만 하는게아니라 세션초기화(Session.initialize())까지 한다면 이 메소드에서는 비밀번호만 확인하기위해 메소드를 호출했다가 의도치않게 세션까지 초기화되는 무수효과로 에러가 발생할 수 있다. 즉, 세션을 초기화해도 괜찮은 경우에만 호출이 가능하게 된다.
//worst case public void appendFooter(StringBuffer report){...} ... appendFooter(s); //good case report.appendFooter();
Java
복사

7. 명령과 조회를 분리하라!

:함수는 뭔가 수행하거나 뭔가에 답하거나 둘 중 하나만 하도록 해라.
//worst case //attribute 속성을 찾아 value로 설정후 성공하면 true 실패하면 false 반환 public boolean set(String attribute, Strinv value); if(set("username", "unclebob")){...} //good case if(attributeExists("username")){ setAttribute("username", "unclebob"); ... }
Java
복사
⇒ worst case는 set메소드를 호출하는데 이게 뭘 어디에 설정하는건지 왜 if문안에 있는건지 알 기 어렵다.
⇒ attribute 조회와 설정(명령)을 나눠서 수행한다.

8. 오류 코드보다 예외를 사용하라!

오류 코드로 반환을 하게되면 그에 대한 처리로직을 따로 조건문을 통해 처리해줘야 하는데, 이는 코드의 indent를 증가시킬 뿐 아니라 오류를 바로 처리해야한다는 문제가 생긴다.
그렇기에 오류코드를 사용하기보다는 예외를 사용하면 try/catch를 통해 catch에서 관리할 수 있다.

Try/Catch 블록 분리

try/catch블록도 분리하는편이 좋다.
//before case try{ deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch(Exception e){ logger.log(e.getMessage()); } //after case pulbic void delete(Page page){ try{ deletePageAndAllReferences(page); } catch(Exception e){ logError(e); } } 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()); }
Java
복사
⇒ delete 함수에서 모든 예외를 받아 처리한다. 실제 페이지 제거 로직은 deletePageAndAllReferences 으로 분리했고 에러 출력에 대한 메소드도 logError로 분리했다.

9. 반복하지 마라

같은 로직을 중복해서 작성하면 해당 로직이 변경되었을때 사용되는 모든 곳에서 수정을 해줘야 한다.
객체지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앤다.