고양이와 코딩
[쏙쏙 들어오는 함수형 코딩] - CH8 ~ CH9 계층형 설계 본문
계층형 설계란?
- 계층형 설계란, 소프트웨어를 계층으로 구성하는 기술입니다. 각 계층에 있는 함수는 바로 아래 계층에 있는 함수를 이용해 정의합니다.
계층형 설계 감각을 키우기 위한 입력
함수 본문
- 길이
- 복잡성
- 구체화 단계
- 함수 호출
- 프로그래밍 언어의 기능 사용
계층 구조
- 화살표 길이
- 응집도
- 구체화 단계
함수 시그니처
- 함수명
- 인자 이름
- 인잣값
- 리턴값
계층형 설계 감각을 키우기 위한 출력
조직화
- 새로운 함수를 어디에 놓을지 결정
- 함수를 다른 곳으로 이동
구현
- 구현 바꾸기
- 함수 추출하기
- 데이터 구조 바꾸기
변경
- 새 코드를 작성할 곳 선택하기
- 적절한 수준의 구체화 단계 결정하기
계층형 설계의 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() 을 호출 해 주기만 하면 되는 코드로 수정했습니다.
추상화 벽
세부 구현을 감춘 함수로 이루어진 계층
추상화 벽에 있는 함수를 사용할 때는 구현을 전혀 몰라도 함수를 쓸 수 있습니다.
추상화 벽을 기준으로 그 윗 계층에 있는 함수와 그 아래 계층에 있는 함수는 서로 독립적으로(의존성x) 사용 될 수 있습니다.
정확히 어떤 느낌인지 아직 완벽히 이해하진 못했지만,
`함수 호출 그래프를 그렸을 때 점선을 가로지르는 화살표가 없다면 추상화벽` 이라고 이해하고 있습니다
작은 인터페이스
새로운 코드를 추가할 위치에 관한 내용. 인터페이스를 최소화하여 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있다.
새로 작성한 코드를 추상화 벽 위에 있는 계층이 구현했을때 생기는 이점
- 더 직접 구현에 가깝다
- 시스템 하위 계층 코드가 늘어나지 않는다 (신경써야 할 코드가 줄어든다)
상위 계층에 어떤 함수를 만들 때 가능한 현재 계층에 있는 함수로 구현하는 것이 작은 인터페이스를 실천하는 방법
편리한 계층
앞에서 배운 세 가지 패턴을 언제 적용하고, 또 언제 적용하지 말아야 하는지를 알려준다.
'함수형 코딩 스터디' 카테고리의 다른 글
[쏙쏙 들어오는 함수형 코딩] CH14 ~ CH15 (0) | 2024.03.19 |
---|---|
[쏙쏙 들어오는 함수형 코딩] CH12 ~ CH13 함수형 반복, 함수형 도구 체이닝 (0) | 2024.03.12 |
[쏙쏙 들어오는 함수형 코딩] CH6 ~ CH7 (얕은 복사, 깊은 복사) (0) | 2024.02.11 |
[쏙쏙 들어오는 함수형 코딩] - CH4 ~ CH5 (1) | 2024.02.04 |
[쏙쏙 들어오는 함수형 코딩] - CH1 ~ CH3 (1) | 2024.01.28 |