Я знаю, что принципы SOLID были написаны для объектно-ориентированных языков.
Я обнаружил в книге Роберта Мартина «Разработка через тестирование для встраиваемого С», следующее предложение в последней главе книги:
«Применение принципа открытого-закрытого типа и принципа подстановки Лискова делает проекты более гибкими».
Поскольку это книга на языке C (без c ++ или c #), должен быть способ реализации этих принципов.
Существует ли какой-либо стандартный способ реализации этих принципов в C?
Открыто-закрытый принцип утверждает, что система должна быть спроектирована так, чтобы она была открыта для расширения, оставляя ее закрытой от модификации, или что она может использоваться и расширяться без ее изменения. Подсистема ввода / вывода, как упомянул Деннис, является довольно распространенным примером: в многократно используемой системе пользователь должен иметь возможность указать, как данные считываются и записываются, вместо того, чтобы предполагать, что данные могут быть записаны, например, только в файлы.
Способ реализации этого зависит от ваших потребностей: вы можете разрешить пользователю передавать дескриптор или дескриптор открытого файла, который уже позволяет использовать сокеты или каналы в дополнение к файлам. Или вы можете разрешить пользователю передавать указатели на функции, которые должны использоваться для чтения и записи: таким образом, ваша система может использоваться с зашифрованными или сжатыми потоками данных в дополнение к тому, что позволяет ОС.
Принцип подстановки Лискова утверждает, что всегда должна быть возможность заменить тип подтипом. В C у вас не часто есть подтипы, но вы можете применять принцип на уровне модуля: код должен быть спроектирован так, чтобы использование расширенной версии модуля, например, более новой версии, не нарушало его. Расширенная версия модуля может использовать struct
который имеет больше полей, чем оригинал, больше полей в enum
и тому подобное, поэтому ваш код не должен предполагать, что переданная структура имеет определенный размер или что значения перечисления имеют определенный максимум.
Одним из примеров этого является то, как адреса сокетов реализованы в API сокетов BSD: существует «абстрактный» тип сокетов struct sockaddr
который может обозначать любой тип адреса сокета и конкретный тип сокета для каждой реализации, такой как struct sockaddr_un
для доменных сокетов Unix и struct sockaddr_in
для IP-сокетов. Функции, которые работают с адресами сокетов, должны передавать указатель на данные а также размер конкретного типа адреса.
Во-первых, это помогает думать о Зачем у нас есть эти принципы проектирования. Почему следование принципам SOLID делает программное обеспечение лучше? Работайте, чтобы понять цели каждого принципа, а не только конкретные детали реализации, необходимые для их использования на определенном языке.
Обратите внимание на то, как каждый принцип способствует улучшению определенного атрибута системы, будь то более высокая сплоченность, более слабая связь или модульность.
Помните, ваша цель — выпускать высококачественное программное обеспечение. Качество состоит из множества различных атрибутов, включая правильность, эффективность, ремонтопригодность, понятность и т. Д. Соблюдение принципов SOLID поможет вам в этом. Так что, как только вы поймете «почему» принципов, «как» реализации станет намного проще.
РЕДАКТИРОВАТЬ:
Я постараюсь более прямо ответить на ваш вопрос.
Для принципа открытия / закрытия правило состоит в том, что как подпись, так и поведение старого интерфейса должны оставаться одинаковыми до и после любых изменений. Не нарушайте код, который его вызывает. Это означает, что для реализации новой вещи абсолютно необходим новый интерфейс, потому что у старой вещи уже есть поведение. Новый интерфейс должен иметь другую подпись, потому что он предлагает новую и другую функциональность. Таким образом, вы отвечаете этим требованиям в C точно так же, как в 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 () не должно быть скрытых зависимостей, или, если они есть, они также должны быть совместимы. В этом упрощенном примере обе функции полагаются только на стандартный вывод, поэтому это не имеет большого значения. Идея состоит в том, что вы сохраняете правильное поведение в обеих ситуациях, когда любая функция может использоваться взаимозаменяемо.
Самая близкая вещь, о которой я могу подумать, — это не совсем идеально (и она не идеальна, поэтому, если у кого-то есть идея получше, он может сразиться со мной), в основном, когда я пишу функции для какой-то библиотеки. ,
Для подстановки Лискова, если у вас есть файл заголовка, который определяет ряд функций, вы не хотите, чтобы функциональность этой библиотеки зависела от который реализация у вас функций; Вы должны быть в состоянии использовать любую разумную реализацию и ожидать, что ваша программа сделает свое дело.
Что касается принципа Open / Closed, если вы хотите реализовать библиотеку ввода / вывода, вы хотите иметь функции, которые выполняют минимум (например, read
а также write
). В то же время вы можете использовать их для разработки более сложных функций ввода-вывода (например, scanf
а также printf
), но вы не собираетесь изменять код, который сделал минимум.
Я вижу, что прошло много времени с тех пор, как вопрос был открыт, но я думаю, что это стоит более нового взгляда.
Пять принципов SOLID относятся к пяти аспектам программных объектов, как показано в Сплошная диаграмма. Хотя это диаграмма классов, она в основном может обслуживать другие типы идентификаторов SW. Интерфейсы, предоставляемые для вызывающих абонентов (стрелка влево, обозначает сегрегацию интерфейса) и интерфейс, запрашиваемый как вызываемые (стрелка вправо, обозначает инверсию зависимости), также могут быть классическими интерфейсами функций и аргументов Си.
Верхняя стрелка (стрелка расширения означает принцип подстановки Лискова) работает для любой другой реализации подобного объекта. Например, если у вас есть API для связанного списка, вы можете изменить реализацию его функций и даже структуру векторного «объекта» (предполагая, например, что он сохраняет структуру исходного, как в BSD). Пример сокетов, или это непрозрачный тип). Конечно, это не так элегантно, как Object в языке ООП, но оно следует тому же принципу и может использоваться, например, с использованием динамического связывания.
Аналогичным образом нижняя стрелка (стрелка обобщения означает расшифровку / закрытие) определяет, определяется ли ваша сущность и что открыто. Например, некоторые функциональные возможности могут быть определены в одном файле и не должны заменяться, в то время как другие функциональные возможности могут вызывать другой набор API, который позволяет использовать различные реализации.
Таким образом, вы также можете написать SOLID SW с C, хотя, вероятно, это будет сделано с использованием сущностей более высокого уровня и может потребовать некоторой дополнительной разработки.