最近、「Raspberry Pi でおもちゃの車を自動走行させる」という試みを始めました。
ゆくゆくは自動走行させたいのですが、まずはキーボード入力に応じて車が動くように進めています。
こういったプログラムを書いていると、
- キーボードから入力を受け付ける処理
- 車の動作アルゴリズムを提供する処理
- Raspberry Pi のピンに出力してモータを動かす処理
などがごちゃごちゃになりやすいです。
遊びなのでごちゃごちゃなコードでも構わないのですが、せっかくなので、クリーンアーキテクチャ的な考え方でコードを整理しました。
※ この記事はコードの解説になるので、使った機器などは以下の記事を参照ください。
全体構成
整理したコードは、以下のような構成になりました。
ファイルの一覧は以下の通りです。
$ tree src/
src/
├── application
│ └── raspberryPiCarApplication.ts
├── domain
│ ├── car.ts
│ ├── carFactory.ts
│ └── motor.ts
├── index.ts
├── infrastructure
│ ├── dummy
│ │ ├── dummyMotor.ts
│ │ └── dummyMotorCarFactory.ts
│ └── rpio
│ ├── pwmMotor.ts
│ └── pwmMotorCarFactory.ts
├── logger.ts
└── presentation
├── dummyController.ts
└── keyboardController.ts
※ TypeScript で書いています。
コードの解説
ここから、各層のコードを見ていきます。
index.ts
このアプリケーションは、以下のコマンドで起動することになります。
$ sudo node dist/index.js
そこでまず最初に、起動時のエンドポイントとなる index.ts を見てみます。
index.ts の主要な処理は以下の内容だけになります。
const controller = new KeyboardController();
const carFactory = new PWMMotorCarFactory();
const app = new RaspberryPiCarApplication(controller, carFactory);
app.run();
これは、RaspberryPiCarApplication を構築し、実行しているだけです。
application 層
RaspberryPiCarApplication は、application 層で定義しています。
export default class RaspberryPiCarApplication {
private car: Car;
constructor(private controller: ControllerPort, carFactory: CarFactory) {
this.car = carFactory.create();
}
run() {
this.controller.enable((command) => {
switch (command) {
case ControllerCommand.GoStraight:
this.car.goStraight();
break;
case ControllerCommand.Stop:
this.car.stop();
break;
case ControllerCommand.GoRight:
this.car.goRight();
break;
case ControllerCommand.GoLeft:
this.car.goLeft();
break;
case ControllerCommand.CleanUp:
this.car.cleanUp();
break;
default:
throw new Error(`Unexpected command: ${command}`);
}
});
}
cleanUp() {
this.car.cleanUp();
}
}
RaspberryPiCarApplication の run メソッドでは、Controller から受け取った入力に応じて、Car インスタンスの対応する処理を呼び出しています。
application 層や domain 層は、ゲーム機で言うところの「本体」に相当します。
そこに対してプラガブルにコントローラを指して使いたいので、コントローラの仕様は application 層で以下のように定義しています。
// このアプリケーションが要求するコントローラの定義
export interface ControllerPort {
enable(send: (command: ControllerCommand) => void): void;
}
export enum ControllerCommand {
GoStraight,
Stop,
GoRight,
GoLeft,
CleanUp,
}
ゲーム機のポートにコントローラを差し込んで使うイメージから、インタフェースには「ControllerPort」という名前をつけました。
コントローラから受け取る信号は、「ControllerCommand」という型で定義しています。
ControllerPort から接続したコントローラを有効化 (enable) すると、入力を受け取ったタイミングで RaspberryPiCarApplication クラス内に記述した処理が動き、車の操作に繋がるという仕組みです。
presentation 層
ControllerPort に接続 (プログラミング的には implements) する KeyboardController は、presentation 層に配置しています。
export default class KeyboardController implements ControllerPort {
enable(send: (command: ControllerCommand) => void) {
keypress(process.stdin);
process.stdin.on("keypress", (ch, key) => {
logger.info(`[keypress] ch = ${ch}, key = ${JSON.stringify(key)}`);
if (!key) {
return;
}
// Ctrl + C
if (key.ctrl && key.name == "c") {
logger.info("Ctrl + C handling...");
send(ControllerCommand.CleanUp);
process.stdin.pause();
}
switch (key.name) {
case "up":
send(ControllerCommand.GoStraight);
break;
case "down":
send(ControllerCommand.Stop);
break;
case "right":
send(ControllerCommand.GoRight);
break;
case "left":
send(ControllerCommand.GoLeft);
break;
}
});
process.stdin.setRawMode(true);
process.stdin.resume();
}
}
キーボードの入力に応じて、RaspberryPiCarApplication 内に記述した処理が呼ばれるようになっています。
現状はキーボード入力を扱う KeyboardController を使っていますが、今後例えば WebSocket などでリモートから命令を出すことなども考えられます。
このコードのように presentation 層が application 層に依存するようにし、その逆向きの依存は排除することで、もしも別のコントローラを作成した場合でも application 層は書き換える必要がなくなります。
WebSocket で操作したくなった場合は、ControllerPort を実装した WebSocketController を記述し、index.ts で KeyboardController を new している箇所を WebSocketController に置き換えるだけです。
domain 層
RaspberryPiCarApplication がコントローラからの入力を受け取った際に操作している Car クラスは、domain 層で定義されています。
export default class Car {
constructor(
private frontRightMotor: Motor,
private frontLeftMotor: Motor,
private backRightMotor: Motor,
private backLeftMotor: Motor
) {}
goStraight() {
:
}
goRight() {
:
}
goLeft() {
:
}
stop() {
:
}
:
Car クラスは、4 つのモータ (Motor) から成ります。
直進、右折、左折、停止などのメソッドを持ち、その中でモータを操作しているわけです。
domain 層において、モータは interface だけ定義しています。
export default interface Motor {
changeToTopSpeed(): void;
changeToMiddleSpeed(): void;
stop(): void;
cleanUp(): void;
}
これは、domain 層を Rapsberry Pi のピンへの出力という技術詳細に依存させないためです。
こうすることで、ピンへの出力をモックしたテストが記述しやすくなったりもします。
infrastructure 層
Motor の具体実装は、infrastructure 層に配置しています。
export default class PWMMotor implements Motor {
protected pwmValue: number;
constructor(protected pin: number) {
:
}
changeToTopSpeed() {
this.changeSpeed(MAX_SPEED_VALUE);
}
changeToMiddleSpeed() {
this.changeSpeed(MIDDLE_SPEED_VALUE);
}
stop() {
this.changeSpeed(STOP_SPEED_VALUE);
}
:
Rapsberry Pi のピンからの出力の具体的な内容は、このクラスだけが知っています。
まとめ
構成図を再掲します。
このようにコードを整理することで、
- キーボードから入力を受け付ける処理
- 車の動作アルゴリズムを提供する処理
- Raspberry Pi のピンに出力してモータを動かす処理
などが分離され、コントローラやモータといった技術詳細がプラガブルになりました。
もともとここまで整理するつもりはなかったのですが、
- コントローラをプラガブルにする
- モータをプラガブルにする
ことを満たすようにコードを整理していたところ、自然にこれに近いかたちになったので、せっかくなのでこの状態まで持ってきてみました。
この規模だとここまでする必要はないかもしれませんが、アプリケーション設計の練習としては面白いと思います。
ソースコード
ソースコードの全容は GitHub の以下のページで公開しています。