[ Clean Code ] 클린코드 시리즈 - 함수

2023. 12. 23. 03:50Computer Science

함수가 이해하기 쉬우려면?

  1. 의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까?
  2. 함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램의 내부를 직관적으로 파악할 수 있을까?[작게 만들어라]

블록과 들여쓰기

if / else / while

중첩구조가 생길만큼 함수가 커져서는 안된다. 그러므로 함수에서 들여쓰기

[ 한가지만 해라 ]

  • 함수는 한 가지를 해야한다. 그 한가지를 잘해야한다.그 한가지는 무엇인가? 알기 어렵다는 점
  1. 페이지가 테스트페이지인지 확인한 후
  2. 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다
  3. 테스트 페이지든 아니든 페이지를 HTML 로 렌더링함

-> 지정된 함수 이름 아래 추상화 수준이 하나의 단계

단순히 다른 표현이 아니라, 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러가지 작업을 하는 셈이다.


[ 함수 당 추상화 수준(abstract Level )은 하나로 ]

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

추상화수준이 뭐지?

추상화 수준을 알아보기 앞서 추상화(abstract) 정의를 살펴보면,
복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것 
덧붙임, 자세함을 숨길수록 추상화 수준이 높고, 덧붙임이나 자세하게 명시할수록 추상화 수준이 낮은 것 


1. 추상화수준(levels of abstraction) 높은 것 
   e.g. 커피를 내린다.
   추상화수준(levels of abstraction)  낮은 것 
    e.g. 커피를 내리기 위해 원두의 종류를 고르고, 원두를 갈고, 필터 종이를 말아, 드립커피식으로 커피를 내린다. 
2. 추상화수준(levels of abstraction) 높은 것 
    e.g. 오늘은 클린코드의 챕터 3을 읽는다.
   추상화수준(levels of abstraction)  낮은 것 
    챕터 3은 함수에 대해서 말하고 있으며, 함수는 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있어야한다.

// 추상화수준(levels of abstraction) 높은 것 

// MARK: 클래스 - `몸상태`를 나타내는 클래스
class BodyCondition {

}

// 추상화수준(levels of abstraction) 낮은 것 
class BodyCondition {
	func performActivity(_ activity: ActivityType) {
        switch activity {
        case .sitUp:
            performSitUp()
        case .squat:
            performSquat()
        case .longDistanceRunning:
            performLongDistanceRunning()
        case .yoga:
            performDoingYoga()
        case .dynamicRest:
            performDynamicRest()
        }
    }
}

* 코드는 🐻야곰 코드스타터 챌린지에서 내가 직접 구현해본 클래스와 그 안에 함수 코드이다. ActivityType 은 enum(열거형) 으로 함수의 전달인자로 가지고 왔다.

 

몸상태를 나타내는 클래스(명사) BodyCondition

클래스 안에 함수 performActivity  (활동을 수행하는 함수, 동적인 메소드, 동사) 안에 보면 자세하게 다양한 케이스를 호출할 수 있는 switch - case 로 되어있다.

 

 

[ 위에서 아래로 코드 읽기 - 내려가기 규칙 ]

🤔 개인적으로 궁금한 것- pseudocode(의사)코드
필요한 행동이나 조건을 잘 설정하여 컴퓨터가 수행해야 하는 일을 절차적으로 파악할 수 있게 도와줌
-> 수도코드를 작성하는 코드도 파악하기 쉽기 위해서가 아닐까? 알고리즘 흐름을 잘 알 수 있게 말이다. 

 

코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

 

NOTE
LOGO 언어에서 키워드 TO 는 루비나 파이썬에서 def 와 똑같다. LOGO 에서 모든 함수는 키워드 TO로 시작한다.
위에서 아래로 TO 문단을 읽어내려가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.

 

[ Switch 문 ]

Switch 문 특성 - N 가지를 처리
=> 각 Switch 문을 저차원 클래스에 숨기고 반복하지 않는 방법
다형성(polymorphism) 이용
48p.
새 직원 유형을 추가할 때마다 코드 변경됨
-> 변경

public class EmployeeFactoryImpl implements EmployeeFactory

다형적 객체 생성하는 코드 안에서 상속관계 숨긴 채로 다른 코드에 노출하지 않는다.

 

[서술적인 이름 사용]

testableHtml -> SetupTeardownIncluder
함수가 하는 일을 좀 더 잘 표현
함수가 작고 단순할 수록 서술적인 이름을 고르기도 쉬워진다.

  1. 이름이 길어도 좋다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  2. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.

서술적인 이름 사용 => 개발자의 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
3. 이름을 붙일 때 일관성이 있어야 함.
모듈내 함수 이름은 같은 문구, 명사, 동사 사용
e.g.
includeSetupAndTeardownPages
includeSetupPages
includeSuiteSetupPages
includeSetupPage

[함수 인수]

  1. 함수에서 이상적인 인수개수는 무항
  2. 단항(1개)
  3. 이항(2개)
  4. 다항(4개 이상) -> 특별한 이유가 있어야 함.

[ 테스트 관점 ]

  • 인수는 더 어려움.
  • 갖가지 인수 조합으로 함수를 검증하는 테스트케이스. 작성을 상상해보라.
  • 출력인수는 입력인수보다 이해하기 어려움
    => 흔히 함수에 인수로 입력을 넘기고 반환값으로 출력많이 쓰는 단항형식(52p)함수의 의도와 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다
  • 동사와 키워드

  e.g.
write(name) 바로 이해한다. 이름(name)은 무엇이든 write(쓴다)
=> 더 나은 뜻 writeField(name)
이름이 name 이 필드Field 라는 사실

  • e.g. 함수이름에 키워드 추가*
    함수이름에 인수이름 넣는다. 예를 들어 assertEquals 보다 assertExpectedEqualsActual(expected, actual) 이 더 좋다.
    => 인수 순서 기억할 필요 X
public boolean set(string attribute, string value)
if (set("username", "uncleBod"))

attribute 인 속성값 찾아서 값을 value 로 설정
=>성공하면 true, 실패하면 false 를 반환

함수를 구현한 개발자는 'set' 을 동사로 의도했다.

좋은 코드 -> 혼란을 야기하지 않는 코드

명령과 조회를 분리해 혼란을 뿌리뽑기

if (attributeExists("username")) {
    setAttribute("username", "unclebob");
}

많이 쓰는 단항형식

- 플래그 인수
- 이항 함수
- 삼항 함수

인수객체

- 인수목록
- 동사와 같은 키워드

부수 효과를 일으키지 마라

명령과 조회를 분리하라


[오류코드보다 예외를 사용하라]

  1. try - catch 블록 뽑아내기
    오류처리도 한가지 작업이다.
  2. Error.java 의존성 자석
    명령함수에서 오류코드를 반환하는 방식은 명령 / 조회 분리규칙을 미묘하게 위반하다. 자칫하면 if문에서 명령을 표현식으로 사용하기 쉬운 탓이다.
    if (deletePage(page) == E_OK)
    여러 단계로 중첩되는 코드를 야기
    => 여러 단계로 중첩되는 코드를 야기

⭐️ 오류 코드를 반환하면, 호출자는 오류코드를 곧바로 처리해야 한다는 문제에 부딪힌다.p. 58

Try / catch 블록 뽑아내기

try {
 deletedPage(page);
 registry.deleteReference(page.name);
 configKeys.deleteKey(page.name.makeKey());
} 
catch 

 

오류처리도 한가지 작업이다.
함수도 1가지
=> 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅함.

Error.java 의존성 자석오류 코드를 반환 == 오류 코드를 정의

public enum Error {
 OK, 
 INVALID,
 NO_SUCH,
 LOCKED,
 OUT_OF_RESOURCE,
 WAITING_FOR_EVENT
}

 

⭐️ Error 코드 없이 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다.
      따라서, 재컴파일 / 재배치 없이도 새 예외 클래스를 추가할 수 있다.

 

? 의존성 자석
Error enum 이 변한다면 Error enum 을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야한다.

 

[ 반복하지마라 ]

  • 중복은 소프트웨어 악의 근원
  • Codd - 관계형 데이터베이스 정규형식
  • 객체지향 프로그래밍은 코드를 부모코드를 몰아 중복을 없앤다.
    AOP, COP, 구조적 프로그래밍 -> 중복 제거 전략

[ 구조적 프로그래밍 ]

다익스트라, 구조적 프로그래밍 원칙

  • 모든 함수와 함수 내 모든 블록: 입구 entry,  출구 exit 하나면 됨. 루프 안에서 break, continue 사용하면 안됨.
  • goto 는 절대 금지[함수를 어떻게 짜죠? (61p.)]소프트웨어를 짜는 행위: 글짓기와 비슷
  • e.g. 논문, 기사 작성 생각 기록 후 읽기 좋게 다듬는다.*결론함수: 그 언어에서 동사
    클래스: 동사

[ 프로그래밍의 기술 -> 언어 설계의 기술 ]

궁극적인 목표: 시스템이라는 이야기를 풀어나가기

 


Swift 에서의 함수 

  • 함수를 정의할 때 파라미터(parameter)라고 알고 있는 함수가 입력받는 하나 이상의 타입으로 된 값을 선택적으로 정의
  • 반환타입이라고 알고 있는 작업이 완료되었을때 함수가 다시 전달하는 값의 타입을 선택적으로 정의할 수 있음
func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

함수는 person의 이름을 입력
상수 greeting반환(return)
=> `person` 이라 불리는 `String` 값인 하나의 파라미터와 그 사람의 greeting(인사말) 포함한 `String` 반환 타입을 정의

반환

반환 화살표_ `->` (하이픈 뒤에 오른쪽 방향 꺽쇄를 붙임) 뒤에 반환 타입의 이름을 붙여 함수의 반환 타입을 나타냄

 

매개변수, 전달인자

  • 매개변수: 외부로부터 받아들이는 전달 값의 이름을 의미
  • 전달인자(argument) 혹은 인자: 함수를 실제 호출할 때 전달하는 값
func hello(name: String) -> String { return "Hello \(name)" } 
let helloJenny: String = hello(name:"Jenny") 
print(helloJenny)

코드예시는 야곰 스위프트 프로그래밍 3판 

`hello(name:)`  함수에서 매개변수는 `name` 이고, 
실제 사용시 전달 받는 값 "Jenny" 가 전달인자(argument) 

 

 

참고

클린코드 서적 - Robert C. Martin 

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/functions/

https://ablue-1.tistory.com/81 (추상화수준 참고)