C에서 인터페이스 분리 원리를 적용하는 방법? 파일에 할당하고 싶습니다. 모든 클라이언트의 경우 필요한

‘C’, ‘C2’, ‘C3’과 같은 클라이언트가있는 ‘M’이라는 모듈이 있습니다. 모듈 M의 네임 스페이스, 즉 API 및 노출 된 데이터 선언을 헤더 파일에 할당하여 다음과 같은 방식으로 헤더 파일에 할당하고 싶습니다.

  1. 모든 클라이언트의 경우 필요한 데이터 및 API 만 표시됩니다. 모듈의 나머지 네임 스페이스는 클라이언트에서 숨겨집니다. 즉, 인터페이스 분리 원칙을 준수하십시오 .
  2. 선언은 여러 헤더 파일에서 반복되지 않습니다 . 즉 DRY를 위반하지 않습니다 .
  3. 모듈 M은 클라이언트에 의존하지 않습니다.
  4. 클라이언트는 사용하지 않는 모듈 M의 일부 변경 사항에 영향을받지 않습니다.
  5. 기존 클라이언트는 더 많은 클라이언트를 추가 (또는 삭제)해도 영향을받지 않습니다.

현재 클라이언트의 요구 사항에 따라 모듈의 네임 스페이스를 나누어서 처리합니다. 예를 들어, 아래 이미지에는 3 개의 클라이언트에 필요한 모듈 네임 스페이스의 다른 부분이 표시되어 있습니다. 고객 요구 사항이 겹칩니다. 모듈의 네임 스페이스는 4 개의 개별 헤더 파일 ( ‘a’, ‘1’, ‘2’및 ‘3’) 로 나뉩니다 .

모듈 네임 스페이스 파티셔닝

그러나 이는 위에서 언급 한 요구 사항, 즉 R3 및 R5 중 일부를 위반합니다. 이 분할은 클라이언트의 특성에 따라 다르므로 요구 사항 3을 위반합니다. 또한 새 클라이언트를 추가 할 때이 분할이 변경되어 요구 사항 5를 위반합니다. 위 이미지의 오른쪽에서 볼 수 있듯이 새 클라이언트를 추가하면 모듈의 네임 스페이스가 7 개의 헤더 파일로 분류됩니다 . ‘,’b ‘,’c ‘,’1 ‘,’2 * ‘,’3 * ‘및’4 ‘ 입니다. 이전 클라이언트 중 2 개에 대한 헤더 파일이 변경되어 재구성이 트리거됩니다.

C에서 인터페이스 분리를 ​​달성 할 수있는 방법으로 달성 할 수있는 방법이 있습니까?
그렇다면 위의 예를 어떻게 다루겠습니까?

비현실적인 가상의 해결책은 다음과 같습니다
. 모듈에는 전체 네임 스페이스를 포함하는 1 개의 팻 헤더 파일이 있습니다. 이 헤더 파일은 Wikipedia 페이지와 같이 주소 지정 가능한 섹션과 하위 섹션으로 나뉩니다. 그런 다음 각 클라이언트에는 특정 헤더 파일이 있습니다. 클라이언트 별 헤더 파일은 팻 헤더 파일의 섹션 / 하위 섹션에 대한 하이퍼 링크 목록 일뿐입니다. 그리고 모듈 헤더에서 가리키는 섹션 중 하나라도 수정되면 빌드 시스템은 클라이언트 특정 헤더 파일을 ‘수정 됨’으로 인식해야합니다.



답변

인터페이스 분리는 일반적으로 클라이언트 요구 사항을 기반으로하지 않아야합니다. 이를 달성하기 위해 전체 접근 방식을 변경해야합니다. 기능을 일관된 그룹 으로 그룹화하여 인터페이스를 모듈화하십시오 . 즉, 그룹화는 클라이언트 요구 사항이 아니라 기능 자체의 일관성을 기반으로합니다. 이 경우 인터페이스 I1, I2 등이 있습니다. 클라이언트 C1은 I2 만 사용할 수 있습니다. 클라이언트 C2는 I1 및 I5 등을 사용할 수 있습니다. 클라이언트가 둘 이상의 Ii를 사용하는 경우 문제가되지 않습니다. 인터페이스를 코 히어 런트 모듈로 분해 한 경우, 그 부분이 문제의 핵심입니다.

다시 말하지만, ISP는 클라이언트 기반이 아닙니다. 인터페이스를 더 작은 모듈로 분해하는 것입니다. 이것이 올바르게 수행되면 클라이언트가 필요한만큼의 기능에 노출되도록합니다.

이 방법을 사용하면 고객이 원하는만큼 증가 할 수 있지만 M은 영향을받지 않습니다. 각 클라이언트는 필요에 따라 하나 이상의 인터페이스 조합을 사용합니다. 클라이언트 C가 I1 및 I3이라고 말해야하지만 이러한 인터페이스의 모든 기능을 사용하지 않는 경우가 있습니까? 예, 그건 문제가되지 않습니다. 인터페이스 수가 가장 적습니다.


답변

인터페이스 독방 원리는 말합니다 :

클라이언트가 사용하지 않는 메소드에 의존해서는 안됩니다. ISP는 매우 큰 인터페이스를 더 작고 더 구체적인 인터페이스로 분할하여 클라이언트가 관심있는 방법에 대해서만 알면됩니다.

여기에 답변되지 않은 몇 가지 질문이 있습니다. 하나는:

얼마나 작습니까?

당신은 말합니다 :

현재 클라이언트의 요구 사항에 따라 모듈의 네임 스페이스를 나누어서 처리합니다.

이 수동 오리 타이핑 이라고 부릅니다 . 클라이언트에게 필요한 것만 노출하는 인터페이스를 빌드합니다. 인터페이스 분리 원리는 단순히 수동 오리 타이핑이 아닙니다.

그러나 ISP는 단순히 재사용 할 수있는 “일관된”역할 인터페이스를 요구하는 것이 아닙니다. “일관성있는”역할 인터페이스 디자인은 자체 역할 요구가있는 새로운 클라이언트의 추가를 완벽하게 막을 수 없습니다.

ISP 는 클라이언트가 서비스 변경으로 인한 영향으로부터 격리시키는 방법입니다. 변경 사항을 작성할 때 빌드가 더 빨라지도록 고안되었습니다. 물론 고객을 해치지 않는 것과 같은 다른 이점도 있지만 이것이 핵심이었습니다. 서비스 count()기능 서명을 변경하는 경우 사용 count()하지 않는 클라이언트를 편집하고 다시 컴파일 할 필요가없는 것이 좋습니다 .

이것이 내가 인터페이스 분리 원리에 관심을 갖는 이유입니다. 내가 중요하게 생각하는 것은 아닙니다. 실제 문제를 해결합니다.

따라서 적용 방식이 문제를 해결해야합니다. ISP를 적용하는 데 필요한 방법은 없습니다. 필요한 변경의 올바른 예만으로는 이길 수 없습니다. 시스템이 어떻게 변하고 있는지 살펴보고 사일런트를 방해 할 수있는 선택을해야합니다. 옵션을 살펴 보자.

먼저 스스로에게 물어보십시오 : 지금 서비스 인터페이스를 변경하기가 어렵습니까? 그렇지 않다면 밖으로 나가 진정 할 때까지 연주하십시오. 이것은 지적 운동이 아닙니다. 치료법이 질병보다 나쁘지 않은지 확인하십시오.

  1. 많은 클라이언트가 동일한 기능의 하위 집합을 사용하는 경우 “일관성있는”재사용 가능한 인터페이스가 필요합니다. 하위 집합은 서비스가 클라이언트에게 제공하는 역할로 생각할 수있는 하나의 아이디어에 중점을 둘 것입니다. 이것이 작동하면 좋습니다. 항상 작동하지는 않습니다.

  2.  

    1. 많은 클라이언트가 다른 기능 하위 집합을 사용하는 경우 클라이언트가 실제로 여러 역할을 통해 서비스를 사용하고있을 수 있습니다. 괜찮지 만 역할을보기가 어렵습니다. 그들을 찾아서 애타게 시도하십시오. 클라이언트는 단순히 하나 이상의 인터페이스를 통해 서비스를 사용합니다. 서비스 캐스팅을 시작하지 마십시오. 서비스가 클라이언트에 두 번 이상 전달되는 것을 의미하는 경우. 그것은 효과가 있지만 서비스가 큰 진흙 공이 아닌지 나에게 의문을 제기합니다.

    2. 많은 클라이언트가 서로 다른 하위 집합을 사용하지만 클라이언트가 둘 이상을 사용할 수 있도록 허용하는 역할을 보지 못하면 인터페이스를 디자인하기 위해 오리 타이핑보다 나은 것은 없습니다. 인터페이스를 설계하는이 방법은 클라이언트가 사용하지 않는 하나의 기능에도 클라이언트가 노출되지 않도록하지만 새 클라이언트를 추가하면 항상 서비스 구현을 알 필요는 없지만 새 인터페이스를 추가해야합니다. 그것에 대해 역할 인터페이스를 집계하는 인터페이스가 수행합니다. 우리는 단순히 하나의 고통을 다른 것으로 바 꾸었습니다.

  3. 많은 클라이언트가 서로 다른 서브 세트를 사용하는 경우 겹치기 때문에 예측할 수없는 서브 세트가 필요한 새 클라이언트가 추가 될 것으로 예상되므로 서비스를 중단하지 않고보다 기능적인 솔루션을 고려하십시오. 처음 두 옵션이 작동하지 않고 패턴을 따르지 않고 더 많은 변화가 오는 나쁜 장소에 있기 때문에 각 기능을 제공하는 것이 좋습니다. 여기서 끝났다고해서 ISP가 실패한 것은 아닙니다. 실패한 것은 객체 지향 패러다임입니다. 단일 메소드 인터페이스는 ISP를 극도로 따릅니다. 키보드 타이핑은 상당히 좋지만 갑자기 인터페이스를 재사용 할 수 있습니다. 다시 한번,

그래서 그들은 실제로 매우 작아 질 수 있습니다.

가장 극단적 인 경우에 ISP를 적용하기위한 도전으로이 질문을했습니다. 그러나 극단을 피하는 것이 가장 좋습니다. 다른 SOLID 원리 를 적용하는 신중한 설계에서 이러한 문제는 일반적으로 거의 발생하지 않거나 중요하지 않습니다.


답변되지 않은 또 다른 질문은 다음과 같습니다.

이 인터페이스는 누가 소유합니까?

나는 계속해서 “라이브러리”라는 개념으로 설계된 인터페이스를 본다. 우리 모두는 당신이 무언가를하고있는 원숭이 참조 몽키도 코딩에 대해 유죄였습니다. 인터페이스도 마찬가지입니다.

라이브러리의 클래스를 위해 설계된 인터페이스를 볼 때 나는 다음과 같이 생각했습니다. 오,이 사람들은 전문가입니다. 인터페이스를 수행하는 올바른 방법이어야합니다. 내가 이해하지 못한 것은 라이브러리 경계에 자체 요구와 문제가 있다는 것입니다. 우선, 라이브러리는 클라이언트의 디자인을 완전히 무시합니다. 모든 경계가 같은 것은 아닙니다. 때로는 같은 경계조차도 다른 방법으로 교차 할 수 있습니다.

다음은 인터페이스 디자인을 보는 간단한 두 가지 방법입니다.

  • 서비스 소유 인터페이스. 어떤 사람들은 서비스가 할 수있는 모든 것을 드러내 기 위해 모든 인터페이스를 설계합니다. IDE에서 리팩토링 옵션을 찾을 수 있습니다. IDE에서 피드를 제공하는 클래스를 사용하여 인터페이스를 작성합니다.

  • 클라이언트 소유 인터페이스. ISP는 이것이 옳고 소유 한 서비스가 잘못되었다고 주장하는 것 같습니다. 클라이언트 요구 사항을 염두에두고 모든 인터페이스를 분리해야합니다. 클라이언트가 인터페이스를 소유하고 있으므로이를 정의해야합니다.

누가 맞습니까?

플러그인을 고려하십시오 :

여기에 이미지 설명을 입력하십시오

누가 인터페이스를 소유합니까? 클라이언트? 서비스?

둘 다 밝혀졌습니다.

여기의 색상은 레이어입니다. 빨간색 레이어 (오른쪽)는 녹색 레이어 (왼쪽)에 대해 아무 것도 알고 있지 않습니다. 녹색 레이어는 빨간색 레이어를 건드리지 않고 변경하거나 교체 할 수 있습니다. 이렇게하면 모든 녹색 레이어를 빨간색 레이어에 꽂을 수 있습니다.

나는 무엇에 대해 알아야하고 무엇을 알아야하는지 알고 싶습니다. 나에게 “무엇을 알고 있는가?”는 가장 중요한 건축 질문입니다.

어휘를 명확하게하자 :

[Client] --> [Interface] <|-- [Service]

----- Flow ----- of ----- control ---->

클라이언트는 사용하는 것입니다.

서비스는 사용되는 것입니다.

Interactor 둘 다 발생합니다.

ISP는 클라이언트를위한 인터페이스를 분리한다고 말합니다. 좋아, 여기에 적용하자.

  • Presenter(서비스)가 Output Port <I>인터페이스를 지시해서는 안됩니다 . 인터페이스는 Interactor클라이언트 (여기서는 클라이언트 역할)의 필요에 따라 좁혀 져야 합니다. 이는 InteractorISP 에 대한 인터페이스 지식 과 ISP에 따라 변경되어야 한다는 것을 의미합니다 . 그리고 이것은 괜찮습니다.

  • Interactor(여기서는 서비스 역할을 함) Input Port <I>인터페이스에 지시해서는 안됩니다 . 인터페이스는 Controller(클라이언트)에게 필요한 것으로 좁혀 져야 합니다. 이는 ControllerISP 에 대한 인터페이스 지식 과 ISP에 따라 변경되어야 한다는 것을 의미합니다 . 그리고 이것은 좋지 않습니다 .

두 번째는 빨간색 레이어가 녹색 레이어에 대해 알지 못하기 때문에 좋지 않습니다. ISP가 잘못 되었습니까? 글쎄요 절대적인 원칙은 없습니다. 인터페이스를 좋아하는 바보가 서비스가 할 수있는 모든 것을 보여주기위한 경우가 옳습니다.

적어도이 Interactor유스 케이스가 아닌 다른 일을하지 않으면 옳습니다 . (가) 경우 Interactor다른 사용 사례에 대한 일을이 이유가 없다 Input Port <I>그들에 대해 알고있다가. Interactor하나의 유스 케이스에만 집중할 수없는 이유 는 확실 하지 않으므로 이것이 문제가되지는 않지만 문제가 발생합니다.

그러나 input port <I>인터페이스는 단순히 Controller클라이언트 자체를 종속시킬 수 없으며 이것이 진정한 플러그인이 될 수 있습니다. 이것이 ‘라이브러리’경계입니다. 완전히 다른 프로그래밍 상점은 빨간 층이 출판 된 후 몇 년 동안 녹색 층을 쓸 수 있습니다.

‘라이브러리’경계를 넘어서고 다른 쪽에서 인터페이스를 소유하지 않아도 ISP를 적용 할 필요가 있다고 생각하면 변경하지 않고 인터페이스를 좁힐 수있는 방법을 찾아야합니다.

그것을 뽑아내는 한 가지 방법은 어댑터입니다. 같은 클라이언트 사이에 넣어 ControlerInput Port <I>인터페이스를 제공합니다. 어댑터는 Interactor로 승인 하고 Input Port <I>작업을 위임합니다. 그러나 Controller역할 인터페이스 나 그린 레이어가 소유 한 인터페이스를 통해 고객이 필요로하는 것만 노출합니다 . 어댑터는 ISP 자체를 따르지 않지만 ControllerISP를 즐기는 것과 같이 더 복잡한 클래스를 허용 합니다. 이 기능은 클라이언트보다 Controller사용하는 어댑터 수보다 적은 수의 어댑터가 있고 라이브러리 경계를 넘어서서 게시 된 경우에도 라이브러리 변경이 중지되지 않는 비정상적인 상황에있을 때 유용합니다. Firefox를보고 있습니다. 이제 이러한 변경으로 인해 어댑터가 손상되었습니다.

이것이 무엇을 의미합니까? 정직하게 당신이해야 할 일을 말해 줄 수있는 충분한 정보를 제공하지 않았다는 것을 의미합니다. ISP를 따르지 않는 것이 문제를 일으키는 지 모르겠습니다. 그것을 따르는 것이 더 이상 문제를 일으키지 않을지 모르겠습니다.

나는 당신이 간단한지도 원리를 찾고 있다는 것을 알고 있습니다. ISP는 그렇게하려고 노력합니다. 그러나 그것은 많은 말이 남지 않습니다. 나는 그것을 믿는다. 그렇습니다. 정당한 이유없이 고객이 사용하지 않는 방법에 의존하도록 강요하지 마십시오!

플러그인을 수용 할 무언가를 설계하는 등의 정당한 이유가있는 경우, ISP 원인을 따르지 않는 문제 (클라이언트를 중단하지 않고 변경하기 어렵다)와이를 완화시키는 방법 ( 안정적인 Interactor또는 최소한 Input Port <I>하나의 안정에 집중 하거나 사용 사례).


답변

따라서이 시점 :

existent clients are unaffected by the addition (or deletion) of more clients.

YAGNI 인 또 다른 중요한 원칙을 위반하고 있음을 포기합니다. 수백 명의 고객이있을 때 관심이 있습니다. 먼저 뭔가를 생각하면이 코드에 대한 추가 클라이언트가 없다는 것이 목적보다 뛰어납니다.

둘째

 partitioning depends on the nature of clients

코드에서 DI를 사용하지 않는 이유, 종속성 반전, 아무것도, 라이브러리의 아무것도 클라이언트 특성에 의존해서는 안됩니다.

결국 DRY를 이길 방식으로 중복 항목에 대한 요구를 충족하기 위해 코드 아래에 추가 레이어가 필요한 것처럼 보입니다 (DI는 프론트 레이어 코드는이 추가 레이어에만 의존하고 클라이언트는 프론트 인터페이스에만 의존합니다).
이것은 당신이 진짜로 그것을 od 것입니다. 따라서 다른 모듈 아래의 yor 모듈 레이어에서 사용하는 것과 동일한 것을 만듭니다. 이 방법으로 아래에 레이어를두면 다음과 같은 효과가 있습니다.

모든 클라이언트의 경우 필요한 데이터 및 API 만 표시됩니다. 모듈의 네임 스페이스의 나머지 부분은 클라이언트에서 숨겨집니다. 즉, 인터페이스 분리 원칙을 따릅니다.

선언은 여러 헤더 파일에서 반복되지 않습니다. 즉 DRY를 위반하지 않습니다. 모듈 M은 클라이언트에 의존하지 않습니다.

클라이언트는 사용하지 않는 모듈 M의 일부 변경 사항에 영향을받지 않습니다.

기존 클라이언트는 더 많은 클라이언트를 추가 (또는 삭제)해도 영향을받지 않습니다.


답변

선언에 제공된 것과 동일한 정보는 항상 정의에서 반복됩니다. 이 언어가 작동하는 방식 일뿐입니다. 또한 여러 헤더 파일에서 선언을 반복해도 DRY를 위반하지 않습니다 . (표준 라이브러리에서) 다소 일반적으로 사용되는 기술입니다.

문서 나 구현을 반복하면 DRY를 위반하게 됩니다.

클라이언트 코드가 나에게 작성되지 않은 한, 나는 이것을 귀찮게하지 않을 것입니다.


답변

나는 혼란을 부인한다. 그러나 실제적인 예는 내 머리 속에 해결책을 제시합니다. 난 내 자신의 말로 할 수있는 경우 : 모듈의 모든 파티션은 M많은 많은 임의의 모든 고객과의 독점적 관계를.

샘플 구조

M.h      // fat header
 - P1    // Partition 1
 - P2    // ... 2
   - P21 // ... 2 section 1
 - P3    // ... 3
C1.c     // Client 1 (Needs to include P1, P3)
C2.c     // ... 2 (Needs to include P2)
C3.c     // ... 3 (Needs to include P1, P21, P3)

#ifdef P1
#define _PREF_ P1_             // Define Prefix ("PREF") = P1_
 void _PREF_init();            // Some partition specific function
#endif /* P1 */

#ifdef P2
#define _PREF_ P2_
 void _PREF_init();
#endif /* P2 */

#if defined(P21) || defined (P2) // Part 2.1
#define _PREF_ P2_1_
 void _PREF_oddone();
#endif /* P21 */

#ifdef P3
#define _PREF_ P3_
 void _PREF_init();
#endif /* P3 */

Mc 파일에서는 .c 파일에 넣은 내용이 클라이언트 파일이 사용하는 기능이 정의되어있는 한 클라이언트 파일에 영향을 미치지 않기 때문에 실제로 #ifdefs를 사용할 필요는 없습니다.

#include "M.h"
#define _PREF_ P1_
void _PREF_init() { ... };

#define _PREF_ P2_
void _PREF_init() { ... }

#define _PREF_ P2_1_
void _PREF_oddone() { ... }

#define _PREF_ P3_
void _PREF_init() { ... }

C1.c

#define P1     // "invite" P1
#define P3     // "invite" P3
#include "M.h" // Open the door, but only the invited come in.

void main()
{
    P1_init();
    //P2_init();
    //P2_1_oddone();
    P3_init();
}

C2.c

#define P2
#include "M.h

void main()
{
    //P1_init();
    P2_init();
    P2_1_oddone();
    //P3_init();
}

C3.c

#define P1
#define P21
#define P3
#include "M.h"

void main()
{
    P1_init();
    //P2_init();
    P2_1_oddone();
    P3_init();
}

다시 말하지만, 이것이 당신이 요구하는 것인지 확실하지 않습니다. 소금 한 조각으로 가져 가십시오.


답변