고양이와 코딩

[쏙쏙 들어오는 함수형 코딩] - CH8 ~ CH9 계층형 설계 본문

함수형 코딩 스터디

[쏙쏙 들어오는 함수형 코딩] - CH8 ~ CH9 계층형 설계

ovovvvvv 2024. 2. 19. 23:39
728x90

계층형 설계란?

- 계층형 설계란, 소프트웨어를 계층으로 구성하는 기술입니다. 각 계층에 있는 함수는 바로 아래 계층에 있는 함수를 이용해 정의합니다.

이미지 출처 : https://velog.io/@qmflf556/%EC%8F%99%EC%8F%99-%EB%93%A4%EC%96%B4%EC%98%A4%EB%8A%94-%ED%95%A8%EC%88%98%ED%98%95-%EC%BD%94%EB%94%A9-CH8

 

계층형 설계 감각을 키우기 위한 입력

 

함수 본문

  • 길이
  • 복잡성
  • 구체화 단계
  • 함수 호출
  • 프로그래밍 언어의 기능 사용

계층 구조

  • 화살표 길이
  • 응집도
  • 구체화 단계

함수 시그니처

  • 함수명
  • 인자 이름
  • 인잣값
  • 리턴값

 

계층형 설계 감각을 키우기 위한 출력

조직화

  • 새로운 함수를 어디에 놓을지 결정
  • 함수를 다른 곳으로 이동

구현

  • 구현 바꾸기
  • 함수 추출하기
  • 데이터 구조 바꾸기

변경

  • 새 코드를 작성할 곳 선택하기
  • 적절한 수준의 구체화 단계 결정하기

 

계층형 설계의 4가지 패턴

  1.  직접 구현
  2.  추상화 벽
  3.  작은 인터페이스
  4.  편리한 계층
function freeTieClip(cart) {
  var hasTie = false;
  var hasTieclip = false;
  for (var i = 0; i < cart.length; i++) {
    var item = cart[i];
    if (item.name === "tie") {
      hasTie = true;
    } 
    if (item.name === "tie clip){
    	hasTieClip = true;
    }
  if (hasTie && !hastieClip) {
    var tieClip = make_item("tie clip", 0);
    return add_item(cart, tieClip);
  }
  return cart;
}

넥타이 하나를 사면 무료로 넥타이 클립을 하나 주는 코드를 직접 구현으로 수정해 봅시다

다음과 같은 접근 방식을 고려할 수 있어요

 

1. 단일 책임 원칙을 준수하도록 함수를 분할 : 함수가 한 가지 목적만 수행하도록 만듭니다. 예를 들어 장바구니에서 넥타이 클립을 추가하는 로직과, 넥타이를 사면 무료로 넥타이 클립을 주는 로직을 분리할 수 있습니다.

2. 더 작은 함수로 분리 : 함수가 더 재사용 가능하고 테스트하기 쉬워지도록, 작은 단위로 분리합니다.

3. 의존성 최소화 :  함수가 외부 변수에 의존하지 않도록 합니다. (액션이 아니게?) 

 

위 코드의 for loop에 해당하는 내용 (카트에 타이 클립이 있는지 확인)은  장바구니에 찾는 제품이 있는지 확인하는 함수를 만들어서 분리 할 수 있습니다.

 

function freeTieClip(cart) {
	var hasTie = isInCart(cart, "tie");
    var hasTieClip = isInCart(cart, "tie clip");
    
    if (hasTie && !hasTieClip) {
    	var tieClip = make_item("tie clip", 0);
        return add_item(cart, tieClip);
    }
    return cart;
}
    

 function inInCart(cart, name) {
 	for(var i = 0; i < cart.length; i++) {
    	if(cart[i].name === name) {
        	return true;
    }
    return false;
}

서로 다른 추상화 단계에 있는 기능을 사용하면 직접 구현 패턴이 아닙니다. 개선되기 전 코드는  { make_item(), add_item() }이 같은 추상화 단계, { array index, for loop } 가 같은 추상화 단계이지만, 두 묶음이 다른 추상화 관계이므로 직접 구현 패턴이 아닙니다. 

 

그러나 개선된 코드는 isIncart(), make_item(), add_item() 함수가 전부 같은 추상화 단계에 있으므로 직접 구현 패턴입니다.

(함수가 모두 비슷한 계층에 있다면 ! . ̫ .)

 

직접 구현

실습

const onInsert = useCallback(() => {
    const todo = {
      id: nextId.current,
      text: inputText,
      state: false,
    };
    setTodos(todos.concat(todo));
    setInputText("");
    nextId.current += 1;
  }, [inputText]);

 

제가 만든 todolist의 일부 함수입니다. 

이 함수의 호출 그래프를 그려보았는데, useState()를 어떻게 그려야 할지 고민이 됐습니다!!

 

친구에게 조언을 구했는데, `useState` 로부터 반환되는 setter 함수들은 상태를 변경하는 역할을 하므로 상태 변경 함수로 간주한다는 말을 듣고!  이렇게 작성 해 보았습니다.

 

 

setTodos() 에서 호출하는 concat() 함수로 인해 같은 추상화 단계를 사용하고 있지 않으므로 이 코드를 억지로 같은 추상화 단계가 되도록 맞춰보겠습니다 

 

const onInsert = useCallback(() => {
    setTodos((prevTodos) => {
      const todo = {
        id: nextId.current,
        text: inputText,
        state: false,
      };
      return [...prevTodos, todo];
    });
    setInputText("");
    nextId.current += 1;
  }, [inputText]);

이렇게 하면 concat() 메서드를 호출하는 부분이 사라지고 이전 상태인 `prevTodos` 를 가져와서 새로운 todo를 추가한 후 새로운 배열을 반환하는 방식으로 배열을 업데이트 합니다. 

 

이제 onInsert() 안에서 호출되는 함수들은 같은 추상화 단계입니다.

하지만 예시가 예시인만큼 근본적으로 추상화 단계를 같게 만드는것이 어떤 이점을 가져오는지 잘 모르겠습니다 ... 🧐 

 

// 제품에 대한 기본 동작: 상품의 가격을 변경한다.
function changeProductPrice(product, newPrice) {
  return { ...product, price: newPrice };
}

// 장바구니 기본 동작: 장바구니에 담긴 상품의 총 가격을 계산한다.
function calculateTotalPrice(cart) {
  return cart.reduce((total, item) => total + item.price * item.quantity, 0);
}

// 일반적인 비지니스 규칙: 장바구니에서 상품을 제거할 때, 해당 상품을 장바구니에서 삭제한다.
function removeFromCart(cart, productId) {
  return cart.filter((item) => item.id !== productId);
}

// 장바구니 비즈니스 규칙: 장바구니에 상품을 추가할 때, 이미 추가된 상품인 경우 수량을 증가시킨다.
function addToCart(cart, product) {
  const existingProductIndex = cart.findIndex((item) => item.id === product.id);
  if (existingProductIndex !== -1) {
    const updatedCart = removeFromCart(cart, product.id); // 일반적인 비지니스 규칙 함수 호출
    return addToCart(updatedCart, {
      ...product,
      quantity: cart[existingProductIndex].quantity + 1,
    }); // 장바구니 비즈니스 규칙 함수 호출
  } else {
    return [...cart, { ...product, quantity: 1 }];
  }
}

// 장바구니 시스템
let cart = [];

gpt가 쪄 준 장바구니 시스템 계층의 가장 위에 있는 addToCart 함수에서 자바스크립트 언어 기능에 해당하는 findIndex() 메서드를 호춣고 있으므로, 인덱스를 찾는 함수를 따로 빼도록 하겠습니다 („• ֊ •„)੭

addToCart 내에서 인덱스를 찾고 cart 배열을 받아오는것도 책에서 말하는 비즈니스 규칙에서 cart가 배열임을 알 필요가 없다! 에 적합한지 궁금해졌습니다 !! (정답은 모름 ㅎㅎㅋㅋ)

 

function addToCart(cart, product) {
  const existingProductIndex = toFindIndex(cart, product);
  if (existingProductIndex !== -1) {
    return changeProductQuantity(
      cart,
      product.id,
      cart[existingProductIndex].quantity + 1
    ); // 장바구니 기본 동작 함수 호출
  } else {
    return [...cart, { ...product, quantity: 1 }];
  }
}

function toFindIndex(cart, product) {
  return cart.findIndex((item) => item.id === product.id);
}

수정하고보니 if, else문도 더 일반적이게 고칠 수 있을 것 같습니다

 

// 일반적인 비지니스 규칙: 장바구니에 상품을 추가할 때, 이미 추가된 상품인 경우 수량을 증가시킨다.
function addToCart(cart, product) {
  const existingProductIndex = toFindIndex(cart, product);

  if (existingProductIndex !== -1) {
    return updateCartForExistingProduct(cart, product, existingProductIndex);
  } else {
    return [...cart, { ...product, quantity: 1 }];
  }
}

// 상품에 해당하는 인덱스를 찾아주는 함수
function toFindIndex(cart, product) {
  return cart.findIndex((item) => item.id === product.id);
}

// 이미 추가된 상품인 경우에 장바구니를 업데이트 하는 함수
function updateCartForExistingProduct(cart, product, existingProductIndex) {
  return changeProductQuantity(
    cart,
    product.id,
    cart[existingProductIndex].quantity + 1
  );
}

이미 장바구니에 있는 상품인 경우에 수량을 더해서 업데이트 하는 함수를 따로 빼고, if문 안에서 return으로 처리 해 줍니다.

 

addToCart 함수에서 조건문을 없애고 싶습니다!

// 일반적인 비지니스 규칙: 장바구니에 상품을 추가할 때, 이미 추가된 상품인 경우 수량을 증가시킨다.
function addToCart(cart, product) {
  return processCartItem(cart, product);
}

// 상품이 이미 장바구니에 존재하는지 확인하고 수량을 증가시키는 함수
function processCartItem(cart, product) {
  const existingProductIndex = toFindIndex(cart, product);

  if (existingProductIndex !== -1) {
    return updateCartForExistingProduct(cart, product, existingProductIndex);
  } else {
    return [...cart, { ...product, quantity: 1 }];
  }
}

// 상품에 해당하는 인덱스를 찾아주는 함수
function toFindIndex(cart, product) {
  return cart.findIndex((item) => item.id === product.id);
}

// 이미 추가된 상품인 경우에 장바구니를 업데이트 하는 함수
function updateCartForExistingProduct(cart, product, existingProductIndex) {
  return changeProductQuantity(
    cart,
    product.id,
    cart[existingProductIndex].quantity + 1
  );
}

 

기존 addToCart() 의 내용을 통째로 processCartItem() 함수로 옮기고, addToCart() 에서는 processCartItem() 을 호출 해 주기만 하면 되는 코드로 수정했습니다.

 

 

추상화 벽

세부 구현을 감춘 함수로 이루어진 계층

추상화 벽에 있는 함수를 사용할 때는 구현을 전혀 몰라도 함수를 쓸 수 있습니다.

이미지 출처 : https://velog.io/@qmflf556/%EC%8F%99%EC%8F%99-%EB%93%A4%EC%96%B4%EC%98%A4%EB%8A%94-%ED%95%A8%EC%88%98%ED%98%95-%EC%BD%94%EB%94%A9-CH9

 

추상화 벽을 기준으로 그 윗 계층에 있는 함수와 그 아래 계층에 있는 함수는 서로 독립적으로(의존성x) 사용 될 수 있습니다.

 

정확히 어떤 느낌인지 아직 완벽히 이해하진 못했지만, 

`함수 호출 그래프를 그렸을 때 점선을 가로지르는 화살표가 없다면 추상화벽` 이라고 이해하고 있습니다 

 

작은 인터페이스

새로운 코드를 추가할 위치에 관한 내용. 인터페이스를 최소화하여 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있다.

 

새로 작성한 코드를 추상화 벽 위에 있는 계층이 구현했을때 생기는 이점

  • 더 직접 구현에 가깝다
  • 시스템 하위 계층 코드가 늘어나지 않는다 (신경써야 할 코드가 줄어든다)

상위 계층에 어떤 함수를 만들 때 가능한 현재 계층에 있는 함수로 구현하는 것이 작은 인터페이스를 실천하는 방법

 

편리한 계층

앞에서 배운 세 가지 패턴을 언제 적용하고, 또 언제 적용하지 말아야 하는지를 알려준다.