C
TDD
embedded
デザインパターン
googletest

組込みに近いものをTDDで開発してみる〜デザインパターン編〜

googletest/googlemockを使って、組込みLinux上で動作するソフトウェアをTDDで開発しています。
実際にTDDで開発している手順をリアルタイムでそのまま書いているため、行き当たりばったりな部分があります
過程も楽しんで頂ければ幸いです。
不明な点、間違っている点があれば、コメント下さい。励みになります。

これまでの経緯を詳しく知りたい方は、前回までの記事をご覧下さい。
組込みに近いものをTDDで開発してみる〜準備編〜
組込みに近いものをTDDで開発してみる〜問題提起編〜
組込みに近いものをTDDで開発してみる〜file open編〜
組込みに近いものをTDDで開発してみる〜libevdev初期化/終了処理編〜
組込みに近いものをTDDで開発してみる〜キー入力検知編〜
組込みに近いものをTDDで開発してみる〜中間振り返り編〜
組込みに近いものをTDDで開発してみる〜インタフェース継承編〜

いよいよ大詰めです。
前回、次の仕様を実装しました。

  • キーボードの「A」ボタンを押すと、caps lockのLEDが点灯し、5秒後に消灯する。消灯するまでは、「A」キー押下を無視する。

キー入力検出とタイマ検出は、EventDetectorインタフェースを共通の基底クラスとすることで、同一インタフェースで使えるようになりました。
今回は、LEDへの異なる操作を、同一インタフェースで使えるようにして、さらにプログラムの拡張性を高めます。

LED操作をどうしたいか

今のLEDドライバには、LEDを操作する3つの関数がある。

led_driver.h
void TurnOnLed(LedDriver self);
void TurnOffLed(LedDriver self);
void ToggleLed(LedDriver self);

これらの3つを同じインタフェースで使用し、main関数を誤魔化しのない状態にしたい。

main.c
  for(int i = 0; detectors[i] != NULL; i++) {
    StartEventDetector(detectors[i]);
    // イベント検出まで待つ
    while(CheckEvent(detectors[i]) != DETECTOR_EVENT_DETECTED) {}

    ToggleLed(caps_led);  // ここで共通インタフェースを呼び出し、On/Offを正しく切り替えたい
    // 仕様を満たすためには、本当は下のように書かなければならない。
    // if (i == 0) TurnOnLed(caps_led);
    // else if (i == 1) TurnOffLed(caps_led);
  }

Commandパターン

そこで、LEDの操作をCommandパターンとして実装する。
Commandパターンは、『命令』をインスタンスとして表現できるデザインパターンだ。

Commandパターンは笑ってしまうほど単純だ(と言ってもC言語だとぼちぼちの量になる)。

command.h
typedef struct CommandInterfaceStruct *CommandInterface;
typedef struct CommandStruct *Command;

typedef struct CommandStruct {
  CommandInterface vtable;
} CommandStruct;

// インタフェースはExecute()だけ
typedef struct CommandInterfaceStruct {
  void (*Execute)(Command);
} CommandInterfaceStruct;

void CommandExecute(Command cmd);

Commandインタフェースを継承するクラスは、Execute()だけを実装していれば良い。
ただ、この単純な実装に、びっくりするほどの柔軟性がある。

LED操作Commandの実装

いつも通り、テストから考えていこう。

// LEDをOnするCommandを生成、実行するテスト
TEST_F(LedOperatorTest, LedTrunOnOperationCallsIoWriteWithOne) {
  // driver_はLedDriverのインスタンス
  Command command = LedOperatorFactory(driver_, OP_LED_TURN_ON);
  EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("1\n"), 2)).Times(1);
  CommandExecute(command);
}

少なくとも、On/Off/Toggleの3つのLED操作Commandが必要なことが分かっている。
そこで、FactoryMethodパターンを用意して、引数で生成インスタンスを切り替えることにした。

まず、LED操作の派生クラスは次のようにした。
Commandインタフェースを継承する以外は、LedDriverのインスタンスがあるだけだ。

led_operator_factory.c
typedef struct LedOperatorStruct {
  CommandStruct base;
  LedDriver driver;
} LedOperatorStruct;

typedef struct LedOperatorStruct *LedOperator;

LEDをOnするExecute()の実装は、次のようになる。

led_operator_factory.c
static void LedTurnOn(Command super) {
  LedOperator self = (LedOperator)super;
  TurnOnLed(self->driver);
}

static CommandInterfaceStruct interface = {
  .Execute = LedTurnOn
};

後は、インスタンスを作ってあげるだけだ。
この時点では、LED操作を切り替える引数は使っていないが、この後すぐ実装する。

led_operator_factory.c
Command LedOperatorFactory(LedDriver driver, int32_t op_id) {
  if (driver == NULL) return NULL;
  LedOperator command = calloc(1, sizeof(LedOperatorStruct));
  command->base.vtable = &interface;
  command->driver = driver;

  return (Command)command;
}

ここまで作れば、LEDをOnするテストが通るようになる。

少し説明が雑になるが、順次、次のテストを用意し、実装を進めていった。

// OffするCommandを作生成、実行するテスト
TEST_F(LedOperatorTest, LedTrunOffOperationCallsIoWriteWithZero) {
  Command command = LedOperatorFactory(driver_, OP_LED_TURN_OFF);
  EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("0\n"), 2)).Times(1);
  CommandExecute(command);
}

// ToggleするCommandを生成、実行するテスト
TEST_F(LedOperatorTest, LedToggleOperationTogglesIoWrite) {
  Command command = LedOperatorFactory(driver_, OP_LED_TOGGLE);

  InSequence s;
  EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("1\n"), 2)).Times(1);
  EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("0\n"), 2)).Times(1);
  EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("1\n"), 2)).Times(1);

  CommandExecute(command);
  CommandExecute(command);
  CommandExecute(command);
}

// FactoryがCommand生成に失敗するテスト
TEST_F(LedOperatorTest, FatoryReturnsNullIfFailed) {
  Command command = LedOperatorFactory(nullptr, OP_LED_TURN_ON);
  EXPECT_EQ(nullptr, command);

  command = LedOperatorFactory(driver_, -1);
  EXPECT_EQ(nullptr, command);

  command = LedOperatorFactory(driver_, OP_MAX_FACTORY_ID);
  EXPECT_EQ(nullptr, command);
}

最終的なプロダクトコードは次のようになる。

led_operator_factory.c
static void LedTurnOn(Command super) {
  LedOperator self = (LedOperator)super;
  TurnOnLed(self->driver);
}

static void LedTurnOff(Command super) {
  LedOperator self = (LedOperator)super;
  TurnOffLed(self->driver);
}

static void LedToggle(Command super) {
  LedOperator self = (LedOperator)super;
  ToggleLed(self->driver);
}

static CommandInterfaceStruct interface[OP_MAX_FACTORY_ID] = {
  { .Execute = LedTurnOn },
  { .Execute = LedTurnOff },
  { .Execute = LedToggle }
};

Command LedOperatorFactory(LedDriver driver, int32_t op_id) {
  if (driver == NULL) return NULL;
  if (op_id <= -1 || OP_MAX_FACTORY_ID <= op_id) return NULL;

  LedOperator command = calloc(1, sizeof(LedOperatorStruct));
  command->base.vtable = &interface[op_id];
  command->driver = driver;

  return (Command)command;
}

これで、LEDのOnだろうが、Offだろうが、Toggleだろうが、全て、Commandインタフェースから実行できるようになった。

プロダクトコード

最終的なプロダクトコードは次の通りだ。

main.c
int main(void) {
  struct timeval kTime = {};
  const struct input_event kPressA = {kTime, EV_KEY, KEY_A, INPUT_KEY_PRESSED};

  EventDetector detectors[NUM_OPERATION_ON_DETECTION];
  detectors[0] = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressA);
  detectors[1] = CreateTimeOutDetector(5000, TIMER_ONE_SHOT);

  LedDriver caps_led = CreateLedDriver();
  if (InitLedDriver(caps_led, LED_DEVICE) != LED_DRIVER_SUCCESS) {
    DEBUG_LOG("Fail to init led device\n");
    exit(1);
  }

  Command operators[NUM_OPERATION_ON_DETECTION];
  operators[0] = LedOperatorFactory(caps_led, OP_LED_TURN_ON);
  operators[1] = LedOperatorFactory(caps_led, OP_LED_TURN_OFF);

  // EventDetectorとCommandインタフェースだけで、仕様を実現している
  for(int i = 0; i < NUM_OPERATION_ON_DETECTION; i++) {
    StartEventDetector(detectors[i]);
    while(CheckEvent(detectors[i]) != DETECTOR_EVENT_DETECTED) {}

    CommandExecute(operators[i]);
  }
// 終了処理は省略
}

ここまでできれば、様々な種類のイベント検出し、何らかの操作をする、という機能が、全て同じインタフェースで実装できる。

2秒のあいだにボタンが3回押されたことを検出する

次の問題を実装する(ただし、レギュレーションはけっこう無視する)。
(問題) 2秒のあいだにボタンが3回押されたかで、真・偽を返す関数を実装してください。

上記の問題では、2秒経過をタイマで計りながら、ボタン押下を検出する。
個々の要素は大したことないが、2つのことを同時にやる必要がある。

今回は、簡易な並列処理のデザインパターンであるActiveObjectパターンを使って、この問題を解いてみる。

話が前後してしまうため、テストの記載は省略しますが、これ以降も全てTDDで開発しています。
ActiveObjectに関するテストはこちら。
active_object_test.c

ActiveObjectパターン

このパターンは、APIとコアとなる実装を見た方が、理解しやすいと思う。
大雑把にいうと、実行したいCommandを複数追加しておいて、Run()で一気に実行するだけだ。

active_object_engine.h
// engineにCommandを追加する
void FuelEngine(ActiveObjectEngine engine, Command cmd);

// engineに搭載されているCommandを実行する
void EngineRuns(ActiveObjectEngine engine);
active_object_engine.c
typedef struct ActiveObjectEngineStruct {
  // glibのリストを拝借。FuelEngineで追加されたCommandリストを保持する。
  GSList *commands;
} ActiveObjectEngineStruct;

void FuelEngine(ActiveObjectEngine engine, Command cmd) {
  if ((engine == NULL) || (cmd == NULL)) return;
  // Commandをengineに追加する
  engine->commands = g_slist_append(engine->commands, (gpointer)cmd);
}

void EngineRuns(ActiveObjectEngine engine) {
  while(g_slist_length(engine->commands) > 0) {
    // engineにCommandが存在する間、Commandを実行し続ける。
    Command cmd = (Command)g_slist_nth_data(engine->commands, 0);
    CommandExecute(cmd);
    // 一度実行したコマンドはengineから除く
    engine->commands = g_slist_remove(engine->commands, (gpointer)cmd);
  }
}

ここまで見る限りは、なんてことないように見える。
ActiveObjectの本領は、Commandがengineに自分自身を追加し始めてからだ

detect_chain.c
// このDetectChainは、後程コナミコマンドを検出するために使用する。
// イベントを検出するとwakeup Commandを実行するCommand
Command CreateDetectChain(EventDetector detector,
                          // 自分を実行するengineを引数に取る
                          ActiveObjectEngine engine,
                          // イベント検出時にengineに追加するコマンド
                          Command wakeup) {
  DetectChain self = calloc(1, sizeof(DetectChainStruct));
  self->base.vtable = &interface;
  self->detector = detector;
  self->wakeup = wakeup;
}

static void DetectChainExecute(Command super) {
  DetectChain self = (DetectChain)super;

  if (CheckEvent(self->detector) == DETECTOR_EVENT_DETECTED) {
    // イベントが検出されると、wakeup Commandをengineに追加する
    FuelEngine(self->engine, self->wakeup);
    self->detector_started = false;
    CleanupEventDetector(self->detector);
  } else {
    // イベントが検出されるまでは自分自身をengineに戻す
    FuelEngine(self->engine, (Command)self);
  }
}

static CommandInterfaceStruct interface = {
  .Execute = DetectChainExecute
};

なんとなく見えてきただろうか?

つまり、2秒経過検出でengineを止めるCommandと、ボタン押下検出をカウントするCommandをengineに追加してあげれば、2つのCommandが同時に動き続ける。
engineが停止した段階で、ボタン押下カウンタの値を取り出してあげれば完成だ。

ActiveObjectは、同時に動くオブジェクトは1つだけ、という並列処理のパターンだ。
個人的には、かなりお気に入りのパターンだが、本当にマルチスレッドが必要とされるような場合には使えない。
フットプリントが軽いので、貧弱な環境では十分使えるはずだ。

2秒のあいだにボタンが3回押されたかで、真・偽を返す実装

プログラム実行直後から、2秒の間に、「A」ボタンが何回押されたか、を出力するようにした。

実行結果
sudo led_controller/three_times_in_two_sec
Press A key 3 times in two seconds

以下、重要な部分のみ抜粋。

three_times_in_two_sec.c
int main(void) {
  struct timeval kTime = {};
  const struct input_event kPressA = {kTime, EV_KEY, KEY_A, INPUT_KEY_PRESSED};

  ActiveObjectEngine engine = CreateActiveObjectEngine();

  // Create components.
  EventDetector press_a = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressA);
  EventDetector two_sec = CreateTimeOutDetector(2000, TIMER_ONE_SHOT);
  Command total = CreateCountTotal();
  Command halt = CreateHaltEngine(engine);

  TriggerActionPair one = CreateTriggerActionPair(press_a, total);
  TriggerActionPair two = CreateTriggerActionPair(two_sec, halt);

  Command cmd_one = CreateActionOnTriggerChain(one, engine, LOOP_CHAIN);
  Command cmd_two = CreateActionOnTriggerChain(two, engine, ONE_SHOT_CHAIN);
  FuelEngine(engine, cmd_one);
  FuelEngine(engine, cmd_two);

  EngineRuns(engine);

  // 3と比較すれば、真偽が返る
  printf("Press A key %d times in two seconds\n.", TotalIs(total));

  // 終了処理
  return 0;
}
action_on_trigger.c
// イベント検出時、所定のCommandを実行するCommand
typedef struct TriggerActionPairStruct {
  EventDetector detector;
  Command command;
} TriggerActionPairStruct;

typedef struct ActionOnTriggerChainStruct {
  CommandStruct base;
  TriggerActionPair chain;
  ActiveObjectEngine engine;
  int32_t loop_flag;
  bool started;
} ActionOnTriggerChainStruct;
typedef struct ActionOnTriggerChainStruct *ActionOnTriggerChain;

static void ExecuteActionOnTrigger(Command super) {
  ActionOnTriggerChain self = (ActionOnTriggerChain)super;

  if (!self->started) {
    StartEventDetector(self->chain->detector);
    self->started = true;
  }

  // イベント検出時
  if (CheckEvent(self->chain->detector) == DETECTOR_EVENT_DETECTED) {
    FuelEngine(self->engine, self->chain->command);
    CleanupEventDetector(self->chain->detector);
    self->started = false;
    if (self->loop_flag == ONE_SHOT_CHAIN)
      return;  // Finish this chain by avoiding FuelEngine().
  }

  FuelEngine(self->engine, (Command)self);
}

static CommandInterfaceStruct interface = {
  .Execute = ExecuteActionOnTrigger
};

TriggerActionPair CreateTriggerActionPair(EventDetector detector, Command command) {
  TriggerActionPair pair = calloc(1, sizeof(TriggerActionPairStruct));
  pair->detector = detector;
  pair->command = command;
  return pair;
}

// Should give ActiveObjectEngine.
Command CreateActionOnTriggerChain(TriggerActionPair chain,
                                   ActiveObjectEngine engine,
                                   int32_t loop_flag) {
  ActionOnTriggerChain self = calloc(1, sizeof(ActionOnTriggerChainStruct));
  self->base.vtable = &interface;
  self->chain = chain;
  self->engine = engine;
  self->loop_flag = loop_flag;
  self->started = false;

  return (Command)self;
}

残りの細かい部分は、GitHubを見てほしい。
ソースコード全体
three_times_in_two_sec.cがmainを含むソースコード。

コナミコマンドを検出する

先ほど紹介した、DetectChainを使って、コナミコマンドを検出する。
コマンドを検出すると、caps lockのLEDを点灯して終了する。途中で間違えるとプログラムは何もせず終了する。
検出するイベントが多いので、少し下準備が長くなっている。
マトリョーシカのようにDetectChain Commandを作っていけば、あとはActiveObjectEngineを動かすだけだ。

konami_command.c
int main(void) {
  struct timeval kTime = {};
  const struct input_event kPressUp = {kTime, EV_KEY, KEY_UP, INPUT_KEY_PRESSED};
  const struct input_event kPressDown = {kTime, EV_KEY, KEY_DOWN, INPUT_KEY_PRESSED};
  const struct input_event kPressLeft = {kTime, EV_KEY, KEY_LEFT, INPUT_KEY_PRESSED};
  const struct input_event kPressRight = {kTime, EV_KEY, KEY_RIGHT, INPUT_KEY_PRESSED};
  const struct input_event kPressA = {kTime, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
  const struct input_event kPressB = {kTime, EV_KEY, KEY_B, INPUT_KEY_PRESSED};

  LedDriver caps_led = CreateLedDriver();
  if (InitLedDriver(caps_led, LED_DEVICE) != LED_DRIVER_SUCCESS) {
    DEBUG_LOG("Fail to init led device\n");
    exit(1);
  }

  // Create components.
  EventDetector press_up = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressUp);
  EventDetector press_down = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressDown);
  EventDetector press_left = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressLeft);
  EventDetector press_right = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressRight);
  EventDetector press_a = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressA);
  EventDetector press_b = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressB);
  Command caps_on = LedOperatorFactory(caps_led, OP_LED_TURN_ON);

  ActiveObjectEngine engine = CreateActiveObjectEngine();
  Command acceptted = CreateDetectChain(press_a, engine, caps_on);
  Command uuddlrlrb = CreateDetectChain(press_b, engine, acceptted);
  Command uuddlrlr = CreateDetectChain(press_right, engine, uuddlrlrb);
  Command uuddlrl = CreateDetectChain(press_left, engine, uuddlrlr);
  Command uuddlr = CreateDetectChain(press_right, engine, uuddlrl);
  Command uuddl = CreateDetectChain(press_left, engine, uuddlr);
  Command uudd = CreateDetectChain(press_down, engine, uuddl);
  Command uud = CreateDetectChain(press_down, engine, uudd);
  Command uu = CreateDetectChain(press_up, engine, uud);
  Command start = CreateDetectChain(press_up, engine, uu);

  FuelEngine(engine, start);

  EngineRuns(engine);

  return 0;
}

2秒で3回ボタン押下検出にせよ、コナミコマンド検出にせよ、これまでに作ったキー入力検出、タイマ、LED操作のモジュールには一切変更を加えていない。
これが大きなポイントとなる。

最後に

@mt08 さん、おもしろい問題を(タイムリーに)提供して頂き、ありがとうございました。
だいぶレギュレーションを無視していますが、かっこいい実装になっていますでしょうか?

組込みにちかいものをTDDで開発してみる、は一旦ここまでとさせて頂きます。
モチベーションが回復すれば、ラズパイあたりに移植して動かしてみます。
最後の方、細かいことが説明しきれなくなって、色々端折ってしまいました…。

後は、コメントで質問やマサカリ頂けば嬉しいです。