기능 분리

16757 단어
잠시 동안 나는 Clojure 프로그래밍 언어를 탐구해 왔습니다. 나는 거의 독점적으로 의 프레젠테이션에 의해 이 작업을 수행했습니다.

내가 가장 좋아하는 것 중 하나이며, 내가 그들을 본 이후로 내 마음에 불타오른 두 가지 특정 사항이 있습니다.

Design means break things apart in such a way that they can be put back together.



…그리고

Whenever there is a problem, it's likely because we haven't broken things apart enough.



그 비디오를 본 이후로 저는 코드의 일부를 볼 때마다 이것을 염두에 두었습니다. 그리고 n.2가 예상했던 것보다 훨씬 더 많이 적용된다는 것이 밝혀졌습니다.

이것은 상당히 큰 코드베이스에서 작업하는 동안 최근에 만난 리팩토링 기회에 대한 작은 연습입니다.
node 유형과 어딘가에서 토큰을 가져오는 함수가 있다고 가정하고 특정 경로가 정의된 경우 처리를 계속하기 전에 상위 노드를 가져와야 합니다.

type AppNode = {
  id: string;
  path: string;
};

declare function extractDataFromNode(n: AppNode): string;

async function getNodeData(
  startNode: AppNode,
  getToken: () => Promise<string>,
  getParentNode: (data: AppNode, token: string) => Promise<AppNode>
) {
  const token = await getToken();

  if (startNode.path === '/p') {
    const parent = await getParentNode(startNode, token);
    return extractDataFromNode(parent);
  }

  return extractDataFromNode(startNode);
}


이 기능은 아마 제 역할을 할 것입니다. 하지만 솔직히 말해서, 이것은 제 눈에는 엉망입니다.
  • 함수 이름이 노드 데이터를 반환한다고 주장합니다. 반면에 필요한 경우 시스템에서 인증 토큰을 검색하고 필요한 경우 상위 노드로 기꺼이 폴백합니다.
  • 이 함수에는 어떤 이유로 계산이 실패한 경우 데이터를 가져오는 데 사용되는 두 개의 추가 함수가 필요합니다.

  • n.2에 대한 이유를 묻거나 논쟁할 때 — 내가 일반적으로 받는 응답은 코드를 테스트할 때 모의 함수를 전달할 수 있기 때문에 이러한 "패턴"이 테스트 가능성을 향상시킨다는 것입니다.

    describe("returns parent data when path starts with /", () => {
      const tokenMock = jest.fn().mockResolvedValue("token");
      const parentNodeMock = jest.fn().mockResolvedValue({ id: "9", path: "main" });
    
      const data = getNodeData(
        { id: "0", path: "/s/10" },
        tokenMock,
        parentNodeMock
      );
    
      return expect(data).resolves.toEqual({}); // assertions
    });
    


    나는 이것이 유효한 주장이 아니라는 것을 알았다. 나에게 이것은 잘못 결합된 것을 고치는 테이프 패치 방식에 가깝습니다.

    Whenever there is a problem, it's likely because we haven't broken things apart enough.



    먼저 이 함수에서 토큰 논리를 이동하고 일반 인수로 전달합니다.

    async function getNodeData(
      startNode: AppNode,
      token: string,
      getParentNode: (data: AppNode, token: string) => Promise<AppNode>
    ) {
      if (startNode.path === "/p") {
        const parent = await getParentNode(startNode, token);
        return extractDataFromNode(parent);
      }
    
      return extractDataFromNode(startNode);
    }
    


    자체적으로 토큰을 가져오는 대신 토큰을 직접 요청하도록 함수 서명을 변경하여 이 함수에서 비순수 연산을 효과적으로 이동했습니다. 좋은 일입니다.

    이 단계에서 token 인수가 getParentNode 함수를 제공하기 위해 독점적으로 사용된다는 것을 알 수 있습니다. 이 함수는 인수로 전달됩니다.

    이에 대해 다음과 같이 할 수 있습니다.

    - getParentNode: (data: node, token: string) => Promise<node>
    + getParentNode: (token: string) => (data: AppNode) => Promise<AppNode>
    


    이제 토큰을 외부에서 바인딩하고 이러한 종속성을 제거할 수 있습니다.

    async function getNodeData(
      startNode: node,
      getParentNode: (data: AppNode) => Promise<node>
    ) {
      if (startNode.path === "/p") {
        const parent = await getParentNode(startNode);
        return extractDataFromNode(parent);
      }
    
      return extractDataFromNode(startNode);
    }
    


    ... 그런 다음 토큰 없는 함수를 호출합니다.

    const tokenizedGetParentNode = getParentNode("tokenValue");
    
    getNodeData(
      { id: "test", path: "/p/10" },
      tokenizedGetParentNode
    );
    


    코드는 이제 틀림없이 더 좋아졌습니다. 여전히, 우리는 getParentNode를 전달하고 있습니다 — 좋지 않습니다.

    위에서 getParentNode 함수에 적용한 것과 동일한 솔루션을 반복하여 부모 노드를 미리 전달할 수 있습니다. 하지만 리소스 낭비일 수 있습니다. 상위 노드는 특정 조건에서만 필요합니다.

    때로는 가독성을 위해 성능/효율성을 희생하는 것이 좋습니다. 그러나 이 사용 사례에서 우리는 부모 노드를 가져오는 것이 우리가 정말로 피하고 싶은 매우 광범위한 작업이라고 가정할 것입니다.

    이 함수를 분리하고 현재 노드에서만 작동하도록 범위를 줄여봅시다. 부모 노드가 필요한 경우 이를 실패로 만들고 호출자는 다음을 정렬해야 합니다.

    async function getNodeData(startNode: AppNode) {
      if (startNode.path === '/p') {
        return false;
      }
    
      return extractDataFromNode(startNode);
    }
    


    그런 다음 호출자는 부모 노드 호출이 필요한지 여부를 이해하기 위해 작업을 수행할 수 있습니다.

    우리는 이제 모든 조각이 하나의 작업을 수행하여 성공적으로 분해했습니다. 이제 그것들을 다시 조립할 시간입니다.

    declare function getParentNode: (data: AppNode) => RTE.ReaderTaskEither<string, Error, AppNode>;
    declare function extractDataFromNode(n: AppNode): string;
    
    function getNodeData(startNode: AppNode) {
      if (startNode.path === "/p") {
        return E.left(new Error("parent node required"));
      }
    
      return E.right(extractDataFromNode(startNode));
    }
    
    const getParentNodeData = (startNode: AppNode) =>
      pipe(getParentNode(startNode), RTE.map(extractDataFromNode))(token);
    
    const node: AppNode = { id: "10", path: "/path" };
    
    return pipe(
      TE.fromEither(getNodeData(node)),
      TE.orElse(() => getParentNodeData(node))
    );
    


    이제 가서 기능을 분리하십시오.

    그런 다음 시스템을 분해하십시오.

    그런 다음 다시 함께 넣습니다.

    좋은 웹페이지 즐겨찾기