Как применить принцип разделения интерфейса в C?

15

У меня есть модуль, скажем «M», в котором есть несколько клиентов, скажем «C1», «C2», «C3». Я хочу распределить пространство имен модуля M, то есть объявления API и данных, которые он предоставляет, в файл (ы) заголовка таким образом, чтобы -

  1. для любого клиента видны только те данные и API, которые ему необходимы; остальная часть пространства имен модуля скрыта от клиента, т.е. придерживается принципа разделения интерфейса .
  2. объявление не повторяется в нескольких заголовочных файлах, то есть не нарушает DRY .
  3. модуль М не имеет никаких зависимостей от своих клиентов.
  4. на клиента не влияют изменения, сделанные в частях модуля M, которые не используются им.
  5. существующие клиенты не зависят от добавления (или удаления) большего количества клиентов.

В настоящее время я занимаюсь этим, разделяя пространство имен модуля в зависимости от требований его клиентов. Например, на рисунке ниже показаны различные части пространства имен модуля, требуемые его 3-мя клиентами. Требования клиента перекрываются. Пространство имен модуля разделено на 4 отдельных заголовочных файла - «a», «1», «2» и «3» .

Разделение пространства имен модуля

Однако это нарушает некоторые из вышеупомянутых требований, то есть R3 и R5. Требование 3 нарушается, поскольку такое разбиение зависит от характера клиентов; также при добавлении нового клиента это разбиение изменяет и нарушает требование 5. Как видно из правой части приведенного выше изображения, с добавлением нового клиента пространство имен модуля теперь разделено на 7 заголовочных файлов - 'a ',' b ',' c ',' 1 ',' 2 * ',' 3 * 'и' 4 ' . Заголовочные файлы предназначены для двух изменений старых клиентов, что приводит к их перестройке.

Есть ли способ достичь сегрегации интерфейса в C не изобретенным способом?
Если да, как бы вы справились с приведенным выше примером?

Я думаю, что это нереальное гипотетическое решение:
модуль имеет 1 толстый заголовочный файл, охватывающий все пространство имен. Этот заголовочный файл разделен на адресуемые разделы и подразделы, такие как страница Википедии. Каждый клиент имеет специальный заголовочный файл, адаптированный для него. Специфичные для клиента заголовочные файлы - это просто список гиперссылок на разделы / подразделы толстого заголовочного файла. И система сборки должна распознавать специфичный для клиента заголовочный файл как «измененный», если какой-либо из разделов, на которые он указывает в заголовке модуля, изменен.

work.bin
источник
1
Почему эта проблема специфична для C? Это потому что C не имеет наследования?
Роберт Харви
Кроме того, нарушение интернет-провайдера делает ваш дизайн лучше?
Роберт Харви
2
C на самом деле не поддерживает концепции ООП (такие как интерфейсы или наследование). Мы делаем с грубыми (но творческими) взломами. Ищу взломать для моделирования интерфейсов. Как правило, весь заголовочный файл является интерфейсом для модуля.
work.bin
1
structэто то, что вы используете в C, когда вы хотите интерфейс. Конечно, методы немного сложны. Вы можете найти это интересным: cs.rit.edu/~ats/books/ooc.pdf
Роберт Харви,
Я не мог придумать эквивалентный интерфейс, используя structи function pointers.
work.bin

Ответы:

5

Разделение интерфейса, как правило, не должно основываться на требованиях клиента. Вы должны изменить весь подход для достижения этого. Я бы сказал, модульный интерфейс, группируя функции в согласованные группы. То есть группировка основана на согласованности самих функций, а не требований клиента. В этом случае у вас будет набор интерфейсов I1, I2 и т. Д. Клиент C1 может использовать только I2. Клиент C2 может использовать I1, I5 и т. Д. Обратите внимание, что если клиент использует более одного Ii, это не проблема. Если вы разложили интерфейс на последовательные модули, вот в чем суть.

Опять же, провайдер не основывается на клиенте. Речь идет о разложении интерфейса на более мелкие модули. Если это будет сделано правильно, это также обеспечит доступ клиентов к минимальному количеству функций.

При таком подходе ваши клиенты могут увеличиваться до любого числа, но на вас М это не влияет. Каждый клиент будет использовать одну или несколько комбинаций интерфейсов в зависимости от своих потребностей. Будут ли случаи, когда клиент C должен включать, скажем, I1 и I3, но не использовать все функции этих интерфейсов? Да, это не проблема. Он просто использует наименьшее количество интерфейсов.

Назар Мерза
источник
Вы наверняка имели в виду непересекающиеся или непересекающиеся группы?
Док Браун
Да, непересекающиеся и не перекрывающиеся.
Назар Мерза
3

Принцип разделения интерфейса гласит:

Ни один клиент не должен зависеть от методов, которые он не использует. Интернет-провайдер разделяет очень большие интерфейсы на более мелкие и более специфичные, чтобы клиенты могли знать только о методах, которые им интересны.

Здесь есть несколько вопросов без ответа. Один:

Как маленький?

Ты говоришь:

В настоящее время я занимаюсь этим, разделяя пространство имен модуля в зависимости от требований его клиентов.

Я называю это ручным вводом утки . Вы создаете интерфейсы, которые предоставляют только то, что нужно клиенту. Принцип разделения интерфейса - это не просто ручная печать.

Но Интернет-провайдер - это не просто призыв к «согласованным» интерфейсам ролей, которые можно использовать повторно. Никакой «согласованный» дизайн интерфейса ролей не может полностью защитить от добавления нового клиента с его собственными потребностями ролей.

ISP - это способ изолировать клиентов от влияния изменений в сервисе. Он был предназначен для ускорения сборки при внесении изменений. Конечно, у него есть и другие преимущества, например, не ломать клиентов, но это было главным. Если я изменяю count()сигнатуру сервисных функций, было бы хорошо, если бы неиспользуемые клиенты count()не нуждались в редактировании и перекомпиляции.

Вот почему я забочусь о принципе разделения интерфейсов. Это не то, что я считаю верой, как важно. Это решает реальную проблему.

Таким образом, способ его применения должен решить проблему для вас. Не существует интеллектуального способа применить провайдера, который нельзя победить только правильным примером необходимого изменения. Предполагается, что вы посмотрите на то, как меняется система, и сделаете выбор, который позволит успокоиться. Давайте рассмотрим варианты.

Сначала спросите себя: сложно ли сейчас вносить изменения в интерфейс сервиса? Если нет, выходите на улицу и играйте, пока не успокоитесь. Это не интеллектуальное упражнение. Пожалуйста, убедитесь, что лекарство не хуже, чем болезнь.

  1. Если многие клиенты используют одно и то же подмножество функций, это говорит о «связных» повторно используемых интерфейсах. Подмножество, вероятно, сосредоточено вокруг одной идеи, которую мы можем рассматривать как роль, которую служба предоставляет клиенту. Приятно, когда это работает. Это не всегда работает.

  2.  

    1. Если многие клиенты используют разные подмножества функций, возможно, что клиент фактически использует службу через несколько ролей. Это нормально, но это делает роли трудно различимыми. Найди их и постарайся дразнить. Это может вернуть нас в случае 1. Клиент просто использует сервис через несколько интерфейсов. Пожалуйста, не начинайте пользоваться услугой. Во всяком случае, это означало бы передачу услуги клиенту более одного раза. Это работает, но это заставляет меня задаться вопросом, не является ли сервис большим клубком грязи, который нужно разбить.

    2. Если многие клиенты используют разные подмножества, но вы не видите роли, даже если клиенты могут использовать более одного, у вас нет ничего лучше, чем печатать на клавиатуре, чтобы разрабатывать свои интерфейсы. Такой способ проектирования интерфейсов гарантирует, что клиент не будет подвержен даже одной функции, которую он не использует, но он почти гарантирует, что добавление нового клиента всегда будет включать добавление нового интерфейса, который в то время как реализация службы не должна знать об этом интерфейс, который агрегирует роль интерфейсов. Мы просто обменяли одну боль на другую.

  3. Если многие клиенты используют разные подмножества, накладываются друг на друга, ожидается добавление новых клиентов, которым потребуются непредсказуемые подмножества, и вы не захотите сломать службу, а затем подумайте о более функциональном решении. Поскольку первые две опции не сработали, и вы действительно находитесь в плохом месте, где ничто не следует шаблону, и грядут дополнительные изменения, тогда рассмотрите возможность предоставления каждой функции свой собственный интерфейс. Завершение здесь не означает, что провайдер провалился. Если что-то не получалось, это была объектно-ориентированная парадигма. Интерфейсы с одним методом следуют за ISP в крайнем случае. Это довольно сложный ввод с клавиатуры, но вы можете обнаружить, что это внезапно делает интерфейсы многоразовыми. Опять же, убедитесь, что нет

Получается, что они могут стать очень маленькими.

Я воспринял этот вопрос как вызов для применения ISP в самых крайних случаях. Но имейте в виду, что крайностей лучше избегать. В хорошо продуманном дизайне, который применяет другие принципы SOLID, эти проблемы обычно не возникают или имеют значение, почти столько же.


Еще один вопрос без ответа:

Кому принадлежат эти интерфейсы?

Снова и снова я вижу интерфейсы, разработанные на основе того, что я называю «библиотечным» менталитетом. Мы все были виновны в кодировании monkey-see-monkey-do, когда вы просто что-то делаете, потому что именно так вы и видели. Мы виноваты в одном и том же с интерфейсами.

Когда я смотрел на интерфейс, разработанный для класса в библиотеке, я думал: о, эти ребята профессионалы. Это должен быть правильный способ сделать интерфейс. Что я не смог понять, так это то, что у границ библиотеки есть свои потребности и проблемы. С одной стороны, библиотека совершенно не знает о дизайне своих клиентов. Не все границы одинаковы. И иногда даже одна и та же граница имеет разные пути ее пересечения.

Вот два простых взгляда на дизайн интерфейса:

  • Сервисный интерфейс. Некоторые люди разрабатывают каждый интерфейс, чтобы показать все, что может сделать сервис. Вы даже можете найти опции рефакторинга в IDE, которые напишут интерфейс для вас, используя любой класс, которым вы его кормите.

  • Клиентский интерфейс. Интернет-провайдер, кажется, утверждает, что это правильно, а принадлежащий сервис - неправильно. Вы должны разбить каждый интерфейс с учетом потребностей клиентов. Поскольку клиент владеет интерфейсом, он должен его определить.

Так кто же прав?

Рассмотрим плагины:

введите описание изображения здесь

Кому здесь принадлежат интерфейсы? Клиенты? Услуги?

Оказывается оба.

Цвета здесь слои. Красный слой (справа) не должен ничего знать о зеленом слое (слева). Зеленый слой можно изменить или заменить, не касаясь красного слоя. Таким образом, любой зеленый слой может быть подключен к красному слою.

Мне нравится знать, что должно знать о чем, а что не должно знать. Для меня «что знает о чем?», Это самый важный архитектурный вопрос.

Давайте проясним некоторые слова:

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

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

Клиент - это то, что использует.

Служба - это то, что используется.

Interactor бывает оба.

Интернет-провайдер говорит, что нужно разбивать интерфейсы для клиентов. Хорошо, давайте применим это здесь:

  • Presenter(сервис) не должен диктовать Output Port <I>интерфейс. Интерфейс должен быть сужен к тому, что Interactor(здесь действует как клиент) необходимо. Это означает, что интерфейс ЗНАЕТ о, Interactorи, чтобы следовать за ISP, должен измениться с этим. И это нормально.

  • Interactor(здесь действует как сервис) не должен диктовать Input Port <I>интерфейс. Интерфейс должен быть сужен до Controllerпотребностей клиента. Это означает, что интерфейс ЗНАЕТ о, Controllerи, чтобы следовать за ISP, должен измениться с этим. И это не хорошо.

Второй не подходит, потому что красный слой не должен знать о зеленом слое. Так что провайдер не прав? Ну вроде. Ни один принцип не является абсолютным. Это тот случай, когда глупцы, которым нравится интерфейс, показывающий все, что может сделать сервис, оказываются правы.

По крайней мере, они правы, если Interactorне делают ничего, кроме потребностей этого варианта использования. Если Interactorони работают для других вариантов использования, то нет причин Input Port <I>, о которых нужно знать. Не уверен, почему Interactorнельзя просто сосредоточиться на одном прецеденте, так что это не проблема, но вещи случаются.

Но input port <I>интерфейс просто не может подчинить себя Controllerклиенту, и это будет настоящий плагин. Это граница «библиотеки». Совсем другой магазин программирования мог бы писать зеленый слой спустя годы после того, как красный слой был опубликован.

Если вы пересекаете границу «библиотеки» и чувствуете необходимость применения интернет-провайдера, даже если у вас нет интерфейса на другой стороне, вам нужно будет найти способ сузить интерфейс, не меняя его.

Один из способов сделать это - адаптер. Поместите это между клиентами, как Controlerи Input Port <I>интерфейс. Адаптер принимает Interactorкак Input Port <I>и делегирует свою работу ему. Однако он раскрывает только то, что Controllerнужно клиентам, через ролевый интерфейс или интерфейсы, принадлежащие зеленому слою. Адаптер не следует самому ISP, но позволяет более сложному классу, например, Controllerнаслаждаться ISP. Это полезно, если адаптеров меньше, чем используют клиенты Controller, использующие их, и когда вы находитесь в необычной ситуации, когда вы пересекаете границу библиотеки и, несмотря на публикацию, библиотека не перестанет меняться. Глядя на тебя Firefox. Теперь эти изменения только сломают ваши адаптеры.

Так что это значит? Честно говоря, это означает, что вы не предоставили мне достаточно информации, чтобы сказать вам, что вам следует делать. Я не знаю, если не следование ISP вызывает у вас проблемы. Я не знаю, приведет ли это к тому, что у вас возникнут дополнительные проблемы.

Я знаю, что вы ищете простой руководящий принцип. ISP пытается быть таким. Но это оставляет много недосказанного. Я верю в это. Да, пожалуйста, не заставляйте клиентов полагаться на методы, которые они не используют, без веской причины!

Если у вас есть веская причина, например, вы разрабатываете что-то, чтобы принимать плагины, то помните о проблемах, не связанных с причинами провайдера (трудно изменить, не нарушая клиентов), и о способах их устранения (сохраняйте Interactorили хотя бы Input Port <I>сосредотачивайтесь на одном стабильном). вариант использования).

candied_orange
источник
Спасибо за вклад. У меня есть модуль предоставления услуг, который имеет несколько клиентов. Его пространство имен имеет логически согласованные границы, но потребности клиентов выходят за эти логические границы. Таким образом, разделение пространства имен на основе логических границ не помогает с ISP. Поэтому я разделил пространство имен на основе потребностей клиента, как показано на диаграмме в вопросе. Но это делает его зависимым от клиентов и плохим способом подключения клиентов к сервису, поскольку клиенты могут добавляться / удаляться относительно часто, но изменения в сервисе будут минимальными.
work.bin
Сейчас я склоняюсь к сервису, обеспечивающему толстый интерфейс, как и в его полном пространстве имен, и клиент должен получить доступ к этим сервисам через специфичные для клиента адаптеры. В терминах C это будет файл обёрток функций, принадлежащих клиенту. Изменения в службе вызовут перекомпиляцию адаптера, но не обязательно клиента. .. <продолжение>
work.bin
<contd> .. Это определенно сохранит минимальное время сборки и сохранит связь между клиентом и службой «свободной» за счет времени выполнения (вызов промежуточной функции-обертки), увеличивает пространство кода, увеличивает использование стека и, возможно, больше пространства для разума (программист) в обслуживании адаптеров.
work.bin
Мое текущее решение удовлетворяет мои потребности сейчас, новый подход потребует больше усилий и вполне может нарушить YAGNI. Мне придется взвесить все «за» и «против» каждого метода и решить, куда идти.
work.bin
1

Итак, этот момент:

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

Отказывается от того, что вы нарушаете еще один важный принцип - ЯГНИ. Я бы заботился об этом, когда у меня есть сотни клиентов. Подумайте о чем-то заранее, и тогда окажется, что у вас нет дополнительных клиентов, потому что этот код превосходит цель.

второй

 partitioning depends on the nature of clients

Почему ваш код не использует DI, инверсию зависимостей, ничего, ничего в вашей библиотеке не должно зависеть от характера вашего клиента.

В конце концов, похоже, что вам нужен дополнительный слой под вашим кодом, чтобы удовлетворить потребности в перекрывающихся вещах (DI, так что ваш фронтальный код зависит только от этого дополнительного уровня, а ваши клиенты зависят только от вашего фронтального интерфейса), так что вы побьете DRY.
Это вы бы по-настоящему. Таким образом, вы делаете то же самое, что используете в слое вашего модуля под другим модулем. Таким образом, имея слой ниже, вы достигнете:

для любого клиента видны только те данные и API, которые ему необходимы; остальная часть пространства имен модуля скрыта от клиента, т.е. придерживается принципа разделения интерфейса.

да

объявление не повторяется в нескольких заголовочных файлах, то есть не нарушает DRY. модуль М не имеет никаких зависимостей от своих клиентов.

да

на клиента не влияют изменения, сделанные в частях модуля M, которые не используются им.

да

существующие клиенты не зависят от добавления (или удаления) большего количества клиентов.

да

Матеуш
источник
1

Та же информация, что и в декларации, всегда повторяется в определении. Просто так работает этот язык. Кроме того, повторение объявления в нескольких заголовочных файлах не нарушает СУХОЙ . Это довольно распространенная техника (по крайней мере, в стандартной библиотеке).

Повторение документации или реализация будет нарушать СУХОЙ .

Я не стал бы беспокоиться об этом, если бы код клиента не был написан мной.

Мацей Халапук
источник
0

Я отказываюсь от своего замешательства. Однако ваш практический пример рисует решение в моей голове. Если я могу сказать своими словами: все разделы в модуле 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)

Mh

#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

В файле Mc вам на самом деле не нужно использовать #ifdefs, потому что то, что вы помещаете в файл .c, не влияет на файлы клиента, если определены функции, используемые файлами клиента.

#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();
}

Опять же, я не уверен, что это то, что вы спрашиваете. Так что возьмите это с зерном соли.

Санчке Деллоар
источник
Как выглядит Мак? Вы определяете P1_init() и P2_init() ?
work.bin
@ work.bin Полагаю, что Mc выглядел бы как простой файл .c, за исключением определения пространства имен между функциями.
Санчке Деллоар
Предполагая, что существуют и C1, и C2 - что значит P1_init()и на что P2_init()ссылается?
work.bin
В файле Mh / Mc препроцессор заменит то, _PREF_к чему он был определен в последний раз. Так _PREF_init()будет P1_init()из-за последнего утверждения #define. Тогда следующий оператор определения будет устанавливать PREF равным P2_, таким образом генерируя P2_init().
Санчке Деллоар