최소한의 지식의 원리


32

나는 최소한의 지식원리에 대한 동기를 이해 하지만 내 디자인에 적용하려고하면 몇 가지 단점을 발견합니다.

Head First Design Patterns 책에서 찾은이 원칙의 예 중 하나 (실제로 사용하지 않는 방법)는이 원칙의 관점에서 다른 메소드 호출에서 반환 된 객체에 대해 메소드를 호출하는 것이 잘못되었다고 지정합니다. .

그러나 때로는 그러한 기능을 사용해야 할 필요가 있습니다.

예를 들어 : 비디오 캡처 클래스, 인코더 클래스, 스 트리머 클래스와 같은 몇 가지 클래스가 있으며 모두 기본 클래스 인 VideoFrame을 사용하며 서로 상호 작용하기 때문에 다음과 같은 작업을 수행 할 수 있습니다.

streamer 수업 코드

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

보시다시피이 원칙은 여기에 적용되지 않습니다. 이 원칙을 여기에 적용 할 수 있습니까, 아니면이 원칙을 항상 이와 같은 디자인에 적용 할 수는 없습니까?




4
아마도 더 가까운 복제본 일 것입니다.
Robert Harvey

1
관련 : 이와 같은 코드가 "기차 사고"(Demeter of Law)를 위반합니까? (전체 공개 : 그건 내 질문이다)
CVn

답변:


21

스 트리머 클래스에 다른 도우미 메소드를 추가하여 함수에 대해 이야기하는 원칙 (더 나은 법칙으로 알려진 ) 적용 할 수 있습니다

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

이제 각 기능은 "친구의 친구"가 아닌 "친구와의 대화"만합니다.

IMHO 그것은 단일 책임 원칙을보다 엄격하게 따르는 방법을 만드는 데 도움이되는 대략적인 지침입니다. 위의 것과 같은 간단한 경우에는 이것이 귀찮은 가치가 있고 결과 코드가 실제로 "깨끗한"지 아니면 눈에 띄는 이득없이 코드를 공식적으로 확장하는지 여부에 대해서는 매우 의견이 많습니다.


이 접근법은 코드에 대한 단위 테스트, 디버그 및 이유를 훨씬 쉽게 만들 수 있다는 이점이 있습니다.
gntskn 2:25에

+1. 그러나 코드를 변경 하지 않는 좋은 이유 는 클래스 인터페이스가 여러 다른 메소드를 하나 또는 몇 번 호출하는 메소드 (추가 로직없이)로 인해 부풀어 질 수 있기 때문입니다. C ++ COM (특히 WIC 및 DirectX)과 같은 일부 프로그래밍 환경에서는 다른 언어와 비교하여 각 단일 메소드를 COM 인터페이스에 추가하는 데 비용이 많이 듭니다.
rwong 님

1
C ++ COM 인터페이스 디자인에서 큰 클래스를 작은 클래스로 분해하면 (여러 객체와 대화해야 할 가능성이 높아짐) 인터페이스 방법의 수를 최소화하는 것은 깊은 이해를 바탕으로 실제 이점 (비용 절감)을 갖는 두 가지 설계 목표입니다. 내부 역학 (가상 테이블, 코드 재사용 성, 기타 여러 가지). 따라서 C ++ COM 프로그래머는 일반적으로 LoD를 무시해야합니다.
rwong 님

10
1 : 데메테르의 법칙 진짜 이름을 지정해야합니다 데메테르의 제안 : YMMV
이진 걱정하는

데메테르의 법칙은 건축 선택을위한 조언이며, 외관상의 변화를 제안하여 '법'을 준수하는 것 같습니다. 구문상의 변화가 갑자기 모든 것이 괜찮다는 의미는 아니기 때문에 오해가 될 것입니다. 내가 데메테르의 법칙이 기본적으로 의미하는 한, OOP를 원한다면 어디서나 getter 함수를 사용하여 결정적 코드 작성을 중단하십시오.
user2180613 2016 년

39

최소한의 지식 원칙 또는 데메테르의 법칙은 계급을 따라 계층을 가로 지르는 다른 계급의 세부 사항으로 계급을 뒤섞는 것에 대한 경고입니다. 그것은 "친구의 친구"가 아닌 "친구"와만 대화하는 것이 낫다는 것을 알려줍니다.

반짝이는 판금 갑옷을 입은 기사의 동상에 방패를 용접하라는 요청을 받았다고 상상해보십시오. 쉴드를 왼쪽 팔에 조심스럽게 배치하여 자연스럽게 보입니다. 팔뚝, 팔꿈치 및 윗팔에 방패가 갑옷에 닿는 3 개의 작은 장소가 있습니다. 연결이 확실해야하므로 세 곳을 모두 용접합니다. 이제 상사가 팔꿈치를 움직일 수 없기 때문에 화가 났다고 상상해보십시오. 당신은 갑옷이 절대 움직이지 않을 것이라고 생각해서 팔뚝과 팔뚝 사이에 움직이지 않는 연결을 만들었습니다. 방패는 친구의 팔뚝에만 연결해야합니다. 친구를 팔지 말아야합니다. 금속 덩어리를 추가해야 만질 수 있습니다.

은유는 훌륭하지만 실제로 친구라는 것은 무엇을 의미합니까? 객체를 생성하거나 찾는 방법을 아는 것은 친구입니다. 또는 객체가 다른 객체를 전달하도록 요청할 수 있습니다.이 객체 는 인터페이스 만 알고 있습니다. 친구를 얻는 방법에 대한 기대가 없기 때문에 이들은 친구로 간주되지 않습니다. 다른 물체가 통과 / 주입되어 물체가 어디에서 왔는지 알지 못하면 친구의 친구가 아니며 친구도 아닙니다. 객체는 사용법 만 알고있는 것입니다. 좋은 일입니다.

이와 같은 원칙을 적용하려고 할 때, 어떤 원칙을 달성하는 것을 절대로 금지하지 않는다는 것을 이해하는 것이 중요합니다. 그들은 같은 것을 성취하는 더 나은 디자인을 얻기 위해 더 많은 일을하는 것을 게을리 할 수도 있다는 경고입니다.

아무도 이유없이 일하기를 원하지 않기 때문에 이것을 따르는 것에서 무엇을 얻는 지 이해하는 것이 중요합니다. 이 경우 코드를 유연하게 유지합니다. 변경을 할 수 있고 걱정할 변경에 의해 영향을받는 다른 클래스가 줄어 듭니다. 잘 들리지만 어떤 종류의 종교 교리로 받아들이지 않으면 어떻게해야할지 결정하는 데 도움이되지 않습니다.

이 원칙을 맹목적으로 따르는 대신이 문제의 간단한 버전을 사용하십시오. 원칙을 따르지 않는 해결책을 쓰십시오. 이제 두 가지 솔루션이 있으므로 두 가지를 모두 시도하여 변경에 대한 수용력을 비교할 수 있습니다.

이 원칙을 따르는 동안 문제를 해결할 수 없으면 다른 기술이 없을 수 있습니다.

특정 문제에 대한 한 가지 해결책은 frame프레임과 대화하는 방법을 알고있는 무언가 (클래스 또는 방법) 에 삽입하여 클래스의 모든 프레임 채팅 세부 정보를 클래스에 분산시킬 필요가 없다는 것입니다. 프레임을 받으세요.

그것은 실제로 또 다른 원칙을 따릅니다 : 건설과 분리 사용.

frame = encoder->WaitEncoderFrame()

이 코드를 사용하면을 (를) 취득하는 데 책임이 Frame있습니다. 님과 대화 할 책임이 아직 없습니다 Frame.

frame->DoOrGetSomething(); 

이제와 대화하는 방법을 알아야 Frame하지만 다음과 같이 바꾸십시오.

new FrameHandler(frame)->DoOrGetSomething();

이제 친구 인 FrameHandler와 대화하는 방법 만 알면됩니다.

이를 달성하는 방법에는 여러 가지가 있으며 아마도 이것이 최선의 방법은 아니지만 원리를 따르는 것이 문제를 해결할 수없는 방법을 보여줍니다. 더 많은 작업이 필요합니다.

모든 좋은 규칙에는 예외가 있습니다. 내가 아는 가장 좋은 예는 내부 도메인 별 언어 입니다. DSL방법 쇄다른 유형을 반환하는 메소드를 계속 호출하고 직접 사용하기 때문에 Demeter의 법칙을 항상 남용하는 것 같습니다. 왜 괜찮습니까? DSL에서 반환되는 모든 것은 신중하게 설계된 친구이므로 직접 대화해야합니다. 설계 상 DSL의 방법 체인이 변경되지 않을 것으로 예상 할 권리가 있습니다. 찾은 것을 무엇이든 함께 연결하는 코드베이스를 무작위로 조사하면 그 권리가 없습니다. 최고의 DSL은 매우 얇은 표현이거나 다른 객체에 대한 인터페이스입니다. DSL이 왜 좋은 디자인인지 알게되면 데메테르의 법칙을 훨씬 더 잘 이해했기 때문에 이것을 언급 할뿐입니다. 어떤 사람들은 DSL이 데메테르의 실제 법칙을 위반하지 않는다고 말하기까지합니다.

다른 해결책은 다른 무언가 frame가 당신 에게 주입 되도록하는 것입니다. framesetter 또는 바람직하게 생성자에서 온 경우 프레임을 생성하거나 확보하는 책임을지지 않습니다. 이것은 당신이 여기서 역할을하고 있다는 것을 의미합니다 FrameHandlers. 대신에 지금 당신은 Frame다른 사람과 대화 하고 다른 것을 만드는 사람입니다 Frame .

SOLID 원칙은 내가 따라갈 큰 것들이다. 여기서 두 가지 중 하나는 단일 책임과 의존성 역전 원칙입니다. 이 두 가지를 존중하기는 어렵지만 여전히 Demeter의 법칙을 위반하게됩니다.

데메테르를 어기는 것은 마음대로 뷔페 레스토랑에서 식사하는 것과 같습니다. 약간의 선행 작업으로 메뉴와 서버를 원하는대로 제공 할 수 있습니다. 편안히 앉아 긴장을 풀고 팁을줍니다.


2
"그것은"친구 "와 대화하는 것이 아니라"친구 "와만 대화하는 것이 낫다는 것을 알려줍니다. ++
RubberDuck

1
두 번째 단락은 어딘가에서 도난 당했습니까? 그 개념을 완전히 이해 하게되었습니다 . 그것은 "친구의 친구"가 무엇인지, 그리고 너무 많은 수업 (관절)을 얽매 게하는 것에 대한 결점을 만들었습니다. + 견적.
die maus

2
@diemaus 어딘가에서 방패 단락을 훔친 경우 소스가 내 머리에서 누출되었습니다. 당시 제 뇌는 영리하다고 생각했습니다. 내가 기억하는 것은 많은 upvotes 후에 그것을 추가했기 때문에 그것을 확인하는 것이 좋습니다. 도움이되어 다행입니다.
candied_orange

19

기능 설계가 객체 지향 설계보다 낫습니까? 따라 다릅니다.

MVVM이 MVC보다 우수합니까? 따라 다릅니다.

아모스와 앤디 또는 마틴과 루이스? 따라 다릅니다.

무엇에 의존합니까? 선택하는 것은 각 기술 또는 기술이 소프트웨어의 기능적 및 비 기능적 요구 사항을 얼마나 잘 충족시키는 지에 따라 디자인, 성능 및 유지 관리 목표를 적절히 만족시키는 것입니다.

[일부 책]에서는 [일부] 잘못되었다고 말합니다.

책이나 블로그에서이 내용을 읽으면 그 장점에 따라 클레임을 평가하십시오. 즉, 왜 그런지 물어보십시오. 소프트웨어 개발에는 옳고 그른 기술이 없으며 "이 기술이 내 목표를 얼마나 잘 충족 시키는가? 효과가 있거나 비효율적입니까? 하나의 문제를 해결하지만 새로운 문제를 만들 수 있습니까? 전체적으로 잘 이해할 수 있습니까?" 개발팀입니까, 아니면 너무 모호합니까? "

이 특별한 경우 (다른 메소드에 의해 리턴 된 객체에 대해 메소드를 호출하는 행위)-이 방법을 팩토리 화하는 실제 디자인 패턴이 있기 때문에 (공장),이를 범주 적으로 어설 션하는 방법을 상상하기는 어렵습니다. 잘못된.

이것이 "최소한 지식의 원리"라고 불리는 이유는 "낮은 커플 링"이 바람직한 시스템 품질이기 때문입니다. 서로 밀접하게 바인딩되지 않은 개체는 더 독립적으로 작동하므로 개별적으로 유지 관리 및 수정하기가 더 쉽습니다. 그러나 예제에서 알 수 있듯이 높은 커플 링이 더 바람직한 경우가 있으므로 객체가 노력을보다 효과적으로 조정할 수 있습니다.


2

Doc Brown의 답변 은 Law of Demeter의 고전적인 교과서 구현을 보여줍니다. 그리고 수십 가지 방법을 추가하는 성가신 / 혼란스러운 코드 팽창은 아마도 자신도 포함 된 프로그래머가 종종 그렇게해도 귀찮게하지 않는 이유 일 것입니다.

객체의 계층 구조를 분리하는 다른 방법이 있습니다.

메서드와 속성을 통해 형식 interface이 아닌 class형식을 노출 하십시오.

원본 포스터 (OP)의 경우, 대신을 encoder->WaitEncoderFrame()반환하고 허용되는 작업을 정의합니다.IEncoderFrameFrame


솔루션 1

가장 쉬운 경우 FrameEncoder클래스가 당신의 통제하에 둘 다, IEncoderFrame공개적으로 이미 방법 프레임의 하위 집합입니다 노출, 그리고 Encoder클래스는 실제로 해당 개체에 무슨 상관하지 않는다. 그런 다음 구현은 간단합니다 ( c #의 코드 ).

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

솔루션 2

Frame정의가 통제 할 수 없거나 중간에 IEncoderFrame메소드를 추가하는 것이 적절하지 않은 중간의 경우 Frame좋은 해결책은 Adapter 입니다. 이것이 CandiedOrange의 답변에서 설명한 바와 같습니다 new FrameHandler( frame ). 중요 :이 작업을 수행 하는 경우 클래스가 아닌 인터페이스 로 노출하면 더 유연합니다 . 에 대해 알아야 하지만 클라이언트 만 알면 됩니다. 또는 내가 명명 한대로 - 인코더의 POV에서 볼 때 특히 프레임 임을 나타냅니다 .Encoderclass FrameHandlerinterface IFrameHandlerinterface IEncoderFrame

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

비용 : 새로운 객체 인 EncoderFrameWrapper의 할당 및 GC encoder.TheFrame가 호출 될 때마다 . 이 래퍼를 캐시 할 수 있지만 더 많은 코드가 추가됩니다. 인코더의 프레임 필드를 새 프레임으로 바꿀 수없는 경우에만 안정적으로 코딩 할 수 있습니다.


솔루션 3

더 어려운 경우, 새로운 래퍼 모두에 대해 알고 있어야 Encoder하고 Frame. 그 객체 자체가 LoD를 위반할 것입니다. 이것은 인코더의 책임이어야하는 인코더와 프레임 간의 관계를 조작하는 것입니다. 그 길을 시작하면 다음과 같은 일이 발생할 수 있습니다.

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

못 생겼어 랩퍼가 작성자 / 소유자 (인코더)의 세부 사항을 터치해야하는 경우 덜 복잡한 구현이 있습니다.

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

물론 내가 여기서 끝날 줄 알았다면 그렇게하지 않을 수도 있습니다. LoD 메소드를 작성하고 완료 할 수 있습니다. 인터페이스를 정의 할 필요가 없습니다. 반면에 인터페이스가 관련 메소드를 함께 래핑하는 것이 좋습니다. 나는 프레임처럼 느껴지는 것에 "프레임 같은 작업"을하는 느낌이 마음에 든다.


최종 의견

이 고려 의 구현이 경우 Encoder노출하는 것을 느꼈다 Frame frame그들이 대신 한 경우에, 그것은 훨씬 더 안전했을 전반적인 아키텍처에 적합한이었다, 또는 "너무 쉽게 디테일 정도를 구현하는 것보다"이었다 처음 내가 보여 니펫을 - 제한된 일부를 노출 인터페이스로서의 프레임. 내 경험상, 그것은 종종 완전히 실행 가능한 솔루션입니다. 필요에 따라 인터페이스에 메소드를 추가하십시오. (프레임에 필요한 메소드가 이미 "알고있는"시나리오에 대해 이야기하고 있습니다. 추가하기가 쉽고 논란의 여지가 없습니다. 각 메소드에 대한 "구현"작업은 인터페이스 정의에 한 줄을 추가하는 것입니다.) 최악의 미래 시나리오에서도 해당 API를 계속 작동시킬 수 있습니다.IEncoderFrameFrameEncoder.

또한주의 당신이 추가 할 수있는 권한이없는 경우 그 IEncoderFrame대상을 Frame, 또는 필요한 방법은 일반에 잘 맞지 않는 Frame솔루션 # 2는 아마도 때문에 별도의 객체 생성 및 파괴, 당신을 적합하지 않는 클래스와, 솔루션 # 3은 EncoderLoD를 달성 하기위한 방법을 구성하는 간단한 방법으로 볼 수 있습니다 . 수십 가지 방법 만 거치지 마십시오. 인터페이스로 래핑하고 "명시 적 인터페이스 구현"(C #에있는 경우)을 사용하면 해당 인터페이스를 통해 개체를 볼 때만 액세스 할 수 있습니다.

강조하고 싶은 또 다른 요점기능을 인터페이스노출 하기로 한 결정 은 위에서 설명한 세 가지 상황을 모두 처리 했다는 것 입니다. 첫 번째 IEncoderFrame는 단순히 Frame기능 의 하위 집합입니다 . 두 번째 IEncoderFrame는 어댑터입니다. 세 번째 IEncoderFrameEncoder기능에 대한 파티션 입니다. 이 세 가지 상황 사이에서 요구 사항이 변경 되더라도 중요하지 않습니다. API는 동일하게 유지됩니다.


클래스를 구체적인 클래스가 아닌 공동 작업자 중 하나가 반환 한 인터페이스에 연결하는 것은 여전히 ​​개선의 원인이됩니다. 인터페이스를 변경해야하는 경우 클래스를 변경해야합니다. 제거해야 할 중요한 점은 불안정한 물체 나 내부 구조물에 불필요한 커플 링 을 피하는 한 커플 링은 본질적으로 나쁘지 않다는 것 입니다. 이것이 데메테르의 법칙이 큰 소금 꼬집음으로 가져와야하는 이유입니다. 상황에 따라 문제가 될 수도 있고 아닐 수도있는 것을 항상 피하도록 지시합니다.
Periata Breatta

@PeriataBreatta-나는 그것에 동의하지 않습니다. 그러나 한 가지 지적하고 싶습니다. 인터페이스 는 정의에 따라 두 클래스 사이의 경계에서 알아야 할 사항 을 나타냅니다 . 그것이 "변경이 필요하다"면, 그것은 근본적입니다 . 어떤 대안도 필요한 코딩을 마 법적으로 피할 수 없었습니다. 내가 설명하는 세 가지 상황 중 하나를 수행하지만 인터페이스를 사용하지 않는 대신 구체적인 클래스를 반환하십시오. 1의 프레임, 2의 EncoderFrameWrapper, 3의 Encoder. 당신은 접근 방식에 자신을 고정시킵니다 . 인터페이스는 그들 모두에게 적응할 수 있습니다.
ToolmakerSteve

@PeriataBreatta ... 인터페이스로 명시 적으로 정의하는 이점을 보여줍니다. 인터페이스가 훨씬 더 많이 사용될 수 있도록 IDE를 개선하여 편리하게 사용할 수 있기를 바랍니다. 대부분의 다중 레벨 액세스는 일부 인터페이스를 통해 이루어 지므로 변경 관리가 훨씬 쉬워집니다. (경우에 따라 "과잉"인 경우 작은 성능 향상 대신 인터페이스가 없을 위험에 대한 주석과 함께 코드 분석을 수행하면 "컴파일"할 수 있습니다. 3 가지 "솔루션"의 3 가지 구체적인 클래스 중 하나.)
ToolmakerSteve

@PeriataBreatta-그리고 "인터페이스가 그들 모두에게 적용 할 수있다"고 말할 때, "아,이 언어 기능이 다른 사례를 다룰 수 있다는 것이 놀랍습니다." 인터페이스를 정의 하면 변경해야 할 사항이 최소화 된다고 합니다. 가장 좋은 경우, 디자인은 인터페이스를 전혀 변경하지 않고도 가장 간단한 경우 (솔루션 1)에서 가장 어려운 경우 (솔루션 3)로 완전히 변경할 수 있습니다. 생산자의 내부 요구 만 더 복잡해졌습니다. 그리고 변경이 필요한 경우에도 IMHO의 보급률이 낮습니다.
ToolmakerSteve
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.