Реализация СОЛИДНЫХ принципов для 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), но вы не собираетесь изменять код, который сделал минимум.