具体的な設計(殴り書き) (PC編)
時間があれば、まとめます。
応用編
コードや考え方は、ちょうぜつ本からの引用です。
自分の考えを入れました。
usbデバイスからマウスやキーボードが生えているとする。
こいつの設計は、
class usb------ class keybord
|
------- class mouse
なのだが、これだとusbがそれぞれのクラスの接続する関数を持たなくちゃいけない。
class USBPort
{
private InternalBus $internalBus;
public function plugKeybord(USBKeyboard $keybord): void
{
$keyboard->connect($this->internalBus);
}
public function plugMouse(USBMouse $mouse){
$mouse->connect($this->internalBus);
}
}
class USBKeyboard
{
public function connect(InternalBus $bus): void {}
public function typeKey(string $code): void {}
}
class USBMouse
{
public function connect(InternalBus $bus): void {}
public function moveCursor(float $direction, float $distance): void {}
}
となるのは、クラスが増えたとき、USBPortクラスを変更しなくてはいけない。
抽象クラスを変更するときは、設計を疑う。
USBPortクラスが具体的な内容を知らないconnectメソッドを持ち、
具象クラスでconnectをそれぞれ持ち、そこで具体的な処理をかく。
呼び出し側は、USBPortの抽象的なconnectメソッドを使うが、実際は具体的な処理が行われる。
これが、ポリモーフィズム。
と思ったが、本書では、新たに抽象クラスUSBDeviceを追加して、plugをもらった時、connectされているか判別している。確かに、USBに接続するデバイスをまとめるクラスは作るべきだった。反省。
(どちらにしろ、ポリモーフィズムを実現するための工程であることには変わらないが...)
class usb --- abstract class AbstractUSBDevice ------ class keybord
|
------- class mouse
これにより、
abstract class AbstractUSBDevice
{
abstract public function connect(InternalBus $bus): void;
}
class USBPort
{
private InternalBus $internalBus;
public function plug(AbstractUSBDevice $device): void
{
$device->connect($this->internalBus);
}
}
class USBKeyboard extends AbstractUSBDevice
{
public function connect(InternalBus $bus): void {}
public function typeKey(string $code): void {}
}
class USBMouse extends AbstractUSBDevice
{
public function connect(InternalBus $bus): void {}
public function moveCursor(float $direction, float $distance): void {}
}
となる。
んで、pcの実際の操作を実装したいのだが、pcにはusbで接続してキーボードを使う場合や、ビルトインされたpcに内蔵してあるキーボードもある。
class BuiltinKeyboard {
public function typeKey(string $code):void {}
}
class BuiltinTrackpad {
public function moveCursor(float $direction, float $distance):void {}
}
class PCOerator
{
public function __construct(
protected BuiltinKeyboard $builtinKeyboard,
protected BuiltinTrackpad $builtinTrackpad,
protected ?USBKeyboard $usbKeyboard = null,
protected ?USBMouse $usbMouse = null,
) { }
public function inputText(array $codes): void
{
foreach( $codes as $code) {
if ($this->usbKeyboard) {
$this->usbKeyboard->typeKey($code);
} else {
$this->builtinKeyboard->typeKey($code);
}
}
public function pointAt(int $x, int $y) {
//inputtTextと同じような面倒がある。
}
}
同じようなデバイスの振る舞いをいちいちifなんかしていたら、if文がどんどん続いて行って、地獄みたいなことになる。大事なのは、キーボードがどんなキーボードだろうが、どんな振る舞いをするかなのであって、そこが本質である。(キーボードが行うアクション自体は、どんなキーボードかどうか知らなくていい。)
つまりそこを切り出す。(いやまあ、正直最初の段階から、振る舞いを切り出すべきだとずっと思っていたのだが、これは成長なのだろうか...)
interface KeyboardInterface
{
public function typeKey(string $code): void;
}
interface PointerDeviceInterface
{
public function moveCursor(float $direction, float $distance): void;
}
うむ。これで各々のデバイスの振る舞いを切り出せた。
class PCOperator
{
public function __construct(
protected KeyboardInterface $keyboard,
protected PointerDeviceInterface $pointerDevice
) {}
public function inputText(array $codes): void
{
foreach ($codes as $code) {
$this->keyboard->typeKey($code);
}
}
public function pointAt(int $x, int $y): void
{
// 仮に方向と距離を計算するなら
$direction = atan2($y, $x); // ラジアン
$distance = sqrt($x ** 2 + $y ** 2);
$this->pointerDevice->moveCursor($direction, $distance);
}
}
interface USBDeviceInterface
{
public function connect(InternalBus $bus): void;
}
class USBPort
{
private InternalBus $internalBus;
public function plug(AbstractUSBDevice $device): void
{
$device->connect($this->internalBus);
}
}
ここから具象
class BuiltinKeyboard implements KeyboardInterface
{
public function typeKey(string $code): void {}
}
class BuiltinTrackpad implements PointerDeviceInterface
{
public function moveCursor(float $direction, float $distance): void {}
}
class USBKeyboard implements KeyboardInterface ,USBDeviceInterface
{
public function connect(InternalBus $bus): void {}
public function typeKey(string $code): void {}
}
class USBMouse extends PointerDeviceInterface, USBDeviceInterface
{
public function connect(InternalBus $bus): void {}
public function moveCursor(float $direction, float $distance): void {}
}
自分は最初、もう一つ抽象度を上げて、function actionにして、inputTextとpointAtをactionとして行おうと思ってしまった。
つまり、
class PCOperator
{
public function __construct(
protected KeyboradInterface $keyboard,
protected PointerDeviceInterface $pointerDevice,
) { }
public function performAction(ActionInterface $action): void {
$action->execute();
}
}
これにして、具象クラスに詳細をやらせるのはどうかと思ったのだがこれだと、keyboardInterfaceとPointerDeviceInterfaceで分けた意味がないということ?でも、それは具象クラスで切り出せるんじゃなかったっけ?
なんでわざわざ、
class内で
public function inputText(array $codes): void
{
foreach ($codes as $code)
{
$this->keyboard->typeKey($code);
}
}
public function pointAt(int $x, int $y): void
{
$this->pointerDevice->moveCursor($direction, $distance);
}
と分けているのだろう。
最終的には、
// キーボード操作を定義
interface KeyboardInterface
{
public function typeKey(string $code): void;
}
// ポインターデバイスの操作を定義
interface PointerDeviceInterface
{
public function moveCursor(float $direction, float $distance): void;
}
// 実行可能なアクションを定義
interface ActionInterface
{
public function execute(): void;
}
interface USBDeviceInterface
{
public function connect(InternalBus $bus): void;
}
class PCOperator
{
public function __construct(
protected KeyboardInterface $keyboard,
protected PointerDeviceInterface $pointerDevice
) {}
// 任意のアクションを実行
public function performAction(ActionInterface $action): void
{
$action->execute();
}
}
class TypeTextAction implements ActionInterface
{
public function __construct(
private KeyboardInterface $keyboard,
private array $codes // タイプするキーコード
) {}
public function execute(): void
{
foreach ($this->codes as $code) {
$this->keyboard->typeKey($code);
}
}
}
class MoveCursorAction implements ActionInterface
{
public function __construct(
private PointerDeviceInterface $pointerDevice,
private float $direction, // ラジアン
private float $distance // 距離
) {}
public function execute(): void
{
$this->pointerDevice->moveCursor($this->direction, $this->distance);
}
}
class BuiltinKeyboard implements KeyboardInterface
{
public function typeKey(string $code): void
{
echo "キー '$code' をタイプしました。\n";
}
}
class BuiltinTrackpad implements PointerDeviceInterface
{
public function moveCursor(float $direction, float $distance): void
{
echo "カーソルを方向 $direction, 距離 $distance で移動しました。\n";
}
}
class USBKeyboard implements KeyboardInterface, USBDeviceInterface
{
public function connect(InternalBus $bus): void
{
echo "USBキーボードを接続しました。\n";
}
public function typeKey(string $code): void
{
echo "USBキーボードで '$code' をタイプしました。\n";
}
}
class USBMouse implements PointerDeviceInterface, USBDeviceInterface
{
public function connect(InternalBus $bus): void
{
echo "USBマウスを接続しました。\n";
}
public function moveCursor(float $direction, float $distance): void
{
echo "USBマウスでカーソルを方向 $direction, 距離 $distance で移動しました。\n";
}
}
classでそれぞれのアクションを作ることになるが、こっちのほうがいいのではないか。
でも、かえってコード増えてない?
各々のactionクラスとか。難しい。
Discussion