Jr. 개발자가 읽을 수있는 '영리한 사람'입니까? JS에서 너무 많은 기능적 프로그래밍? [닫은]


133

저는 Babel ES6로 코딩하는 선임 개발자입니다. 우리 앱의 일부는 API 호출을 만들고 API 호출에서 가져온 데이터 모델을 기반으로 특정 양식을 작성해야합니다.

이러한 양식은 이중 연결 목록에 저장됩니다 (백엔드에서 일부 데이터가 유효하지 않다고 표시하는 경우 사용자가 엉망인 한 페이지로 신속하게 돌아온 다음 대상을 다시 수정하면됩니다. 명부.)

어쨌든 페이지를 추가하는 데 사용되는 많은 기능이 있으며, 내가 너무 영리한지 궁금합니다. 이것은 기본적인 개요 일뿐입니다. 실제 알고리즘은 페이지와 페이지 유형이 다양하기 때문에 훨씬 더 복잡하지만 이에 대한 예를 제공합니다.

이것이 초보자 프로그래머가 처리하는 방법이라고 생각합니다.

export const addPages = (apiData) => {
   let pagesList = new PagesList(); 

   if(apiData.pages.foo){
     pagesList.add('foo', apiData.pages.foo){
   }

   if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

   if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

   return pagesList;
}

더 테스트하기 위해 모든 if 문을 가져 와서 분리하고 독립형 함수로 만든 다음 그 위에 매핑합니다.

이제 테스트 가능한 것이 한 가지이지만 읽을 수 있으므로 여기서 읽을 수없는 부분이 있는지 궁금합니다.

// file: '../util/functor.js'

export const Identity = (x) => ({
  value: x,
  map: (f) => Identity(f(x)),
})

// file 'addPages.js' 

import { Identity } from '../util/functor'; 

export const parseFoo = (data) => (list) => {
   list.add('foo', data); 
}

export const parseBar = (data) => (list) => {
   data.forEach((bar) => {
     list.add(bar.name, bar.data)
   }); 
   return list; 
} 

export const parseBaz = (data) => (list) => {
   data.forEach((baz) => {
      list.add(customBazParser(baz)); 
   })
   return list;
}


export const addPages = (apiData) => {
   let pagesList = new PagesList(); 
   let { foo, arrayOfBars: bars, customBazes: bazes } = apiData.pages; 

   let pages = Identity(pagesList); 

   return pages.map(foo ? parseFoo(foo) : x => x)
               .map(bars ? parseBar(bars) : x => x)
               .map(bazes ? parseBaz(bazes) : x => x)
               .value

}

여기 내 관심사가 있습니다. 에 바닥이 더 조직이다. 코드 자체는 작은 테스트 단위로 분리되어 테스트 할 수 있습니다. 그러나 나는 생각하고있다 : 나는 신원 펑터, 카레 또는 삼진 진술을 사용하는 것과 같은 개념에 익숙하지 않은 주니어 개발자로서 그것을 읽어야한다면 후자의 해결책이 무엇을하는지 이해할 수 있을까? 때때로 "잘못되고 쉬운"방법을 사용하는 것이 더 낫습니까?


13
JS에서 10 년만의 자기 교육을받은 사람으로서, 나는 나 자신을 주니어라고 생각하고 나를 잃어 버렸습니다Babel ES6
RozzA

26
OMG-1999 년부터 업계에서 활동했으며 1983 년부터 코딩을 해왔으며 가장 해로운 개발자입니다. "영리한"이라고 생각하는 것은 실제로 "비싸고", "유지하기 어렵다"및 "버그의 원인"이라고하며 비즈니스 환경에는 존재하지 않습니다. 첫 번째 예는 간단하고 이해하기 쉬우 며 작동 하는 반면 두 번째 예는 복잡하고 이해하기 어렵고 아마도 정확하지 않습니다. 이런 종류의 일을 중단하십시오. 실제 세계에 적용되지 않는 학문적 의미를 제외하고는 더 좋지 않습니다.
user1068

15
브라이언 커닝 한 (Brian Kerninghan)은 다음과 같이 인용하고 싶습니다. "처음에는 프로그램을 작성하는 것보다 디버깅이 두 배나 어렵다는 것을 알고 있습니다. " - en.wikiquote.org/wiki/Brian_Kernighan , 제 2 판, 2 장 / "프로그래밍 스타일의 요소"
MarkusSchaber

7
@Logister Coolness는 단순성 이상의 주요 목표가 아닙니다. 여기서 반대 의견은 무의미한 복잡성에 관한 것으로, 이는 코드가 추론하기 어렵고 예상치 못한 코너 케이스를 포함 할 가능성이 높기 때문에 정확성의 적입니다 (확실히 주요 관심사). 실제로 테스트하기가 더 쉽다는 주장에 대한 회의론을 감안할 때, 나는이 스타일에 대해 설득력있는 주장을 보지 못했습니다. 최소 권한 wrt 보안 규칙과 유사하게 간단한 언어 기능을 사용하여 간단한 작업을 수행하는 것에주의해야한다는 경험 법칙이있을 수 있습니다.
sdenham

6
코드는 주니어 코드처럼 보입니다. 나는 선배가 첫 번째 예를 쓸 것으로 기대합니다.
sed

답변:


322

코드에서 여러 가지 사항을 변경했습니다.

  • 의 필드에 액세스하도록 할당을 파괴하는 pages것은 좋은 변화입니다.
  • parseFoo()함수 등을 추출하는 것이 좋은 변화 일 수 있습니다.
  • 펑터를 도입하는 것은 매우 혼란 스럽다.

여기서 가장 혼란스러운 부분 중 하나는 기능적 프로그래밍과 명령형 프로그래밍을 혼합하는 방법입니다. functor를 사용하면 실제로 데이터를 변환하지 않고 다양한 기능을 통해 가변 목록 을 전달하는 데 사용합니다 . 그것은 매우 유용한 추상화처럼 보이지 않습니다, 우리는 이미 그것에 대한 변수를 가지고 있습니다. 추상화되어 있어야만하는 항목 (해당 항목이있는 경우에만 구문 분석)은 여전히 ​​코드에 명시되어 있지만 이제는 모퉁이를 생각해야합니다. 예를 들어, parseFoo(foo)함수를 반환하는 것은 다소 분명하지 않습니다. JavaScript에는 이것이 유효한지 알려주는 정적 유형 시스템이 없으므로 이러한 코드는 더 나은 이름 ( makeFooParser(foo)?) 없이 오류가 발생하기 쉽습니다 . 이 난독 화에는 아무런 이점이 없습니다.

내가 대신 기대하는 것 :

if (foo) parseFoo(pages, foo);
if (bars) parseBar(pages, bars);
if (bazes) parseBaz(pages, bazes);
return pages;

그러나 콜 사이트에서 항목이 페이지 목록에 추가되는지 확실하지 않기 때문에 이상적이지 않습니다. 대신 구문 분석 함수가 순수하고 페이지에 명시 적으로 추가 할 수있는 빈 목록을 반환하는 것이 좋습니다.

pages.addAll(parseFoo(foo));
pages.addAll(parseBar(bars));
pages.addAll(parseBaz(bazes));
return pages;

추가 이점 : 항목이 비어있을 때 수행 할 작업에 대한 논리가 이제 개별 구문 분석 기능으로 이동되었습니다. 이것이 적절하지 않은 경우에도 조건을 도입 할 수 있습니다. pages목록 의 변경 가능성 은 이제 여러 호출에 분산되는 대신 단일 함수로 통합됩니다. 로컬이 아닌 돌연변이를 피하는 것은 같은 재미있는 이름을 가진 추상화보다 기능 프로그래밍의 훨씬 더 큰 부분입니다 Monad.

예, 코드가 너무 영리했습니다. 영리한 코드를 작성하지 말고 현명한 영리함을 피할 수있는 영리한 방법을 찾아보십시오. 가장 좋은 디자인은하지 않는 모양 화려한 있지만 보는 사람에게 분명 본다. 그리고 프로그래밍을 단순화하기 위해 좋은 추상화가 있습니다. 먼저 풀어야 할 레이어를 추가하지 않아도됩니다 (여기서는 functor가 변수와 동일하고 효과적으로 제거 될 수 있음을 알아냅니다).

의심스러운 경우 코드를 간단하고 어리석게 유지하십시오 (KISS 원칙).


2
대칭의 관점에서 볼 때와 let pages = Identity(pagesList)다른 점은 무엇 parseFoo(foo)입니까? 감안할 때, 나는 아마 것 ... {Identity(pagesList), parseFoo(foo), parseBar(bar)}.flatMap(x -> x).
ArTs

8
(내 훈련받지 않은 눈에) 매핑 된 목록을 수집하기 위해 세 개의 중첩 된 람다 식을 갖는 것이 너무 영리 할 수 있다고 설명해 주셔서 감사합니다 .
Thorbjørn Ravn Andersen 2016

2
의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
yannis

어쩌면 유창한 스타일이 두 번째 예제에서 잘 작동합니까?
user1068

225

의심 스러우면 아마도 너무 영리 할 것입니다! 두 번째 예는 과 같은 표현식으로 우연히 복잡해foo ? parseFoo(foo) : x => x 지며 코드 전체가 더 복잡하므로 따르기가 더 어렵습니다.

청크를 개별적으로 테스트 할 수 있다는 장점은 개별 기능을 중단함으로써보다 간단한 방법으로 달성 할 수 있습니다. 두 번째 예에서는 별개의 반복을 결합하므로 실제로 격리 가 줄어 듭니다 .

일반적으로 기능적 스타일에 대한 느낌이 어떻든간에 이것은 코드를 더 복잡하게 만드는 예입니다.

간단하고 간단한 코드를 "초보 개발자"와 연결한다는 경고 신호가 있습니다. 이것은 위험한 사고 방식입니다. 내 경험상 그것은 정반대입니다. 초보 개발자는 가장 간단하고 명확한 솔루션을 볼 수있는 더 많은 경험이 필요하기 때문에 지나치게 복잡하고 영리한 코드를 사용하는 경향이 있습니다.

"영리한 코드"에 대한 조언은 실제로 코드가 초보자가 이해할 수없는 고급 개념을 사용하는지 여부에 관한 것이 아닙니다. 오히려 필요 이상으로 복잡하거나 복잡한 코드를 작성하는 것에 관한 입니다. 이로 인해 모든 사람 , 초보자 및 전문가 모두 가 코드를 따라 가기가 더 어려워지고 몇 달이 지나면 스스로 코드를 작성하기가 어려울 수 있습니다.


156
"초보 개발자는 가장 단순하고 명확한 솔루션을 볼 수있는 더 많은 경험이 필요하기 때문에 지나치게 복잡하고 영리한 코드를 사용하는 경향이 있습니다." 탁월한 답변!
Bonifacio

23
지나치게 복잡한 코드도 수동적입니다. 고의로 다른 사람은 쉽게 읽거나 디버깅 할 수없는 코드를 생성하고 있습니다. 이는 작업 보안을 의미하며 부재시 다른 모든 사람을 위해 완전히 지옥을 의미합니다. 기술 문서를 전적으로 라틴어로 작성할 수도 있습니다.
Ivan

14
나는 영리한 코드가 항상 과장된 것이라고 생각하지 않습니다. 때로는 자연스럽고 두 번째 검사에서만 어리석게 보입니다.

5
의도 한 것보다 더 판단력이있어 "보여주기"에 대한 문구를 제거했습니다.
JacquesB

11
@BaileyS-코드 검토의 중요성을 강조합니다. 코더에게 자연스럽고 솔직한 느낌, 특히 점진적으로 개발할 때 검토 자에게 쉽게 혼란스러워 보일 수 있습니다. 그런 다음 코드는 컨볼 루션을 제거하기 위해 리팩터링 / 재 작성 될 때까지 검토를 통과하지 않습니다.
Myles

21

이 답변은 조금 늦었지만 여전히오고 싶습니다. 최신 ES6 기술을 사용하거나 가장 인기있는 프로그래밍 패러다임을 사용한다고해서 반드시 코드가 더 정확하거나 주니어 코드 인 것은 아닙니다. 잘못되었습니다. 기능적 프로그래밍 (또는 다른 기술)은 실제로 필요할 때 사용해야합니다. 최신 프로그래밍 기술을 모든 문제에 적용 할 수있는 가장 작은 기회를 찾으려면 항상 과도하게 엔지니어링 된 솔루션을 얻게됩니다.

물러서서 잠시 해결하려는 문제를 말로 표현해보십시오. 본질적으로 addPages다른 부분을 apiData일련의 키-값 쌍으로 변환 한 다음 모든 부분을에 추가 하는 함수 가 필요합니다 PagesList.

그리고 그것이 전부라면 identity functionwith with ternary operator또는 functorinput parsing을 사용 하는 것이 왜 귀찮 습니까? 게다가 왜 목록을 변경하여 부작용functional programming 을 일으키는 적절한 방법이라고 생각 합니까? 필요한 모든 것이 왜 이것일까요?

const processFooPages = (foo) => foo ? [['foo', foo]] : [];
const processBarPages = (bar) => bar ? bar.map(page => [page.name, page.data]) : [];
const processBazPages = (baz) => baz ? baz.map(page => [page.id, page.content]) : [];

const addPages = (apiData) => {
  const list = new PagesList();
  const pages = [].concat(
    processFooPages(apiData.pages.foo),
    processBarPages(apiData.pages.arrayOfBars),
    processBazPages(apiData.pages.customBazes)
  );
  pages.forEach(([pageName, pageContent]) => list.addPage(pageName, pageContent));

  return list;
}

(실행 가능한 jsfiddle 여기 )

보다시피,이 접근법은 여전히을 사용 functional programming하지만 적당히 사용합니다. 또한 3 가지 변형 함수가 부작용을 일으키지 않기 때문에 테스트하기가 쉽지 않습니다. 이 코드는 addPages초보자 나 전문가가 한 눈에 알아볼 수있을 정도로 사소하고 무의미합니다.

자,이 코드를 위에서 생각 해낸 것과 비교해보십시오. 차이점이 있습니까? 의심 할 여지없이 functional programmingES6 구문은 강력하지만 이러한 기술을 사용하여 문제를 잘못 해결하면 더 복잡한 코드가 생길 수 있습니다.

문제에 몰두하지 않고 올바른 장소에 올바른 기술을 적용하는 경우 실제로 모든 팀 구성원이 매우 체계적으로 구성하고 유지 관리 할 수있는 기능적인 코드를 가질 수 있습니다. 이러한 특성은 상호 배타적이지 않습니다.


2
이 광범위한 태도를 지적하는 데 +1 (OP에는 반드시 적용되는 것은 아님) : "최신 ES6 기술을 사용하거나 가장 인기있는 프로그래밍 패러다임을 사용한다고해서 반드시 코드가 더 정확하다는 의미는 아닙니다. 또는 주니어 코드가 잘못되었습니다. "
Giorgio

+1. 작은 pedantic 발언이라면 _.concat 대신 spread (...) 연산자를 사용하여 해당 종속성을 제거 할 수 있습니다.
YoTengoUnLCD 2016 년

1
@YoTengoUnLCD 아, 잘 했어. 이제 나와 저의 팀은 여전히 ​​우리가 lodash사용하는 것을 배우지 않고 있습니다. 이 코드는 spread operator, 또는 코드 [].concat()의 모양을 그대로 유지하려는 경우 에도 사용할 수 있습니다 .
b0nyb0y

죄송하지만이 코드 목록은 OP 게시물의 원래 "주니어 코드"보다 훨씬 덜 명확합니다. 기본적으로 삼진 연산자를 피할 수 있으면 절대 사용하지 마십시오. 너무 긴장합니다. "실제"기능적 언어에서 if 문은 표현이 아니라 표현이므로 더 읽기 쉽습니다.
Olle Härstedt 2016 년

@ OlleHärstedt 음, 그것은 당신이 만든 흥미로운 주장입니다. 기능 프로그래밍 패러다임 또는 패러다임은 특정 "실제"기능적 언어와 관련이 없으며 구문이 훨씬 적습니다. 따라서 어떤 조건부 구성을 사용해야하는지 또는 "사용하지 않아야"하는지 지시하는 것은 의미가 없습니다. A ternary operatorif당신이 좋아하든 그렇지 않든 일반적인 진술 만큼이나 유효합니다 . 캠프 if-else?:캠프 사이의 가독성 토론은 끝이 없으므로 나는 그것에 들어 가지 않을 것입니다. 내가 말할 것은, 훈련 된 눈으로, 이와 같은 선은 거의 "너무 긴장하지 않다"는 것입니다.
b0nyb0y

5

두 번째 스 니펫은 첫 번째 스 니펫 보다 더 테스트 할 수 없습니다 . 두 스 니펫 중 하나에 필요한 모든 테스트를 설정하는 것이 합리적으로 간단합니다.

두 스 니펫의 실제 차이점은 이해력입니다. 첫 번째 스 니펫을 상당히 빠르게 읽고 무슨 일이 일어나고 있는지 이해할 수 있습니다. 두 번째 스 니펫은 그리 많지 않습니다. 훨씬 덜 직관적 일뿐만 아니라 훨씬 길다.

따라서 첫 번째 스 니펫을 유지 관리하기가 더욱 쉬워집니다. 이는 유용한 코드 품질입니다. 두 번째 스 니펫에서 가치가 거의 없습니다.


3

TD; DR

  1. 10 분 이내에 주니어 개발자에게 코드를 설명 할 수 있습니까?
  2. 2 개월 후 코드를 이해할 수 있습니까?

상세 분석

명확성과 가독성

원래 코드는 모든 수준의 프로그래머에게 매우 명확하고 이해하기 쉽습니다. 모두에게 친숙한 스타일 입니다.

가독성은 토큰의 수학적 계산이 아니라 친숙성을 기반으로 합니다. 이 단계에서 IMO는 재 작성에 너무 많은 ES6이 있습니다. 아마 몇 년 후에 나는이 부분을 내 대답으로 바꿀 것입니다. :-) BTW, 나는 또한 @ b0nyb0y 답변이 합리적이고 명확한 타협으로 좋아합니다.

테스트 가능성

if(apiData.pages.foo){
   pagesList.add('foo', apiData.pages.foo){
}

PagesList.add ()에 테스트가 있다고 가정하면이 코드는 완전히 간단한 코드이므로이 섹션에서 별도의 별도 테스트가 필요한 이유는 없습니다.

if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

다시 말하지만이 섹션에 대한 별도의 별도 테스트가 필요하지 않습니다. PagesList.add ()에 널 (null), 복제 또는 기타 입력에 특별한 문제가없는 한.

if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

이 코드는 매우 간단합니다. customBazParser테스트되었고 너무 많은 "특별한"결과를 반환하지 않는다고 가정합니다 . 다시 말해,`PagesList.add ()에 까다로운 상황이 아닌 한 (도메인에 익숙하지 않을 수 있음)이 섹션에 특별한 테스트가 필요한 이유는 알 수 없습니다.

일반적으로 전체 기능을 테스트하면 정상적으로 작동합니다.

면책 조항 : 세 가지 if()진술 중 8 가지 가능성을 모두 테스트 해야하는 특별한 이유가 있다면 테스트를 분할하십시오. 또는 PagesList.add()민감한 경우 테스트를 분할하십시오.

구조 : 갈리아와 같이 세 부분으로 나눌 가치가 있습니까?

여기에 가장 좋은 주장이 있습니다. 개인적으로, 나는 원래 코드가 "너무 길다"고 생각하지 않습니다 (SRP 광신자가 아닙니다). 그러나 몇 개의 if (apiData.pages.blah)섹션이 더 있으면 SRP가 뒤 떨어지고 머리를 찢을 가치가 있습니다. 특히 DRY가 적용되고 코드의 다른 위치에서 함수를 사용할 수있는 경우.

내 제안

YMMV. 한 줄의 코드와 일부 논리를 저장하기 위해 iflet 을 한 줄로 결합 할 수 있습니다 . 예 :

let bars = apiData.pages.arrayOfBars || [];
bars.forEach((bar) => {
   pagesList.add(bar.name, bar.data);
})

apiData.pages.arrayOfBars가 숫자 또는 문자열 인 경우 실패하지만 원래 코드도 실패합니다. 그리고 나에게 그것은 더 명확합니다 (과도한 관용구).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.