"말하고 묻지 말 것"이 어떻게 좋은 OO로 간주되는지에 대한 설명


49

이 블로그 게시물 은 여러 업 보트와 함께 Hacker News에 게시되었습니다. C ++에서 나온 대부분의 예제는 내가 배운 것과 반대되는 것 같습니다.

예 # 2와 같은 :

나쁜:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

대 좋은 :

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

C ++의 조언은 캡슐화가 증가함에 따라 멤버 함수 대신 자유 함수를 선호해야한다는 것입니다. 둘 다 의미 상 동일하므로 더 많은 주에 액세스 할 수있는 선택을 선호하는 이유는 무엇입니까?

예 4 :

나쁜:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

대 좋은 :

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

User관련이없는 오류 문자열을 포맷 해야하는 이유는 무엇 입니까? 'No street name on file'거리가없는 경우 인쇄 이외의 작업을 수행하려면 어떻게 합니까? 거리의 이름이 같은 경우 어떻게해야합니까?


누군가가 "말하지 말고"장점과 이론적 근거를 알려줄 수 있습니까? 나는 어느 것이 더 나은지 찾지 않고 저자의 관점을 이해하려고 노력합니다.


코드 예제는 Python이 아니라 Ruby 일 수 있습니다.
Pubby

2
나는 항상 첫 번째 예제와 같은 것이 SRP를 위반하지 않는지 궁금합니다.
stijn

1
: 당신은 읽을 수 있습니다 pragprog.com/articles/tell-dont-ask을
Mik378

루비. 예를 들어 @는 속기이며 파이썬은 블록을 암시 적으로 공백으로 끝냅니다.
Erik Reppen

3
"C ++의 조언은 캡슐화가 증가함에 따라 멤버 함수 대신 자유 함수를 선호해야한다는 것입니다." 누가 당신에게 그런 말을했는지 모르겠지만 사실이 아닙니다. 무료 기능을 사용하여 캡슐화를 늘릴 수 있지만 반드시 캡슐화를 늘릴 필요는 없습니다.
Rob K

답변:


81

객체의 상태에 대해 질문 한 다음 객체 외부에서 내려진 결정에 따라 해당 객체에서 메소드를 호출한다는 것은 객체가 이제 새는 추상화라는 것을 의미합니다. 그 동작 중 일부는 객체 외부 에 위치하고 내부 상태는 (아마도 불필요하게) 외부 세계에 노출됩니다.

개체에게 원하는 것을 말하려고 노력해야합니다. 그들에게 자신의 상태에 대해 질문하지 말고 결정을 한 다음 무엇을해야하는지 이야기하십시오.

문제는 호출자로서 호출 된 객체의 상태에 따라 결정을 내려서 객체의 상태를 변경해서는 안된다는 것입니다. 당신이 구현하고있는 논리는 아마도 당신의 책임이 아닌 객체의 책임 일 것이다. 객체 외부에서 결정을 내리려면 캡슐화를 위반합니다.

물론 그렇게 말할 수도 있습니다. 그런 코드를 쓰지 않을 것입니다. 그럼에도 불구하고 참조 된 객체를 검사 한 다음 결과에 따라 다른 메소드를 호출하는 것이 매우 쉽습니다. 그러나 이것이 최선의 방법은 아닙니다. 대상에게 원하는 것을 말하십시오. 그것을하는 방법을 알아 내자. 절차 적이 라기보다는 선언적으로 생각하십시오!

책임에 따라 클래스를 디자인하여 시작하면이 함정에서 벗어나는 것이 더 쉽습니다. 그런 다음 객체의 상태를 알려주는 쿼리와 달리 클래스가 실행할 수있는 명령을 지정하여 자연스럽게 진행할 수 있습니다.

http://pragprog.com/articles/tell-dont-ask


4
예제 본문 은 분명히 좋은 연습을하는 많은 것들을 허용하지 않습니다 .
DeadMG

13
@DeadMG 그것은 사이트 이름에서 "실용적"을 맹목적으로 무시 하고 자신의 교재에 명확하게 언급 된 사이트 작성자 의 주요 생각을 무시하고 노예를 따르는 사람들에게만 당신이하는 말을합니다 : " 최고의 해결책 은 없습니다 ... "
gnat

2
책을 읽지 마십시오. 나도 원하지 않겠 어 나는 단지 예제 텍스트를 읽었습니다.
DeadMG

3
@DeadMG 걱정하지 마십시오. 이제이 예제 (및 그 문제에 대한 pragprog의 다른 예제)를 의도 된 컨텍스트 ( "최상의 솔루션 같은 것은 없습니다")에 넣는 요점을 알았으므로이 책을 읽지 않는 것이 좋습니다
gnat

1
나는 아직도 텔, 물어 보지 말아야 할 것이 무엇인지 잘 모르겠지만 상황에 상관없이 당신을 위해 철자해야하지만 이것은 좋은 OOP 조언입니다.
Erik Reppen

16

일반적으로이 기사는 다른 사람 이 자신에 대해 추론 할 수있는 경우 다른 사람이 추론하도록 회원국을 노출해서는 안된다고 제안합니다 .

그러나 분명하게 언급되지 않은 것은이 법이 특정 계급의 책임을 넘어 설 때 매우 명백한 한계에 빠진다 는 것입니다. 예를 들어, 어떤 직업이 어떤 가치를 유지하거나 어떤 가치, 특히 일반적인 것들을 제공하거나 직업이 확장해야하는 행동을 제공하는 모든 직업.

예를 들어, 시스템 temperature이 쿼리를 제공하면 내일이면 클라이언트는 check_for_underheating변경할 필요가 없습니다 SystemMonitor. 이것은 SystemMonitor구현 check_for_overheating자체 가 아닌 경우 입니다. 따라서 SystemMonitor온도가 너무 높을 때 알람을 발생시키는 직업은 다음과 같습니다. 그러나 SystemMonitor다른 코드 조각이 온도를 읽도록 허용하여 터보 부스트 또는 이와 유사한 것을 제어 할 수있는 직업 해서는 안됩니다.

또한 두 번째 예제는 Null Object Anti-pattern을 무의미하게 사용합니다.


19
“Null Object”는 반 패턴이라고 부르는 것이 아니므로 왜 그렇게했는지 궁금하십니까?
Konrad Rudolph

4
"아무것도하지 않음"으로 지정된 메소드가 아무도 없는지 확인하십시오. 그것은 그들을 무의미하다고 부릅니다. 즉, Null 개체를 구현하는 모든 개체는 최소한 LSP를 중단하고 실제로는 그렇지 않은 작업을 구현하는 것으로 설명합니다. 사용자 가치를 다시 기대 합니다. 그들의 프로그램의 정확성은 그것에 달려 있습니다. 그렇지 않은 경우 가치가 있다고 가장하여 더 많은 문제를 가져옵니다. 자동으로 실패한 메소드를 디버그하려고 시도한 적이 있습니까? 불가능 하고 아무도 그것을 겪을 필요 가 없습니다 .
DeadMG

4
나는 이것이 전적으로 문제 영역에 달려 있다고 주장한다.
Konrad Rudolph

5
@DeadMG 나는 위의 예는 널 개체 패턴의 나쁜 사용 점에 동의하지만, 거기 입니다 장점은 그것을 사용하는. 몇 번이나 나는 null 검사를 피하거나 시스템에 침투하는 진정한 'null'을 피하기 위해 일부 인터페이스 또는 다른 인터페이스의 'no-op'구현을 사용했습니다.
Max

6
"고객 check_for_underheating이 변경하지 않고도 할 수 있습니다"라는 요점이 확실하지 않습니다 SystemMonitor. SystemMonitor그 시점에서 고객과 어떻게 다릅니 까? 이제 여러 클래스에서 모니터링 논리를 소멸시키지 않습니까? 또한 알람 기능을 예약하면서 다른 클래스에 센서 정보를 제공하는 모니터 클래스의 문제점을 보지 못했습니다. 부스트 컨트롤러는 온도가 너무 높아지면 알람 발생에 대해 걱정할 필요없이 부스트를 제어해야합니다.
TMN

9

과열 예의 실제 문제는 과열로 규정 되는 규칙 이 시스템마다 쉽게 다를 수 없다는 것입니다. 시스템 A가있는 그대로 (온도> 100이 과열 됨) 시스템 B가 더 섬세하다고 가정합니다 (온도> 93이 과열 됨). 제어 기능을 변경하여 시스템 유형을 확인한 다음 올바른 값을 적용합니까?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

또는 각 유형의 시스템에서 가열 용량을 정의합니까?

편집하다:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

전자는 더 많은 시스템을 다루기 시작할 때 제어 기능을 못 생겼습니다. 후자는 시간이 지남에 따라 제어 기능을 안정적으로 유지합니다.


1
각 시스템이 모니터에 등록되지 않는 이유는 무엇입니까? 등록하는 동안 과열 발생시기를 표시 할 수 있습니다.
Martin York

@LokiAstari-가능하지만 습도 나 대기압에 민감한 새로운 시스템을 사용할 수 있습니다. 원리는 다양한 것을 이해하는 것입니다.이 경우 과열에 취약합니다.
Matthew Flynn

1
이것이 바로 tell 모델이 있어야하는 이유입니다. 시스템에 현재 상태를 알려주고 정상적인 작동 조건을 벗어난 경우 알려줍니다. 따라서 SystemMoniter를 수정할 필요가 없습니다. 그것은 당신을위한 캡슐화입니다.
마틴 요크

@LokiAstari-우리는 여기서 교차 목적에 대해 이야기하고 있다고 생각합니다. 다른 모니터가 아닌 다른 시스템을 만드는 것을 실제로보고있었습니다. 문제는 시스템이 외부 컨트롤러 기능과 반대로 알람을 발생시키는 상태에 있다는 것을 알아야합니다. SystemA에는 기준이 있어야하고 SystemB에는 자체 기준이 있어야합니다. 컨트롤러는 시스템이 정상인지 아닌지를 정기적으로 물어볼 수 있어야합니다.
Matthew Flynn

6

먼저, 예제의 "나쁜"및 "좋은"특성을 제외해야한다고 생각합니다. 이 기사에서는 "좋지 않음"과 "더 나은"이라는 용어를 사용합니다. 이러한 용어는 이유 때문에 선택되었다고 생각합니다. 이는 지침이며, 상황에 따라 "좋지 않은"접근 방식이 적절하거나 실제로 유일한 솔루션 일 수 있습니다.

선택의 여지를 제공 할 때 제공해야합니다 선호도를 클래스에 전적으로 의존하는 기능을 포함하여 이유 때문에 캡슐화하고, 그것을 쉽게 시간이 지남에 따라 클래스를 진화 할 수 있다는 사실 - 클래스 대신에 그것을 외부를. 이 클래스는 또한 많은 무료 기능보다 기능을 광고하는 데 더 효과적입니다.

결정은 클래스 외부의 무언가에 의존하거나 단순히 클래스의 대부분의 사용자가 원하지 않는 것이기 때문에 말해야합니다. 행동은 수업에 반 직관적이며 수업의 대부분의 사용자를 혼동하고 싶지 않기 때문에 때때로 말하기를 원합니다.

예를 들어, 오류 메시지를 반환하는 번지에 대해 불평하지만 그렇지 않은 경우 기본값을 제공하는 것입니다. 그러나 때로는 기본값이 적합하지 않습니다. 이것이 주 또는 도시인 경우, 모든 알 수없는 사람이 특정 사람에게 전달되도록 레코드를 판매원 또는 설문 조사 담당자에게 할당 할 때 기본값을 원할 수 있습니다. 반면에 봉투를 인쇄하는 경우 배달 할 수없는 편지에 용지를 낭비하지 못하게하는 예외 또는 경비원을 선호 할 수 있습니다.

따라서 "좋지 않음"이 좋은 방법 일 수 있지만 일반적으로 "더 나은"것이 더 좋습니다.


3

데이터 / 오브젝트 대칭

다른 사람들이 지적했듯이 Tell-Dont-Ask는 요청 후 객체 상태변경하는 경우를위한 것입니다 (예 :이 페이지의 다른 곳에 게시 된 Pragprog 텍스트 참조). 항상 그런 것은 아닙니다. 예를 들어, user.address를 요청한 후 'user'개체가 변경되지 않습니다. 따라서 이것이 Tell-Dont-Ask를 적용하기에 적합한 경우 논쟁의 여지가 있습니다.

Tell-Dont-Ask는 책임감에 관심이 있으며, 클래스 내에서 논리를 정당화 할 수는 없습니다. 그러나 객체를 다루는 모든 논리가 반드시 그 객체의 논리 인 것은 아닙니다. 이것은 Tell-Dont-Ask를 넘어서서 더 깊은 수준에서 암시하고 있으며, 그것에 대해 간단히 말하고 싶습니다.

건축 설계와 관련하여 실제로는 속성의 컨테이너 일뿐 아니라 불변 인 객체를 원할 수도 있습니다. 그런 다음 이러한 객체의 컬렉션에 대해 다양한 기능을 실행하여 명령을 보내지 않고 평가, 필터링 또는 변환합니다. Tell-Dont-Ask의 도메인).

문제에 더 적합한 결정은 함수 측면에서 변경 / 추가와 함께 안정적인 데이터 (선언적 개체)를 기대하는지 여부에 따라 다릅니다. 또는 이러한 기능의 안정적이고 제한된 세트를 기대하지만 객체 레벨에서 새로운 유형 추가와 같은 더 많은 흐름을 기대하는 경우. 첫 번째 상황에서는 두 번째 객체 방법에서 자유 기능을 선호합니다.

Bob Martin은 자신의 저서 인 "Clean Code"에서이를 "데이터 / 물체 반대 칭"(p.95ff)이라고하며 다른 커뮤니티에서는이를 " 표현 문제 " 라고 지칭 할 수 있습니다 .


3

이 패러다임은 때때로 '말하고 묻지 말' 이라고도합니다 . 즉, 대상에게해야 할 일을 말하고 상태에 대해 묻지 않습니다. 때로는 '묻고 말하지 말라' 라는 의미로, 객체에게 무언가를 해달라고 요청하고 상태가 무엇인지 말하지 마십시오. 모범 사례를 둘러싼 방법은 동일합니다. 개체가 작업을 수행하는 방법은 호출하는 개체가 아니라 해당 개체의 관심사입니다. 인터페이스는 상태 (예 : 접근 자 또는 공용 속성 등)를 노출하지 말고 구현이 불투명 한 '실행'메서드를 노출해야합니다. 다른 사람들은 실용 프로그래머와의 링크로 이것을 다루었습니다.

이 규칙은 종종 언급되는 '만 즉시 친구 이야기'로 언급 "이중 점"또는 "이중 화살표"코드 피해에 대한 규칙과 관련이 foo->getBar()->doSomething()나쁜 대신 사용 foo->doSomething();바의 기능 래퍼 호출 인을하고, 간단하게 구현 return bar->doSomething();- 경우 foo의 관리를 담당 bar하고 그렇게하자!


1

"tell, do n't ask"에 대한 다른 좋은 답변 외에도 다음과 같은 특정 예에 대한 주석이 있습니다.

C ++의 조언은 캡슐화가 증가함에 따라 멤버 함수 대신 자유 함수를 선호해야한다는 것입니다. 둘 다 의미 상 동일하므로 더 많은 주에 액세스 할 수있는 선택을 선호하는 이유는 무엇입니까?

그 선택은 더 많은 주에 접근 할 수 없습니다. 둘 다 같은 양의 상태를 사용하여 작업을 수행하지만 '나쁜'예에서는 클래스 상태가 공개되어 작업을 수행해야합니다. 또한 '나쁜'예제에서 해당 클래스의 동작은 자유 함수로 확산되어 찾기가 더 어렵고 리팩토링하기가 더 어렵습니다.

관련이없는 오류 문자열을 포맷하는 것은 왜 사용자의 책임입니까? 거리가없는 경우 '파일에 거리 이름 없음'이라는 인쇄 이외의 작업을 수행하려면 어떻게합니까? 거리의 이름이 같은 경우 어떻게해야합니까?

'street name'과 'error message 제공'을 모두 수행하는 것이 'street_name'의 책임 인 이유는 무엇입니까? 최소한 '좋은'버전에서는 각 조각마다 하나의 책임이 있습니다. 여전히 좋은 예는 아닙니다.


2
그건 사실이 아니야. 과열을 점검하는 것이 온도와 관련이있는 유일한 방법이라고 가정합니다. 클래스가 여러 온도 모니터 중 하나가되고 시스템이 많은 결과에 따라 다른 조치를 취해야하는 경우 어떻게해야합니까? 이 동작이 단일 인스턴스의 사전 정의 된 동작으로 제한 될 있으면 확실합니다. 그렇지 않으면 분명히 적용 할 수 없습니다.
DeadMG

물론, 온도 조절기와 경보가 다른 등급에 존재하는 경우 (필요한 경우).
Telastyn

1
@DeadMG : 일반적인 조언은 액세스가 필요할 때까지 사물을 보호 / 보호하는 것입니다. 이 특정 예는 meh이지만 표준 관행에 위배되지는 않습니다.
Guvante

'meh'라는 관행관한 기사의 한 예가 그것을 논박한다. 이 방법이 큰 이점으로 인해 표준이라면, 왜 적절한 예를 찾는 데 어려움이 있습니까?
Stijn de Witt

1

이 답변은 매우 훌륭하지만 여기에 강조해야 할 또 다른 예가 있습니다. 일반적으로 복제를 피하는 방법입니다. 예를 들어 다음과 같은 코드가있는 SEVERAL 장소가 있다고 가정 해 보겠습니다.

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

즉, 다음과 같은 것이 더 좋습니다.

Product product = productMgr.get(productUuid, currentUser)

중복은 인터페이스의 대부분의 클라이언트가 여기저기서 동일한 논리를 반복하는 대신 새로운 방법을 사용한다는 것을 의미합니다. 직접 정보를 요청하는 대신 대리인에게 원하는 작업을 제공합니다.


0

나는 이것이 고급 객체를 작성할 때 더 사실이라고 생각하지만, 모든 클래스 소비자를 만족시키기 위해 모든 단일 메소드를 작성할 수 없기 때문에 더 높은 수준의 클래스 라이브러리로 내려갈 때 덜 사실이라고 생각합니다.

예를 들어 # 2, 나는 그것이 너무 단순화되었다고 생각합니다. 실제로 이것을 구현하려는 경우, SystemMonitor는 저수준 하드웨어 액세스를위한 코드와 동일한 클래스에 내장 된 고수준 추상화를위한 논리를 갖게됩니다. 불행히도, 우리가 두 클래스로 분리하려고하면 "말하지 말아라"자체를 위반하게됩니다.

예제 # 4는 다소 비슷합니다. UI 로직을 데이터 계층에 포함시킵니다. 주소가없는 경우 사용자가보고 싶은 것을 고치려면 데이터 계층에서 개체를 수정해야합니다. 두 개의 프로젝트가 동일한 개체를 사용하지만 null 주소에 대해 다른 텍스트를 사용해야하는 경우 어떻게해야합니까?

우리가 모든 것에 대해 "Tell, Do n't ask"를 구현할 수 있다면 매우 유용 할 것입니다. 실생활에서 물어 보지 않고 직접 말할 수 있다면 나 자신도 행복 할 것입니다! 그러나 실제와 마찬가지로 솔루션의 실행 가능성은 높은 수준의 클래스로 제한됩니다.

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