第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.cppvoid 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);
}// 第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は、どのクラスが持つべきかを議論する - 「距離センサの値が近ければ止まる」という判断は、センサとコントローラのどちらの責務かを考える
- 悪い例のクラス図を自分の言葉で分割し直す