第14回

クラス設計

1クラス1責務。どの処理をどのクラスに置くかを考える。

1クラス1責務

例えば、

IMU

はセンサの読み取りや状態管理を担当する。一方、

PIDController

はPID計算を担当する。全部を1クラスに詰め込まない。

悪い例

FlightController
 ├── IMU読み取り
 ├── GPS読み取り
 ├── PID計算
 ├── サーボ制御
 ├── モータ制御
 ├── WiFi
 ├── ログ
 └── 状態管理

このクラスは、センサを変えても、制御を変えても、通信を変えても修正が必要になる。変更理由が多すぎる。

分割例

FlightController
 │
 ├── IMU
 ├── GPS
 ├── PIDController
 ├── ServoManager
 ├── MotorManager
 └── FlightState

それぞれのクラスに責務を持たせる。FlightControllerは「まとめ役」になり、細部は各クラスが持つ。

設計を考えるときの問い

  • このクラスは一言で何をするものか説明できるか
  • この変数はどのクラスが持つべきか
  • この処理はどのクラスの責務か
  • このメンバはprivateにすべきか
  • 共通インターフェースが必要か

動かして確かめる

演習に入る前に、まずこのサンプルを動かして結果を見る。追加の部品は要らない。光るのはボードに載っている内蔵LEDで、それ以外はシリアルモニタの出力で確かめられる。code/examples/14-design/を開き、pio run -t uploadで書き込んでからpio device monitorで出力を見る(手順と、外付けのLEDなどをつなぐ場合は開発環境にまとめてある)。

code/examples/14-design/
├── include/
│   ├── FlightController.hpp  まとめ役。順番に呼ぶだけ
│   ├── IMU.hpp               姿勢を読む
│   ├── PIDController.hpp     操作量を計算する
│   └── MotorManager.hpp      モータに出す
└── src/
    ├── FlightController.cpp / IMU.cpp / PIDController.cpp / MotorManager.cpp
    └── main.cpp
code/examples/14-design/src/FlightController.cpp
void FlightController::update() {
    // 1. 読む(IMUの責務)
    imu.update();
    float roll = imu.getRoll();

    // 2. 計算する(PIDControllerの責務)
    float correction = pid.compute(targetRoll - roll);

    Serial.print("roll=");
    Serial.print(roll);
    Serial.print(" correction=");
    Serial.println(correction);

    // 3. 出力する(MotorManagerの責務)
    motors.apply(correction);
}
code/examples/14-design/src/main.cpp
// 第14回:1クラス1責務
//
//   FlightController   まとめ役(順番に呼ぶだけ)
//    ├── IMU            姿勢を読む
//    ├── PIDController  操作量を計算する
//    └── MotorManager   モータに出す
//
// main.cpp にも FlightController にも、計算式は1つも書かれていない。
// 「センサを変える」「ゲインを変える」「出力先を変える」のどれをやっても
// 直すファイルが1つで済むことを確かめる。

#include <Arduino.h>

#include "FlightController.hpp"

FlightController controller;

void setup() {
    Serial.begin(115200);
    delay(1000);

    controller.begin();
}

void loop() {
    controller.update();
    delay(500);
}
シリアルモニタの出力
roll=9.60 correction=-15.84
  motor L=65 R=34
roll=7.68 correction=-12.19
  motor L=62 R=37
roll=6.14 correction=-10.23
  motor L=60 R=39

確かめること。

  • FlightController::update()に計算式が1つもない。「読む → 計算する → 出す」を順番に呼んでいるだけ
  • ゲインを変えたいときに開くのはFlightController.cppのコンストラクタだけ。PIDの式を直すならPIDController.cppだけ
  • PIDControllerはセンサもモータも知らない。compute(5.0f)を直接呼べば、この計算だけを切り離して確かめられる
  • これを全部1つのクラスに書いていたら、「センサを変える」「ゲインを変える」「出力先を変える」でどこを直すことになるかを考える

演習:責務を割り当てる

  • 第9回で作ったRobotStateは、どのクラスが持つべきかを議論する
  • 「距離センサの値が近ければ止まる」という判断は、センサとコントローラのどちらの責務かを考える
  • 悪い例のクラス図を自分の言葉で分割し直す