“하나의 일”패러다임은 언제 해롭습니까?


21

인수를 위해 주어진 파일의 내용을 한 줄씩 인쇄하는 샘플 함수가 있습니다.

버전 1 :

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  string line;
  while (std::getline(file, line)) {
    cout << line << endl;
  }
}

함수가 한 추상화 레벨에서 한 가지 작업을 수행하는 것이 좋습니다. 위 코드는 거의 한 가지 일이지만 상당히 원자 적입니다.

Robert C. Martin의 Clean Code와 같은 일부 책은 위의 코드를 별도의 기능으로 나누는 것을 제안하는 것 같습니다.

버전 2 :

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  printLines(file);
}

void printLines(fstream & file) {
  string line;
  while (std::getline(file, line)) {
    printLine(line);
  }
}

void printLine(const string & line) {
  cout << line << endl;
}

나는 그들이 달성하고자하는 것을 이해하지만 (파일 열기 / 줄 읽기 / 인쇄 줄) 약간 과잉되지 않습니까?

원래 버전은 간단하고 어떤 의미에서는 이미 한 가지 작업을 수행합니다. 파일을 인쇄합니다.

두 번째 버전은 첫 번째 버전보다 훨씬 덜 읽기 쉬운 많은 기능을 제공합니다.

이 경우 코드를 한 곳에 두는 것이 낫지 않습니까?

"하나의 일"패러다임은 어느 시점에서 해로운가?


13
이러한 종류의 코딩 실습은 항상 경우에 따라 다릅니다. 단일 접근 방식은 없습니다.
iammilind

1
@Alex-허용되는 답변은 문자 그대로 질문과 관련이 없습니다. 정말 이상하다고 생각합니다.
ChaosPandion

2
리팩토링 된 버전이 거꾸로되어 가독성이 떨어집니다. 파일 아래로 읽기, 당신은 보여야하는데 printFile, printLines마지막으로하고 printLine.
Anthony Pegram

1
@ 케빈, 나는 다시 한번, 특히 그 분류에 동의하지 않을 수 있습니다. 그것은 pedantry가 아니라 요점입니다! 두 번째 버전을 읽을 수 없을 수도 있다고 구체적으로 말한 것은 OP입니다. 특히 클린 코드를 두 번째 버전에 대한 영감으로 인용하는 것은 OP입니다. 필자의 의견은 본질적으로 Clean Code가 그런 식으로 코드를 작성 하지 못하게한다는 것입니다. 순서는 실제로 가독성을 위해 중요합니다. 신문 기사를 읽는 것처럼 파일을 읽고 기본적으로 무관심해질 때까지 점점 더 자세히 설명합니다.
Anthony Pegram

1
시를 거꾸로 읽을 것으로 기대하지 않는 것처럼, 특정 클래스 내에서 가장 낮은 수준의 세부 사항을 가장 먼저 볼 것으로 기대하지는 않습니다. 요컨대, 코드는 신속하게 정렬하는 데 약간의 시간 걸리지만이 코드는 그가 작성하려는 유일한 코드가 아니라고 가정합니다. 내 생각에, 그가 Clean Code를 인용한다면, 최소한 그가 할 수있는 것은 그것을 따르는 것입니다. 코드의 순서가 맞지 않으면 다른 코드보다 읽기 어려울 것입니다.
Anthony Pegram

답변:


15

물론, 이것은 단지 질문을 구걸한다 "란 하나의 일을?" 한 줄을 읽고 다른 줄을 쓰고 있습니까? 아니면 한 스트림에서 다른 스트림으로 한 줄을 복사하여 한 가지로 간주합니까? 아니면 파일을 복사합니까?

그것에 대한 객관적인 답은 없습니다. 그것은 당신에게 달려 있습니다. 결정할 수 있습니다. 당신이 해야 할 결정합니다. "한 가지 일"패러다임의 주요 목표는 가능한 한 이해하기 쉬운 코드를 작성하는 것이므로이를 지침으로 사용할 수 있습니다. 불행히도 이것은 객관적으로도 측정 할 수 없으므로 장감과 "WTF" 에 의존해야합니다. 코드 검토에 포함 됩니다.

IMO 한 줄의 코드로만 구성된 함수는 그다지 문제가되지 않습니다. 귀하는 printLine()사용 아무런 장점이없는 std::cout << line << '\n'일을 직접. 내가 보이면 printLine()이름이 말한 것을 수행한다고 가정하거나 찾아보고 확인해야합니다. 내가 볼 경우 std::cout << line << '\n', 문자열 내용을에 대한 행으로 출력하는 표준 방법이기 때문에 그 기능을 즉시 알 수 std::cout있습니다.

그러나 패러다임의 또 다른 중요한 목표는 코드 재사용을 허용하는 것이며, 이는 훨씬 더 객관적인 조치입니다. 예를 들어, 두 번째 버전 에서는 한 스트림에서 다른 스트림으로 라인을 복사하는 보편적으로 유용한 알고리즘이되도록 쉽게 작성할 printLines() 있습니다.

void copyLines(std::istream& is, std::ostream& os)
{
  std::string line;
  while( std::getline(is, line) );
    os << line << '\n';
  }
}

이러한 알고리즘은 다른 상황에서도 재사용 될 수 있습니다.

그런 다음이 유스 케이스에 특정한 모든 것을이 일반 알고리즘을 호출하는 함수에 넣을 수 있습니다.

void printFile(const std::string& filePath) {
  std::ifstream file(filePath.c_str());
  printLines(file, std::cout);
}

1 대신에 사용했습니다 . 개행 문자를 출력하는 기본 선택해야한다 , 홀수 경우입니다 .'\n'std::endl'\n'std::endl


2
+1-나는 대부분 동의하지만, "직감"보다 더 많은 것이 있다고 생각합니다. 문제는 사람들이 구현 세부 사항을 세어 "한 가지"를 판단 할 때입니다. 나에게 함수는 하나의 명확한 추상화를 구현해야합니다. 함수 이름을 "do_x_and_y"로 지정해서는 안됩니다. 구현은 여러 가지 (더 간단한) 작업을 수행 할 수 있고 수행해야합니다. 이러한 간단한 작업은 각각 더 간단한 작업으로 분해 될 수 있습니다. 추가 규칙을 사용하면 기능 분해가 가능합니다. 함수 (및 해당 이름)는 각각 하나의 명확한 개념 / 작업 / 무엇을 설명해야합니다.
Steve314

@ Steve314 : 구현 세부 정보를 가능성으로 나열하지 않았습니다. 한 스트림에서 다른 스트림으로 라인을 복사하는 것은 하나의 추상화입니다. 아니면? 대신 do_x_and_y()함수의 이름을 지정하면 피할 수 do_everything()있습니다. 그렇습니다. 어리석은 예이지만이 규칙이 나쁜 설계의 가장 극단적 인 예를 막지 못한다는 것을 보여줍니다. IMO이 있다 규칙에 의해 결정 한만큼 직감 결정은. 그렇지 않으면 객관적인 경우이를 수행 할 수없는 메트릭을 얻을 수 있습니다.
sbi

1
나는 모순을 의도하지 않았으며 추가를 제안하기 만했습니다. 내가 말한 것을 잊어 버린 것은 질문에서 분해 printLine등이 유효하다는 것입니다. 각각 하나의 추상화이지만 필요한 것은 아닙니다. printFile이미 "한 가지"입니다. 이를 3 개의 개별 하위 레벨 추상화로 분해 할 수 있지만 가능한 모든 추상화 레벨 에서 분해 할 필요는 없습니다 . 각 함수는 "한 가지"를 수행해야하지만 가능한 모든 "한 가지"가 함수일 필요는 없습니다. 호출 그래프로 너무 많은 복잡성을 옮기는 것은 문제가 될 수 있습니다.
Steve314

7

기능이 "한 가지"만하는 것은 하나님의 계명이 아니라 두 가지 바람직한 목적을위한 수단입니다.

  1. 함수가 "한 가지"만을 수행하는 경우 코드 복제 및 API 부풀림을 피하는 데 도움이됩니다. 함수를 구성하여 더 높은 수준의 구성이 어려운 함수의 조합 폭발 대신 더 복잡한 작업을 수행 할 수 있기 때문입니다. .

  2. 기능이 "한 가지"만하 것은 코드를 더 읽기 쉽게. 이것은 사물을 분리 할 수있는 구조의 세부, 간접적, 개념적인 오버 헤드로 잃어 버리는 것보다 사물을 분리함으로써보다 명확하고 추론의 용이성을 얻는 지 여부에 달려 있습니다.

따라서 "한 가지"는 불가피하게 주관적이며 프로그램과 관련된 추상화 수준에 따라 다릅니다. 만약printLines단일의 기본 작업으로 생각하거나 관심을 보거나 돌보는 선을 인쇄하는 유일한 방법으로 생각 목적을 위해 printLines한 가지만 수행하십시오. 두 번째 버전을 더 읽을 수 없다면 (첫 번째 버전은 괜찮습니다).

당신이 낮은 추상화 레벨에 더 많은 제어를 필요로하는 미묘한 복제 및 조합 폭발과 함께 끝나는 (즉, 시작하는 경우 printLines파일 이름에 대한과 완전히 분리 printLines에 대한 fstreamA, 객체 printLines콘솔과 printLines다음 파일) printLines수준에서 두 개 이상의 일을하고 당신이 걱정하는 추상화의.


세 번째를 추가하면 더 작은 기능이 더 쉽게 테스트됩니다. 함수가 한 가지 작업 만 수행하면 입력이 더 적을 수 있으므로 독립적으로 테스트하기가 더 쉽습니다.
PersonalNexus

@ PersonalNexus : 테스트 문제에 다소 동의하지만 IMHO는 구현 세부 사항을 테스트하는 것은 바보입니다. 나에게 단위 테스트는 내 대답에 정의 된 "한 가지"를 테스트해야합니다. 세분화 된 것은 테스트 구현을 변경하기 때문에 (구현 세부 사항을 변경하면 테스트 변경이 필요하기 때문에) 코드가 성가신 장황하고 간접적 인 것입니다 (테스트를 지원하기 위해 간접적으로 추가하기 때문에).
dsimcha

6

이 규모에서는 중요하지 않습니다. 단일 기능 구현은 완벽하고 명백합니다. 그러나 조금 더 복잡성을 추가하면 반복을 작업에서 분리하는 것이 매우 매력적입니다. 예를 들어, "* .txt"와 같은 패턴으로 지정된 파일 세트에서 행을 인쇄해야한다고 가정하십시오. 그런 다음 반복을 작업과 분리합니다.

printLines(FileSet files) {
   files.each({ 
       file -> file.eachLine({ 
           line -> printLine(line); 
       })
   })
}

이제 파일 반복을 개별적으로 테스트 할 수 있습니다.

테스트를 단순화하거나 가독성을 높이기 위해 함수를 분할했습니다. 각 데이터 라인에서 수행 된 작업이 주석을 보증 할 정도로 복잡하다면,이를 별도의 함수로 분리 할 것입니다.


4
나는 당신이 그것을 못 박았다 생각합니다. 라인을 설명하기 위해 주석이 필요하다면 항상 메소드를 추출 할 때입니다.
Roger CS Wernersson

5

설명이 필요하다고 생각 될 때 메소드를 추출하십시오.

그들의 이름이 분명한 방식으로 만 행동하거나 영리하게 명명 된 방법을 호출하여 이야기를하는 방법을 작성하십시오.


3

간단한 경우에도 단일 책임 원칙이 더 나은 관리에 도움이되는 세부 정보가 누락되었습니다. 예를 들어 파일을 열 때 문제가 발생하면 어떻게됩니까? 예외 처리에서 파일 액세스 에지 사례를 강화하기 위해 추가하면 7-10 줄의 코드가 함수에 추가됩니다.

파일을 연 후에도 안전하지 않습니다. 메모리가 부족할 수 있습니다 (특히 네트워크의 파일 인 경우). 메모리가 부족할 수 있습니다. 다시 강화해야 할 여러 가지 경우가 발생할 수 있으며 모 놀리 식 기능을 부 풀릴 수 있습니다.

한 줄짜리 프린트 라인은 무해한 것처럼 보입니다. 그러나 새로운 기능이 파일 프린터에 추가됨에 따라 (텍스트 구문 분석 및 서식 지정, 다른 종류의 디스플레이로 렌더링 등) 그 기능이 커지고 나중에 감사하게 될 것입니다.

SRP의 목표는 한 번에 하나의 작업에 대해 생각할 수 있도록하는 것입니다. 마치 큰 텍스트 블록을 여러 단락으로 나누면 독자가 이해하려는 요점을 이해할 수 있습니다. 이러한 원칙을 준수하는 코드를 작성하는 데 시간이 조금 더 걸립니다. 그러나 그렇게하면 해당 코드를보다 쉽게 ​​읽을 수 있습니다. 코드에서 버그를 추적하고 깔끔하게 분할 된 것을 발견 할 때 미래의 자아가 얼마나 행복 할 지 생각해보십시오.


2
동의하지 않더라도 논리가 마음에 들기 때문에이 답변을 찬성했습니다. 미래에 일어날 수있는 일에 대한 복잡한 생각을 바탕으로 구조를 제공하는 것은 역효과를 낳습니다. 필요할 때 코드를 분해하십시오. 필요할 때까지 추상화하지 마십시오. 현대의 코드는 작동하는 코드를 작성하고 마지 못해 적용하는 대신에 노예로 규칙을 따르려고하는 사람들에 시달리고 있습니다. 좋은 프로그래머는 게으르다 .
트릴

의견 주셔서 감사합니다. 참고 나는 조기 Ababculation을 옹호하지 않고 논리 연산을 나누기 만하면 나중에 쉽게 할 수 있습니다.
Michael Brown

2

나는 개인적으로 후자의 접근 방식을 선호한다. 왜냐하면 그것은 미래의 작업을 저장하고 "일반적인 방식으로하는 방법"사고 방식을 강요하기 때문이다. 그럼에도 불구하고 버전 1이 버전 2보다 낫습니다. 버전 2로 해결 된 문제가 너무 사소하고 fstream에 따라 다르기 때문입니다. 다음과 같은 방법으로 수행해야한다고 생각합니다 (Nawaz가 제안한 버그 수정 포함).

일반 유틸리티 기능 :

void printLine(ostream& output, const string & line) { 
    output << line << endl; 
} 

void printLines(istream& input, ostream& output) { 
    string line; 
    while (getline(input, line)) {
        printLine(output, line); 
    } 
} 

도메인 별 기능 :

void printFile(const string & filePath, ostream& output = std::cout) { 
    fstream file(filePath, ios::in); 
    printLines(file, output); 
} 

이제 printLines와 뿐만 아니라 모든 스트림과 printLine함께 사용할 수 있습니다 fstream.


2
동의하지 않습니다. 그 printLine()기능은 가치가 없습니다. 내 답변을 참조하십시오 .
sbi

1
printLine ()을 유지하면 줄 번호 나 구문 색상을 추가하는 데코레이터를 추가 할 수 있습니다. 그 말을했지만, 이유를 찾을 때까지 그 방법을 추출하지 않을 것입니다.
Roger CS Wernersson

2

모든 패러다임반드시 따라야 할 은 약간의 훈련이 필요하므로 "언론의 자유"를 줄이면 초기 오버 헤드가 발생합니다 (적어도 그것을 배워야하기 때문입니다). 이런 의미에서 모든 패러다임은 패러다임이 자체적으로 유지하도록 설계된 이점에 의해 오버 헤드 비용이 지나치게 보상되지 않을 때 해로울 수 있습니다.

따라서이 질문에 대한 진정한 대답은 다음과 같이 미래를 "예견"할 수있는 능력이 필요합니다.

  • 나는 지금 해야 A하고B
  • 확률은에, 무엇 가까운 미래 또한 할 필요합니다 A-B+(즉, A와 B, 그러나 다만 조금 다른 같은 모습, 즉 뭔가를)?
  • 더 먼 미래에 A +가 A*또는 될 확률은 A*-얼마입니까?

그 확률이 ​​상대적으로 높으면 A와 B에 대해 생각할 때 가능한 변형에 대해 생각하여 공통 부분을 분리하여 재사용 할 수있는 좋은 기회가 될 것입니다.

그 확률이 ​​매우 낮 으면 (주변 변형 A은 본질적으로 A그 자체가 아닙니다 ) A를 분해하는 방법을 연구하십시오.

예를 들어,이 실제 이야기를 들려 드리겠습니다.

교사로서의 과거 생애 동안, 나는 대부분의 학생 프로젝트에서 거의 모든 것이 C 문자열의 길이계산하는 자신의 기능을 제공한다는 것을 발견했습니다 .

약간의 조사를 거쳐 빈번한 문제로 모든 학생이 그 기능을 사용하려고한다는 것을 알게되었습니다. 그들에게 라이브러리 기능이 있다고 말한 후 (strlen )에 , 많은 사람들이 문제가 너무 간단하고 사소하기 때문에 C 라이브러리 매뉴얼을 찾는 것보다 자신의 함수 (2 줄의 코드)를 작성하는 것이 더 효과적이라고 대답했습니다. (1984 년, WEB 및 Google을 잊어 버렸습니다!)이 알파벳순으로 준비되어 있는지 확인하십시오.

이것은 효과적인 휠 카탈로그없이 "바퀴를 재발 명하지 마십시오"라는 패러다임이 해로울 수있는 예입니다!


2

어제 특정 작업을 수행하는 데 필요한 폐기 도구에 사용하는 것이 좋습니다. 또는 관리자가 직접 제어하는 ​​관리 도구입니다. 이제 고객에게 적합하도록 강력하게 만드십시오.

의미있는 메시지로 적절한 오류 / 예외 처리를 추가하십시오. 기존 파일이 아닌 파일을 처리하는 방법 등 의사 결정을 포함하여 매개 변수 확인이 필요할 수 있습니다. 정보 및 디버깅과 같은 다른 수준의 로깅 기능을 추가하십시오. 팀 동료가 무슨 일이 일어나고 있는지 알 수 있도록 의견을 추가하십시오. 일반적으로 간결하게 생략하고 코드 예제를 제공 할 때 독자를위한 연습으로 남겨둔 모든 부분을 추가하십시오. 단위 테스트를 잊지 마십시오.

여러분의 좋은 꽤 선형 약간의 기능이 갑자기 복잡한 혼란에 끝나는 돌려서는 별도의 함수로 갈라한다.


2

IMO 기능이 더 이상 아무것도 수행하지 않고 다른 기능에 작업을 위임한다는 것은 멀어지면 해 롭습니다. 왜냐하면 더 이상 아무것도 추상화되지 않으며 그러한 기능을 이끌어내는 사고 방식은 항상 위험에 처하기 때문입니다. 더 나쁜 일을 ...

원래 게시물에서

void printLine(const string & line) {
  cout << line << endl;
}

충분한 지식을 가지고 있다면 printLine은 여전히 ​​두 가지 일을합니다. cout 줄을 작성하고 "end line"문자를 추가하는 것입니다. 어떤 사람들은 새로운 기능을 만들어서 처리하고 싶을 수도 있습니다.

void printLine(const string & line) {
  reallyPrintLine(line);
  addEndLine();
}

void reallyPrintLine(const string & line) {
  cout << line;
}

void addEndLine() {
  cout << endl;
}

아뇨, 이제 우리는 문제를 더욱 악화 시켰습니다! 이제 printLine이 두 가지 일을하는 것은 명백합니다! 1! 행을 인쇄하는 것은 행 자체를 인쇄하고 행 끝 문자를 추가하는 것으로 구성되는 불가피한 문제를 제거하기 위해 상상할 수있는 가장 터무니없는 "해결 방법"을 만드는 것은 그리 어리석지 않습니다.

void printLine(const string & line) {
  for (int i=0; i<2; i++)
    reallyPrintLine(line, i);
}

void reallyPrintLine(const string & line, int action) {
  cout << (action==0?line:endl);
}

1

짧은 대답은 ... 그것은 달려 있습니다.

이것에 대해 생각해보십시오. 장래에 표준 출력으로 만 인쇄하지 않고 파일로 인쇄하려면 어떻게해야합니까?

나는 YAGNI가 무엇인지 알고 있지만, 일부 구현이 필요하다고 알려져 있지만 연기 된 경우가있을 수 있습니다. 따라서 설계자 또는 해당 기능을 알고있는 사람은 파일로 인쇄 할 수 있어야하지만 지금은 구현을 원하지 않습니다. 따라서 그는이 추가 기능을 생성하므로 나중에 출력을 한 곳에서만 변경하면됩니다. 맞는 말이다?

그러나 콘솔에서 출력 만 필요하다는 것이 확실하다면 실제로 의미가 없습니다. "래퍼"를 작성하는 cout <<것은 쓸모없는 것 같습니다.


1
그러나 엄밀히 말하면 printLine 함수는 라인을 반복하는 것과 다른 추상화 수준이 아닌가?

@Petr 그렇게 생각합니다. 기능을 분리하는 것이 좋습니다. 나는 그 개념이 맞다고 생각하지만 사례별로 적용해야합니다.

1

"한 가지 일"의 미덕에 관한 장을 다루는 책이있는 모든 이유는 여전히 4 페이지 길이의 함수를 작성하는 개발자가 여전히 존재하기 때문입니다. 코드가 단순하고 명확하면 올바르게 수행 한 것입니다.


0

다른 포스터가 언급했듯이 한 가지 일을하는 것은 규모의 문제입니다.

또한 One Thing 아이디어는 사람들이 부작용으로 코딩하는 것을 막는 것이라고 제안합니다. 이는 '올바른'결과를 얻기 위해 특정 순서로 메소드를 호출해야하는 순차 결합으로 표시 됩니다.

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