😽

具体的な設計(殴り書き) (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