Реализация СОЛИДНЫХ принципов для C
Я знаю, что принципы SOLID были написаны для объектно-ориентированных языков.
Я нашел в книге: "Test driven development for embedded C" Роберта Мартина, следующее предложение в последней главе книги:
"Применение принципа открытого закрывания и принципа замены Лискова делает более гибкие конструкции".
Поскольку это книга C (нет С++ или С#), должен быть способ реализации этих принципов.
Существует стандартный способ реализации этих принципов в C?
Ответы
Ответ 1
Открытый принцип указывает, что система должна быть сконструирована так, чтобы она открывалась для расширения, сохраняя ее закрытой от модификации или ее можно было использовать и расширять без ее модификации. Подсистема ввода-вывода, как упоминается Деннисом, является довольно распространенным примером: в многократно используемой системе пользователь должен иметь возможность указать, как данные считываются и записываются, а не предполагать, что данные могут быть записаны только в файлы, например.
Способ реализации этого зависит от ваших потребностей: вы можете разрешить пользователю передавать дескриптор или дескриптор открытого файла, который уже позволяет использовать сокеты или каналы в дополнение к файлам. Или вы можете разрешить пользователю передавать указатели на функции, которые должны использоваться для чтения и записи: таким образом ваша система может использоваться с зашифрованными или сжатыми потоками данных в дополнение к тому, что позволяет ОС.
Принцип подстановки Liskov утверждает, что всегда необходимо заменить тип подтипом. В C у вас часто нет подтипов, но вы можете применить принцип на уровне модуля: код должен быть спроектирован так, чтобы использование расширенной версии модуля, как и более новая версия, не должно было его нарушать. В расширенной версии модуля может использоваться struct
, у которого больше полей, чем у исходного, больше полей в enum
и т.д., Поэтому ваш код не должен предполагать, что передаваемая структура имеет определенный размер, или что значения перечисления имеют определенный максимум.
Одним из примеров этого является то, как адреса сокетов реализуются в API-интерфейсе BSD: существует "абстрактный" тип сокета struct sockaddr
, который может стоять для любого типа адреса сокета, и конкретный тип сокета для каждой реализации, например struct sockaddr_un
для сокетов домена Unix и struct sockaddr_in
для IP-сокетов. Функции, которые работают на адресах сокетов, должны быть переданы указателем на данные и размером конкретного типа адреса.
Ответ 2
Во-первых, это помогает думать о том, почему у нас есть эти принципы проектирования. Почему следование принципам SOLID делает программное обеспечение лучше? Работайте, чтобы понять цели каждого принципа, а не только конкретные детали реализации, необходимые для использования их с определенным языком.
- Принцип единой ответственности улучшает модульность за счет увеличения
сплоченность; более высокая модульность приводит к улучшению тестируемости,
юзабилити и повторного использования.
- Принцип Open/Closed позволяет асинхронное развертывание посредством
развязывающие реализации друг от друга.
- Принцип замещения Лискова способствует модульности и повторному использованию модулей посредством
обеспечивая совместимость их интерфейсов.
- Принцип разделения сегментов уменьшает связь между
несвязанные потребители интерфейса, увеличивая удобочитаемость и
понятность.
- Принцип инверсии зависимостей уменьшает сцепление, и он сильно
позволяет тестировать.
Обратите внимание, как каждый принцип стимулирует улучшение в определенном атрибуте системы, будь то более высокая сплоченность, более слабая связь или модульность.
Помните, что ваша цель - создать высококачественное программное обеспечение. Качество составлено из множества различных атрибутов, включая правильность, эффективность, ремонтопригодность, понятность и т.д. После этого принципы SOLID помогают вам туда добраться. Поэтому, когда у вас есть "почему" принципов, "как" реализации становится намного проще.
EDIT:
Я постараюсь более точно ответить на ваш вопрос.
Для принципа Open/Close правило заключается в том, что как подпись, так и поведение старого интерфейса должны оставаться прежними до и после любых изменений. Не прерывайте код, вызывающий его. Это означает, что он абсолютно принимает новый интерфейс для реализации нового материала, потому что у старого материала уже есть поведение. Новый интерфейс должен иметь другую подпись, потому что он предлагает новую и разную функциональность. Таким образом, вы отвечаете этим требованиям на C так же, как и на С++.
Скажем, у вас есть функция int foo(int a, int b, int c)
, и вы хотите добавить версию, почти такую же, но она принимает четвертый параметр, например: int foo(int a, int b, int c, int d)
. Требование о том, чтобы новая версия была обратно совместима со старой версией, и что некоторые значения по умолчанию (такие как ноль) для нового параметра заставят это произойти. Вы переместили бы код реализации из старого foo в новый foo, и в своем старом foo вы сделали бы это: int foo(int a, int b, int c) { return foo(a, b, c, 0);}
Таким образом, хотя мы радикально изменили содержимое int foo(int a, int b, int c)
, мы сохранили его функциональность. Он оставался закрытым для изменения.
Принцип подстановки Лискова гласит, что разные подтипы должны работать совместимо. Другими словами, вещи с общими сигнатурами, которые могут быть заменены друг на друга, должны вести себя рационально одинаково.
В C это может быть выполнено с помощью указателей функций на функции, которые принимают идентичные наборы параметров. Скажем, у вас есть этот код:
#include <stdio.h>
void fred(int x)
{
printf( "fred %d\n", x );
}
void barney(int x)
{
printf( "barney %d\n", x );
}
#define Wilma 0
#define Betty 1
int main()
{
void (*flintstone)(int);
int wife = Betty;
switch(wife)
{
case Wilma:
flintstone = &fred;
case Betty:
flintstone = &barney;
}
(*flintstone)(42);
return 0;
}
fred() и barney() должны иметь совместимые списки параметров, чтобы это работало, конечно, но не отличалось от подклассов, наследующих их vtable от их суперклассов. Часть контракта на поведение заключалась бы в том, что как fred(), так и barney() не должны иметь скрытых зависимостей, или если они это делают, они также должны быть совместимы. В этом упрощенном примере обе функции полагаются только на stdout, поэтому это не очень важно. Идея заключается в том, что вы сохраняете правильное поведение в обеих ситуациях, где любая функция может использоваться взаимозаменяемо.
Ответ 3
Самое близкое, что я могу думать о моей голове (и это не идеально, поэтому, если у кого-то есть намного лучшая идея, они могут приветствовать меня) в основном, когда я пишу функции для некоторых вид библиотеки.
Для подстановки Liskov, если у вас есть файл заголовка, который определяет несколько функций, вы не хотите, чтобы функциональность этой библиотеки зависела от того, какая реализация у вас есть функций; вы должны иметь возможность использовать любую разумную реализацию и ожидать, что ваша программа сделает свое дело.
Что касается принципа Open/Closed, если вы хотите реализовать библиотеку ввода-вывода, вы хотите иметь функции, которые выполняют минимальный минимум (например, read
и write
). В то же время вы можете использовать их для разработки более сложных функций ввода-вывода (например, scanf
и printf
), но вы не собираетесь изменять код, который сделал минимум.