Javaにおけるツインパターン:同期化されたツインによる機能の倍増
ツインデザインパターンの目的
Javaにおけるツインデザインパターンは、共通の基底クラスから継承することなく、複数の関連するクラスを連携して動作させる方法を提供します。
現実世界の例を用いたツインパターンの詳細な説明
現実世界の例
ツインデザインパターンの現実世界の類似例として、ドライバーとドライビングシミュレーターの関係が挙げられます。ドライバー(最初のクラス)とドライビングシミュレーター(2番目のクラス)はどちらも、同じ車両制御装置(ステアリング、アクセル、ブレーキ)と、同じフィードバック(速度、エンジン状態)をやり取りする必要があります。
同様の機能を実行するにもかかわらず、ドライバーとシミュレーターは、物理世界と仮想環境という根本的に異なる環境で動作するため、共通の基底クラスを共有することはできません。代わりに、「ツイン」として関連付けられ、車両制御装置とフィードバックメカニズムとの一貫性のある相互作用が確保されます。この設定により、シミュレーターに対する改善や変更をドライバーに影響を与えることなく行うことができ、システム全体の柔軟性とレジリエンスが維持されます。
簡単に言うと
これは、2つの密接に結合されたサブクラスを形成し、2つの端を持つツインクラスとして機能させる方法を提供します。
Wikipediaによると
ツインパターンは、サポートされていない言語で多重継承をシミュレートすることを可能にするソフトウェアデザインパターンです。複数の親から継承する単一クラスを作成する代わりに、それぞれ親の1つから継承する2つの密接に関連するサブクラスが作成されます。これらのサブクラスは相互に依存しており、ペアとして連携して目的の機能を実現します。このアプローチは、多重継承に関連する複雑さと非効率性を回避しながら、異なるクラスからの機能の再利用を可能にします。
Javaにおけるツインパターンのプログラミング例
ボールが`GameItem`と`Thread`の両方として機能する必要があるゲームを考えてみましょう。両方から継承する代わりに、2つの密接に関連するオブジェクト`BallItem`と`BallThread`を使用してツインパターンを使用します。
こちらが`GameItem`クラスです
@Slf4j
public abstract class GameItem {
public void draw() {
LOGGER.info("draw");
doDraw();
}
public abstract void doDraw();
public abstract void click();
}
`BallItem`と`BallThread`サブクラス
@Slf4j
public class BallItem extends GameItem {
private boolean isSuspended;
@Setter
private BallThread twin;
@Override
public void doDraw() {
LOGGER.info("doDraw");
}
public void move() {
LOGGER.info("move");
}
@Override
public void click() {
isSuspended = !isSuspended;
if (isSuspended) {
twin.suspendMe();
} else {
twin.resumeMe();
}
}
}
@Slf4j
public class BallThread extends Thread {
@Setter
private BallItem twin;
private volatile boolean isSuspended;
private volatile boolean isRunning = true;
public void run() {
while (isRunning) {
if (!isSuspended) {
twin.draw();
twin.move();
}
try {
Thread.sleep(250);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public void suspendMe() {
isSuspended = true;
LOGGER.info("Begin to suspend BallThread");
}
public void resumeMe() {
isSuspended = false;
LOGGER.info("Begin to resume BallThread");
}
public void stopMe() {
this.isRunning = false;
this.isSuspended = true;
}
}
これらのクラスを一緒に使用するには
public class App {
public static void main(String[] args) throws Exception {
var ballItem = new BallItem();
var ballThread = new BallThread();
ballItem.setTwin(ballThread);
ballThread.setTwin(ballItem);
ballThread.start();
waiting();
ballItem.click();
waiting();
ballItem.click();
waiting();
// exit
ballThread.stopMe();
}
private static void waiting() throws Exception {
Thread.sleep(750);
}
}
`App`で何が起こるかを分解してみましょう。
- `BallItem`と`BallThread`のインスタンスが作成されます。
- `BallItem`と`BallThread`のインスタンスがお互いのツインとして設定されます。これは、各インスタンスがお互いを参照していることを意味します。
- `BallThread`が開始されます。これにより、`BallThread`クラスの`run`メソッドの実行が開始され、`BallThread`が中断されない限り、`BallItem`(そのツイン)の`draw`メソッドと`move`メソッドが継続的に呼び出されます。
- プログラムは750ミリ秒間待機します。これは、`BallThread`がその`run`メソッドを数回実行できるようにするためです。
- `BallItem`の`click`メソッドが呼び出されます。これにより、`BallItem`とそのツインである`BallThread`の`isSuspended`状態が切り替えられます。`BallThread`が実行中であれば中断され、中断されていれば再開されます。
- ステップ4と5が2回繰り返されます。これは、`BallThread`が1回中断され、再開されることを意味します。
- 最後に、`BallThread`の`stopMe`メソッドが呼び出され、その実行が停止されます。
コンソール出力
14:29:33.778 [Thread-0] INFO com.iluwatar.twin.GameItem -- draw
14:29:33.780 [Thread-0] INFO com.iluwatar.twin.BallItem -- doDraw
14:29:33.780 [Thread-0] INFO com.iluwatar.twin.BallItem -- move
14:29:34.035 [Thread-0] INFO com.iluwatar.twin.GameItem -- draw
14:29:34.035 [Thread-0] INFO com.iluwatar.twin.BallItem -- doDraw
14:29:34.035 [Thread-0] INFO com.iluwatar.twin.BallItem -- move
14:29:34.291 [Thread-0] INFO com.iluwatar.twin.GameItem -- draw
14:29:34.291 [Thread-0] INFO com.iluwatar.twin.BallItem -- doDraw
14:29:34.291 [Thread-0] INFO com.iluwatar.twin.BallItem -- move
14:29:34.533 [main] INFO com.iluwatar.twin.BallThread -- Begin to suspend BallThread
14:29:35.285 [main] INFO com.iluwatar.twin.BallThread -- Begin to resume BallThread
14:29:35.308 [Thread-0] INFO com.iluwatar.twin.GameItem -- draw
14:29:35.308 [Thread-0] INFO com.iluwatar.twin.BallItem -- doDraw
14:29:35.308 [Thread-0] INFO com.iluwatar.twin.BallItem -- move
14:29:35.564 [Thread-0] INFO com.iluwatar.twin.GameItem -- draw
14:29:35.564 [Thread-0] INFO com.iluwatar.twin.BallItem -- doDraw
14:29:35.565 [Thread-0] INFO com.iluwatar.twin.BallItem -- move
14:29:35.817 [Thread-0] INFO com.iluwatar.twin.GameItem -- draw
14:29:35.817 [Thread-0] INFO com.iluwatar.twin.BallItem -- doDraw
14:29:35.817 [Thread-0] INFO com.iluwatar.twin.BallItem -- move
この設定により、`BallItem`と`BallThread`は、多重継承なしで`GameItem`と`Thread`の両方の機能を活用して、ゲーム内で単一のまとまった単位として連携して動作できます。
Javaでツインパターンを使用するケース
- 異なるフレームワークや言語の使用など、さまざまな理由で共通の基底クラスから継承できない、共通の機能を共有するクラスをデカップリングする必要がある場合に使用します。
- 継承によって不要なオーバーヘッドが発生する可能性のある、パフォーマンスが重要なアプリケーションに役立ちます。
- 一方のツインをもう一方に影響を与えることなく置換または更新できる能力を通じて、レジリエンスを必要とするシステムに適用できます。
ツインパターンのJavaチュートリアル
Javaにおけるツインパターンの現実世界の応用
- レンダリングとロジックに異なるフレームワークが使用されているユーザーインターフェース。
- 直接的な継承が不可能な、レガシーコードと新しい実装を統合するシステム。
ツインパターンのメリットとトレードオフ
メリット
- クラス間の結合を減らし、モジュール性と保守性を向上させます。
- 異なるフレームワークや言語間でのクラスの柔軟性と再利用性を向上させます。
- 継承に関連するオーバーヘッドを回避することにより、パフォーマンスを向上させます。
トレードオフ
- 適切に管理されない場合、コードの重複につながる可能性があります。
- ツインクラス間の相互作用の管理が複雑になります。
関連するJavaデザインパターン
- アダプター:両方のパターンは互換性の問題に対処しますが、アダプターはインターフェースの変換に焦点を当てている一方、ツインは継承なしでクラスの連携に対処します。
- ブリッジ:実装から抽象化をデカップリングするという点で似ていますが、ツインは特に継承を回避します。
- プロキシ:オブジェクトアクセスを管理し、ツインが相互作用を処理する方法と似ていますが、プロキシは通常、制御とログに焦点を当てています。