Избегайте глобальных переменных во встроенном программировании

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

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

У нас есть несколько глобальных массивов структур и несколько глобальных классов. Существует один массив структур для мьютексов, один для семафоров и один для очередей сообщений (они инициализируются в main). Для каждого запущенного потока класс, которому он принадлежит, является глобальной переменной.

Самая большая проблема, с которой я столкнулся, заключается в модульном тестировании. Как я могу вставить фиктивный объект, когда класс, который я хочу протестировать, глобальные переменные #includes, которых я не делаю?

Вот ситуация в псевдокоде:

foo.h

#include "Task.h"
class Foo : Task {
public:
  Foo(int n);
  ~Foo();
  doStuff();
private:
  // copy and assignment operators here
}

бар.ч

#include <pthread.h>
#include "Task.h"

enum threadIndex { THREAD1 THREAD2 NUM_THREADS };
struct tThreadConfig {
  char      *name,
  Task      *taskptr,
  pthread_t  threadId,
  ...
};
void startTasks();

bar.cpp

#include "Foo.h"

Foo foo1(42);
Foo foo2(1337);
Task task(7331);

tThreadConfig threadConfig[NUM_THREADS] = {
  { "Foo 1", &foo1, 0, ... },
  { "Foo 2", &foo2, 0, ... },
  { "Task",  &task, 0, ... }
};

void FSW_taskStart() {
    for (int i = 0; i < NUMBER_OF_TASKS; i++) {
        threadConfig[i].taskptr->createThread(  );
    }
}

Что делать, если я хочу больше или меньше задач? Другой набор аргументов в конструкторе foo1? Я думаю, что мне нужно было бы иметь отдельные bar.h и bar.cpp, что кажется гораздо более трудоемким, чем необходимо.


person Nate Parsons    schedule 13.08.2009    source источник
comment
Я предполагаю, что вы имеете в виду «& foo1», а не «% foo1» (оператор модуля)?   -  person DaveR    schedule 14.08.2009
comment
Спасибо. Вот что я получаю за переписывание вместо копирования/вставки   -  person Nate Parsons    schedule 14.08.2009


Ответы (3)


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

По сути, использование компоновщика для вставки фиктивных/фальшивых объектов и функций должно быть последним средством, но все же вполне допустимо.

Однако вы также можете использовать инверсию управления, без фреймворка это может возложить некоторую ответственность на клиентский код. Но это действительно помогает тестировать. Например, чтобы проверить FSW_taskStart()

tThreadConfig threadConfig[NUM_THREADS] = {
  { "Foo 1", %foo1, 0, ... },
  { "Foo 2", %foo2, 0, ... },
  { "Task",  %task, 0, ... }
};

void FSW_taskStart(tThreadConfig configs[], size_t len) {
    for (int i = 0; i < len; i++) {
        configs[i].taskptr->createThread(  );
    }
}

void FSW_taskStart() {
    FSW_taskStart(tThreadConfig, NUM_THREADS);
}

void testFSW_taskStart() {
    MockTask foo1, foo2, foo3;
    tThreadConfig mocks[3] = {
          { "Foo 1", &foo1, 0, ... },
          { "Foo 2", &foo2, 0, ... },
          { "Task",  &foo3, 0, ... }
        };
    FSW_taskStart(mocks, 3);
    assert(foo1.started);
    assert(foo2.started);
    assert(foo3.started);
}

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

person iain    schedule 13.08.2009
comment
Что ж, мне повезло, что это еще не унаследованный код, поэтому я хочу получить все прямо сейчас. Я уже имел в виду IOC, но пример помог мне поверить, что это возможно. Таким образом, производственный код может использовать глобальные переменные, поэтому вы можете предсказать, где они будут находиться в памяти, но тестовый код может просто не включать bar.cpp. Я все еще не уверен. Как вы предложили, мне не нужно будет менять какие-либо файлы .h, просто включить разные файлы .cpp в проект модульного теста? - person Nate Parsons; 14.08.2009
comment
+1. DIP и IOC являются хорошим архитектурным шаблоном, особенно для ... тестирования с фиктивным объектом. Если вы не можете изменить сигнатуру своей функции, вы можете сделать это с некоторой косвенностью. Вместо передачи объекта контекста вы можете вызвать функцию, которая его возвращает. Эта функция может использовать IOC и возвращать объекты реального контекста или фиктивный объект в зависимости от его инициализации... - person neuro; 14.08.2009
comment
@drhorrible, название книги может немного вводить в заблуждение ;-) Его определение унаследованного кода - это код без модульного тестирования. В основном это набор методов для модульного тестирования в различных сложных сценариях. - person iain; 14.08.2009
comment
Я рад, что вам понравилось, в нем есть несколько отличных советов по тестированию кода, который не выглядит тестируемым. - person iain; 14.09.2009

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

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

Для каждой среды (целевой, симулятор, модульный тест...) вы создаете одну функцию «конфигурации», которая создает все необходимые объекты, драйверы и все потоки, предоставляя потокам их список зависимостей. Например, целевая конфигурация может создать USB-драйвер и внедрить его в какой-либо поток связи, а тестовая конфигурация модуля связи может создать заглушку USB-драйвера, которым будут управлять тесты.

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

Это немного больше работы, чем статические списки объектов, но гибкость фантастическая, особенно когда вы сталкиваетесь с некоторыми сложными проблемами интеграции и хотите поменять местами компоненты для тестирования.

Грубо:

// Config specific to one target.
void configure_for_target_blah(System_config& cfg)
{   // create drivers
    cfg.drivers.push_back("USB", new USB_driver(...))
    // create threads
    Thread_cfg t;
    t.main = comms_main; // main function for that thread
    t.drivers += "USB"; // List of driver names to pass as dependencies
    cfg.threads += t;
}

// Main function for the comms thread.
void comms_main(Thread_config& cfg)
{
    USB_driver* usb = cfg.get_driver("USB");
    // check for null, then store it and use it...
}

// Same main for all configs.
int main()
{
    System_config& cfg;
    configure_for_target_blah(cfg);
    //for each cfg.drivers
    //    initialise driver
    //for each cfg.threads
    //    create_thread with the given main, and pass a Thread_config with dependencies
}
person squelart    schedule 14.08.2009

Вы можете выделить память, используя malloc, а затем получить новый оператор, чтобы сделать объект в этой позиции

void* mem = malloc(3*sizeof(SomeClass));
SomeClass *a = new(mem) SomeClass();
mem += sizeof(SomeClass);
SomeClass *b = new(mem) SomeClass();
mem += sizeof(SomeClass);
SomeClass *c = new(mem) SomeClass();

поэтому вы можете выделить всю память, а затем выделить ее по своему усмотрению. Примечание: убедитесь, что вы вызываете деконструкцию вручную, так как это не происходит при вызове удаления.

person Lodle    schedule 14.08.2009
comment
-1, malloc так же запрещен в этих средах, как и новый. Это добавляет боли без выгоды. - person MSalters; 14.08.2009
comment
Значит, выделение одного блока памяти в начале запрещено? Кажется, это глупое правило. - person Lodle; 31.08.2009