분석법 추출과 기본 가정


27

큰 방법 (또는 절차 또는 함수)을 나눌 때이 질문 OOP에만 국한된 것이 아니지만 OOP 언어로 99 % 작업을하므로 가장 편한 용어입니다. , 나는 종종 결과에 불만을 느낀다. 큰 메소드에서 코드 블록 일 때보 다 작은 메소드에 대해 추론하기가 더 어려워집니다. 추출 할 때 호출자의 컨텍스트에서 오는 많은 기본 가정을 잃기 때문입니다.

나중에이 코드를보고 개별 메서드를 볼 때 해당 메서드의 위치를 ​​즉시 알 수 없으며 파일의 어느 곳에서나 호출 할 수있는 일반적인 개인 메서드로 생각합니다. 예를 들어, 초기화 방법 (생성자 또는 다른 방법)이 일련의 작은 것으로 나뉘어 있다고 상상해보십시오. 메소드 자체의 상황에서 객체의 상태가 여전히 유효하지 않다는 것을 분명히 알고 있지만 일반적인 개인용 메소드에서는 아마도 해당 객체를 가정했을 것입니다. 이미 초기화되었으며 유효한 상태입니다.

내가 본 유일한 해결책 where은 Haskell 의 절로, "부모"함수에서만 사용되는 작은 함수를 정의 할 수 있습니다. 기본적으로 다음과 같습니다.

len x y = sqrt $ (sq x) + (sq y)
    where sq a = a * a

그러나 내가 사용하는 다른 언어에는 이와 같은 것이 없습니다. 가장 가까운 것은 로컬 범위에서 람다를 정의하는 것입니다.

그래서 제 질문은 – 당신은 이것을 경험하고 있고, 이것이 문제라고 생각합니까? 그렇다면, 특히 Java / C # / C ++와 같은 "주류"OOP 언어에서 일반적으로 어떻게 해결합니까?

중복에 대한 편집 : 다른 사람들이 알았 듯이 이미 분할 방법과 작은 질문에 대한 질문이 하나 있습니다. 나는 그것들을 읽었으며, 호출자의 컨텍스트 (예 : 위의 객체가 초기화되고 있음)에서 파생 될 수있는 기본 가정 의 문제에 대해서는 논의하지 않습니다 . 그것이 내 질문의 요점이며, 내 질문이 다른 이유입니다.

업데이트 : 이 질문과 아래에 토론을했다면 John Carmack 이이 기사 를 특히 좋아할 것입니다 .

실제 코드 실행에 대한 인식 외에도 인라인 함수는 다른 위치에서 함수를 호출 할 수 없도록하는 이점도 있습니다. 어리석게 들리지만 요점이 있습니다. 코드베이스가 수년에 걸쳐 사용됨에 따라 바로 가기를 수행하고 필요한 작업 만 수행하는 함수를 호출 할 수있는 기회가 많이 있습니다. PartialUpdateA () 및 PartialUpdateB ()를 호출하는 FullUpdate () 함수가있을 수 있지만, 일부 경우 PartialUpdateB () 만하면된다는 사실을 깨닫거나 생각할 수 있으며 다른 것을 피함으로써 효율적입니다 작업. 많은 버그가 이것에서 비롯됩니다. 대부분의 버그는 실행 상태가 정확하게 생각한 결과가 아닌 결과입니다.




@ 질문을하지 않으면 서 함수를 추출할지 여부를 논의합니다. 대신 가장 적합한 방법에 의문을 제기합니다.
Max Yankov

2
@ gnat 거기에 연결된 다른 관련 질문이 있지만이 코드는 호출자의 컨텍스트에서만 유효한 특정 가정에 의존 할 수 있다는 사실에 대해서는 논의하지 않습니다.
Max Yankov

1
내 경험에서 @Doval은 실제로 그렇게합니다. 설명하는 것처럼 귀찮은 도우미 메서드가있는 경우 새 응집력있는 클래스를 추출하면 이를 처리합니다.
gnat

답변:


29

예를 들어, 초기화 방법이 일련의 작은 것으로 나뉘어져 있다고 상상해보십시오. 방법 자체의 맥락에서 객체의 상태가 여전히 유효하지 않다는 것을 분명히 알고 있지만 일반적인 개인 방법에서는 객체가 이미 초기화되어 있다고 가정합니다. 유효한 상태입니다. 내가 본 유일한 해결책은 ...

당신의 관심사는 잘 정립되어 있습니다. 다른 해결책이 있습니다.

물러나 방법의 목적은 무엇입니까? 메소드는 다음 두 가지 중 하나만 수행합니다.

  • 가치 창출
  • 효과를 일으키다

또는 불행히도 둘 다. 나는 두 가지 방법 모두를 피하려고 노력하지만 많은 방법을 사용합니다. 생성 된 효과 또는 생성 된 가치가 방법의 "결과"라고 가정 해 봅시다.

메소드는 "컨텍스트"로 호출됩니다. 그 맥락은 무엇입니까?

  • 인수의 가치
  • 메소드 외부의 프로그램 상태

본질적으로 당신이 지적하는 것은 : 메소드 결과의 정확성은 그것이 호출되는 컨텍스트에 달려 있습니다.

우리는 호출 하는 방법 본체가 올바른 결과를 생산하는 방법에 대해 시작하기 전에 필요한 조건을전제 조건을 , 우리는 전화 메소드 본문 반환 후 생산 될 것이다 조건사후을 .

따라서 본질적으로 지적한 것은 코드 블록을 자체 메소드로 추출하면 전제 조건 및 사후 조건에 대한 컨텍스트 정보가 손실 됩니다.

이 문제에 대한 해결책 은 프로그램에서 사전 조건과 사후 조건을 명시 적으로 만드는 것 입니다. 예를 들어 C #에서 Debug.Assert또는 Code Contracts를 사용 하여 사전 조건과 사후 조건을 표현할 수 있습니다 .

예를 들면 : 여러 컴파일 단계를 거친 컴파일러 작업을했습니다. 먼저 코드를 분석 한 다음 파싱 한 다음 유형을 확인한 다음 상속 계층 구조를 확인하여주기 등을 수행합니다. 코드의 모든 비트는 컨텍스트에 매우 민감했습니다. 예를 들어 "이 유형이 해당 유형으로 변환 가능한가?"라고 묻는 것은 비참한 일입니다. 기본 유형의 그래프가 아직 비순환 인 것으로 알려지지 않은 경우! 따라서 모든 코드는 사전 조건을 명확하게 문서화했습니다. 우리 assert는 이미 "기본 유형 acylic"검사를 통과 한 형식 변환 가능성을 검사 한 다음 메서드를 호출 할 수있는 위치와 호출 할 수없는 위치를 독자에게 명확하게 알 수있었습니다.

물론 우수한 분석법 설계는 식별 한 문제를 완화하는 방법이 많이 있습니다.

  • 효과 나 가치에 유용하지만 둘 다에 유용한 방법은 아닙니다.
  • 가능한 한 "순수한"방법을 만드십시오; "순수한"방법은 인수 에만 의존하는 값을 생성하며 아무런 영향을 미치지 않습니다. 필요한 "컨텍스트"가 매우 현지화되어 있기 때문에 추론하기 가장 쉬운 방법입니다.
  • 프로그램 상태에서 발생하는 돌연변이의 양을 최소화하고; 돌연변이는 코드가 추론하기 어려워지는 지점입니다.

전제 조건 / 사후 조건의 관점에서 문제를 설명하는 답인 +1.
QuestionC

5
필자는 사전 및 사후 조건 검사를 유형 시스템에 위임하는 것이 종종 가능하고 좋은 아이디어라고 덧붙입니다. a를 가져 와서 string데이터베이스에 저장 하는 기능이 있으면 정리를 잊어 버린 경우 SQL 삽입의 위험이 있습니다. 반면에 함수가을 사용하고을 SanitisedString얻는 유일한 방법 SantisiedString은을 호출하는 Sanitise것이라면 구성을 통해 SQL 주입 버그를 배제했습니다. 컴파일러가 잘못된 코드를 거부 할 수있는 방법을 찾고 있습니다.
Benjamin Hodgson

+1 중요한 것은 큰 방법을 작은 덩어리로 나누는 데 비용이 든다는 것입니다. 전제 조건과 사후 조건이 원래보다 느슨해지지 않으면 일반적으로 유용하지 않습니다. 이미 수행 한 점검을 다시 수행하여 비용을 지불하십시오. 완전히 "무료"리팩토링 프로세스는 아닙니다.
Mehrdad

"그 맥락은 무엇입니까?" 명확히하기 위해, 나는이 메소드가 호출되는 객체의 개인 상태를 주로 의미했습니다. 두 번째 범주에 포함 된 것 같습니다.
Max Yankov

이것은 훌륭하고 생각을 자극하는 답변입니다. 감사합니다. (물론 다른 대답이 나쁘다는 것은 말할 것도 없습니다). 나는 여기서 토론을 정말로 좋아하기 때문에 (답이 답변으로 표시되면 중단되는 경향이 있음) 질문을 처리하고 생각할 시간이 필요하기 때문에 아직 질문에 답변 된 것으로 표시하지 않습니다.
Max Yankov

13

나는 종종 이것을보고 문제라는 것에 동의한다. 일반적으로 메소드 객체 를 작성하여 문제를 해결 합니다 . 멤버가 너무 큰 원래 메소드의 로컬 변수 인 새 특수 클래스입니다.

새로운 클래스는 'Exporter'또는 'Tabulation'과 같은 이름을 갖는 경향이 있으며 더 큰 컨텍스트에서 특정 작업을 수행하는 데 필요한 정보가 전달됩니다. 그 다음은 무엇에 사용되는없이 위험에 더 작은 도우미 코드 조각 자유롭게 정의 할 수있다 하지만, 도표 작성 또는 수출을.


나는이 생각이 내가 생각할수록 더 좋아한다. 공개 또는 내부 클래스 내의 개인 클래스 일 수 있습니다. 매우 로컬에만 관심이있는 클래스로 네임 스페이스를 복잡하게 만들지 않으며 "constructor helpers"또는 "parse helpers"또는 무엇이든 표시 할 수 있습니다.
Mike는 Monica를 지원합니다

최근에는 아키텍처 관점에서 이상적인 상황에 처했습니다. 렌더러 클래스와 공개 렌더 메소드로 소프트웨어 렌더러를 작성했는데, 여기에는 다른 메소드를 호출하는 데 사용되는 컨텍스트가 많이있었습니다. 이를 위해 별도의 RenderContext 클래스를 만들려고했지만 매 프레임마다이 프로젝트를 할당하고 할당 해제하는 것은 엄청나게 낭비 적이었습니다. github.com/golergka/tinyrenderer/blob/master/src/renderer.h
Max Yankov

6

많은 언어를 사용하면 Haskell과 같은 함수를 중첩 할 수 있습니다. Java / C # / C ++는 실제로 이와 관련하여 상대적으로 특이한 요소입니다. 불행하게도, 그들은 사람들이 생각하는 올 정도로 인기가있다 "는 있다 , 그렇지 않으면 내가 가장 좋아하는 '주류'언어가 그것을 허용 것, 나쁜 생각합니다."

Java / C # / C ++는 기본적으로 클래스가 필요한 유일한 메소드 그룹이어야한다고 생각합니다. 컨텍스트를 결정할 수없는 방법이 너무 많은 경우, 두 가지 일반적인 접근 방식이 있습니다 : 컨텍스트별로 정렬하거나 컨텍스트별로 분리합니다.

문맥에 따라 정렬하는 것은 Clean Code 에서 작성된 권장 사항 중 하나이며 저자는 "TO 단락"패턴을 설명합니다. 이것은 기본적으로 헬퍼 함수를 ​​호출하는 함수 바로 뒤에 헬퍼 함수를 ​​배치하므로 신문 기사의 단락과 같이 읽을 수 있으므로 더 자세히 읽을 수 있습니다. 나는 그의 비디오에서 그는 심지어 들여 쓰기를 생각합니다.

다른 방법은 수업을 나누는 것입니다. 객체에 대한 메소드를 호출하기 전에 객체를 인스턴스화 해야하는 성가신 필요성과 각 데이터 조각을 소유 해야하는 몇 가지 작은 클래스를 결정하는 데 고유 한 문제가 있기 때문에 이것은 멀리까지 취할 수 없습니다. 그러나 실제로 하나의 컨텍스트에만 적합한 여러 메소드를 이미 식별했다면 아마도 자신의 클래스에 포함시키는 것이 좋습니다. 예를 들어, 빌더와 같은 작성 패턴으로 복잡한 초기화를 수행 할 수 있습니다.


중첩 함수 ... C # (및 Java 8)에서 람다 함수가 달성하는 것이 아닌가요?
Arturo Torres Sánchez

나는 이 파이썬 예제 와 같이 이름으로 정의 된 클로저와 같이 생각했습니다 . 람다는 그런 일을하는 가장 명확한 방법은 아닙니다. 필터 술어와 같은 짧은 표현식에 더 적합합니다.
Karl Bielefeldt

이러한 파이썬 예제는 C #에서 가능합니다. 예를 들어 factorial 입니다. 더 장황하지만 100 % 가능합니다.
Arturo Torres Sánchez

2
아무도 불가능하다고 말한 사람은 없습니다. OP는 심지어 그의 질문에 람다를 사용한다고 언급했습니다. 가독성을 위해 메소드를 추출하면 더 읽기 쉽다면 좋을 것입니다.
Karl Bielefeldt

첫 번째 단락은 불가능하다는 것을 암시하는 것으로 보입니다. 특히 인용문으로는 "나쁜 생각이어야합니다. 그렇지 않으면 내가 가장 좋아하는 '주류'언어가 허용 할 것입니다."
Arturo Torres Sánchez

4

나는 대부분의 경우에 대한 답은 맥락이라고 생각합니다. 코드를 작성하는 개발자는 나중에 코드가 변경 될 것이라고 가정해야합니다. 클래스는 다른 클래스와 통합되거나 내부 알고리즘을 대체하거나 추상화를 만들기 위해 여러 클래스로 분리 될 수 있습니다. 초보 개발자가 일반적으로 고려하지 않는 것들로, 나중에 복잡한 해결 방법이 필요하거나 나중에 정밀 검사가 필요합니다.

추출 방법은 좋지만 어느 정도는 있습니다. 검사하거나 코드를 작성하기 전에 항상 다음과 같은 질문을합니다.

  • 이 코드는이 클래스 / 함수에서만 사용됩니까? 앞으로도 계속 유지 될까요?
  • 구체적인 구현 중 일부를 전환 해야하는 경우 쉽게 할 수 있습니까?
  • 팀의 다른 개발자가이 기능에서 수행 한 작업을 이해할 수 있습니까?
  • 이 클래스의 다른 곳에서 동일한 코드가 사용됩니까? 거의 모든 경우에 중복을 피해야합니다.

어쨌든 항상 단일 책임을 생각하십시오. 클래스는 하나의 책임을 가져야하며, 함수는 하나의 상수 서비스를 제공해야하며, 여러 조치를 수행하는 경우 해당 조치에는 자체 함수가 있어야하므로 나중에 쉽게 구분하거나 변경할 수 있습니다.


1

큰 메소드에서 코드 블록 일 때보 다 작은 메소드에 대해 추론하기가 더 어려워집니다. 추출 할 때 호출자의 컨텍스트에서 오는 많은 기본 가정을 잃기 때문입니다.

ECS를 채택하여 더 큰 루프 시스템 기능 (시스템이 기능을 갖는 유일한 시스템)과 추상화가 아닌 원시 데이터에 대한 종속성을 권장 할 때까지 이것이 얼마나 큰 문제인지 알지 못했습니다 .

놀랍게도, 과거에 내가 작업했던 코드베이스와 비교할 때 훨씬 쉽게 추론하고 유지하기 쉬운 코드베이스를 만들었습니다. 여기서 디버깅하는 동안 모든 작은 작은 함수를 추적해야합니다. 종종 추상 함수 호출을 통해 순수한 인터페이스는 코드를 추적 할 때까지 어디에서 알 수 있는지를 알려주는 코드로, 일련의 이벤트를 발생시켜 코드가 결코 이끌지 않아야한다고 생각하지 않는 장소로 이어집니다.

John Carmack과는 달리, AAA 게임 엔진의 대기 시간이 매우 짧고 처리량과 관련된 대부분의 성능 문제가 없었기 때문에 이러한 코드베이스의 가장 큰 문제는 성능이 아닙니다. 물론 구조가 방해받지 않고 더 좁고 좁은 범위의 더 작고 더 작은 기능과 클래스에서 작업 할 때 핫스팟을 최적화하는 것이 점점 더 어려워 질 수 있습니다 (이 모든 작은 조각을 다시 융합해야 함) 효과적으로 대처하기 전에 더 큰 무언가로).

그러나 가장 큰 문제는 모든 테스트 통과에도 불구하고 시스템의 전반적인 정확성에 대해 자신있게 추론 할 수 없다는 것이 었습니다. 그 유형의 시스템이 모든 작은 세부 사항과 사소한 기능과 사물 사이의 끝없는 상호 작용을 고려하지 않고 그것에 대해 추론하지 못했기 때문에 내 두뇌에 너무 많이 이해하고 이해했습니다. "무엇을해야합니까?", 적시에 호출해야 할 항목이 너무 많았으며, 잘못된 시간을 호출하면 어떤 일이 발생할 지에 대한 질문이 너무 많았습니다. 하나의 이벤트가 다른 이벤트를 트리거하여 다른 이벤트를 트리거하면 모든 종류의 예측할 수없는 장소로 연결됩니다.

이제는 단호하고 명확한 책임을 수행하고 8 레벨의 중첩 블록이없는 한 내 엉덩이 큰 80 줄 함수가 여기저기서 좋습니다. 그들은 더 큰 기능의 작은 버전이 다른 사람이 호출 할 수없는 개인 구현 세부 정보 일지라도 시스템에서 테스트하고 이해해야 할 것이 적다는 느낌을 갖게됩니다. 시스템 전체에서 진행되는 상호 작용이 적은 것처럼 느껴지는 경향이 있습니다. 함수가 적다는 의미에서 복잡한 논리 (예 : 2-3 줄의 코드)가 아닌 한 아주 겸손한 코드 복제를 좋아합니다. 나는 Carmack이 그 기능을 소스 파일의 다른 곳에서 호출 할 수 없도록 만드는 것에 대한 추론을 좋아합니다. 그곳에'

옵션이 복잡한 함수 그래프로 서로를 호출하는 12 개의 간단한 함수 대 간단한 함수 사이에있는 경우 단순성이 큰 그림 수준에서 복잡성을 항상 감소시키는 것은 아닙니다. 하루가 끝나면 함수를 넘어서서 일어나는 일에 대해 추론하고, 이러한 함수가 궁극적으로 어떤 기능을 추가하는지 추론해야하며, 가장 작은 퍼즐 조각.

물론 잘 테스트 된 매우 일반적인 라이브러리 유형 코드는이 규칙에서 제외 될 수 있습니다. 이러한 범용 코드는 종종 자체적으로 작동하고 잘 작동하기 때문입니다. 또한 응용 프로그램의 도메인 (수백만 줄이 아니라 수천 줄)에 더 가까운 코드와 비교할 때 조그마한 경향이 있으며 , 매일 적용되는 어휘의 일부가되기 시작합니다. 그러나 시스템 전체의 불변성이 단일 함수 또는 클래스를 훨씬 넘어서는 응용 프로그램에 더 구체적인 것으로, 어떤 이유로 든 더 강력한 함수를 갖는 것이 도움이되는 경향이 있습니다. 큰 그림으로 무슨 일이 일어나고 있는지 파악하려고 더 큰 퍼즐 조각으로 작업하는 것이 훨씬 쉽다는 것을 알았습니다.


0

문제 라고 생각하지 않지만 번거 롭다는 데 동의합니다. 일반적으로 수혜자 바로 뒤에 도우미를 배치하고 "Helper"접미사를 추가합니다. 그 외에도 private액세스 지정자는 역할을 명확하게해야합니다. 도우미가 호출 될 때 유지되지 않는 불변이있는 경우 도우미에 주석을 추가합니다.

이 솔루션은 도움이되는 기능의 범위를 캡처하지 않는 불행한 단점이 있습니다. 이상적으로는 함수가 작기 때문에 매개 변수가 너무 많지 않기를 바랍니다. 일반적으로 매개 변수를 묶기 위해 새로운 구조체 또는 클래스를 정의 하여이 문제를 해결할 수 있지만 필요한 경우 상용구의 양이 도우미 자체보다 쉽게 ​​길어질 수 있습니다. 그런 다음 명확한 연결 방법으로 시작하지 않았습니다 함수가있는 구조체.

이미 다른 해결책을 언급했습니다. 주 함수 내부에 도우미를 정의하십시오. 일부 언어에서는 다소 흔치 않은 관용어 일 수 있지만 혼동하지 않을 것이라고 생각합니다 (일반적으로 람다에 의해 동료가 혼동되지 않는 한). 함수 나 함수와 유사한 객체를 쉽게 정의 할 수있는 경우에만 작동합니다. 예를 들어, 익명 클래스는 가장 작은 "함수"에 대해 2 단계의 중첩을 도입해야하므로 Java 7에서는 이것을 시도하지 않을 것입니다. 이것은 당신이 얻을 수있는 것처럼 letor where절에 가깝습니다 . 정의하기 전에 지역 변수를 참조 할 수 있으며 도우미를 해당 범위 밖에서 사용할 수 없습니다.

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