LoginSignup
8
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-28

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

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

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9