まえがき
昨今においては、書籍や動画学習サイトなどにおいて、プログラミング言語の初心者の方に対して、RPG(ロールプレイングゲーム)の作り方を例に出して、プログラムの書き方を解説することが定番化しています。誰でもとは言わずとも、多くの方にとって、子どもの頃に親しんできた身近なモチーフで、頭にスーッと入りやすいという大きなメリットがあります。
そこで、プログラミング学習者にとって鬼門の一つである、GoFのデザインパターンを23種類、全部、RPGの作り方の例だけで、説明してみようというのが、本記事の試みです。
23パターン
Iteratorパターン
RPGでは、「やくそう」や「たいまつ」、「鉄の剣」などアイテム(道具)を荷物袋(リスト)の中選んで、使ったり捨てたり、あるいは、手に入れたりなどの操作をすることがあります。例えば、十字キーで操作するタイプのゲームの場合、下ボタンを押すと、次のアイテムが選択されることが一般的です。
RPGで要素を選択するのは何もアイテムだけではありません。戦闘になれば攻撃するターゲットや呪文などを選択しなければならないですし、質問をNPCからされたら「はい」「いいえ」などで答えなくてはいけません。
各要素に対する繰り返し処理を抽象化したものをItaratorといいます。
//荷物袋クラス
public class ItemBag{
//荷物袋には20個までのアイテムが入る
private int MAX_SIZE = 20;
//アイテムを選択するカーソルの位置
int index = 0;
//アイテムを収納する配列
private Item[] items = new Item[MAX_SIZE];
//次のアイテムがあるかどうか確認する
public boolean hasNext(){
if(index < MAX_SIZE){
return true;
}
return false;
}
//次のアイテムを選択する
public Item next(){
Item item = items[index];
index++;
return Items[index];
}
}
Adapterパターン
ゲームを作るにあたって、ドラゴンというクラスを作ってみました。作った時点では、敵として登場させるのか、NPCとして会話させるキャラクターなのかは決まっていませんでした。とりあえず、空を飛んで、火を吐くだけのキャラクターとして作成しました。ゲーム完成後、バージョンアップするときに、ドラゴンを敵として登場させて主人公に攻撃させる機能を追加してみることにしました。
そこで、すでにある炎を吐くというプログラムを再利用して攻撃するような新しい「Adepter(適合)」クラスを作るデザインパターンで実装することにしました。それが、ドラゴンクラスと敵キャラクタークラスの両方を継承したApapterクラス「悪のドラゴン」です。
適合クラスはWrapper(包むもの)パターンとも呼ばれ、ドラゴンクラスを悪のドラゴンクラスで包むことで、敵キャラクター用途として使えるように変換してくれる役割を果たしています。
//ドラゴン
public class Dragon{
//空を飛ぶ
public void fly(){
System.out.println("ドラゴンは空を飛んでいる!");
}
//火を吐く
public void fire(){
System.out.println("ドラゴンは火を吐いた!");
}
}
//敵キャラクター
public interface EnemyCharacter{
//攻撃する
public void attack():
}
//「ドラゴン」を「敵キャラクター」に「適合」させる「悪のドラゴン」クラス
public class EvilDragon extends Dragon implements EnemyCharacter{
//悪のドラゴンは火を吐く攻撃をする
public void attack(){
fire();
}
}
//実行クラス
public class Main{
public static void main(String args[]){
EnemyCharacter e = new EvilDragon();
//ドラゴンは火を吐いた!
e.attack();
}
}
Template Methodパターン
テンプレートメソッドパターンとはメソッドの使い方が抽象クラスに定義されていて、サブクラスで具体的な実装をするパターンです。たとえば、RPGの戦闘のターンのメッセージ流れは大まかに決まっています。
「①◯◯のターン!」→「②◯◯は××した!」→「③◯◯のターンは終了した!」
といった具合に。
①②③という順番で行動することは、どんなキャラクターのターンであっても変わりないため、親クラスで順番をあらかじめ定めておくことができます。
また、①と③はどんな敵が出てきても、◯◯の部分がキャラクターの名前に置き換わるだけで、同じメッセージが流れる定型文なので、あらかじめ、親クラスで実装しておくことができます。
②の部分だけは、スライムか魔法使いかによってどんなメッセージが出てくるか全然変わってくるので、継承先のするクラスで実装します。
「ねばねば攻撃でダメージを食らった!」「呪文による攻撃でダメージを与えた!」などなど
こうしておけば、新しいキャラクターのターンを追加するときに、キャラクターの名前と②さえ実装すれば①と③は共通処理化することができるので、楽勝になるメリットがあります。
//戦闘ターンクラス
public abstract class BattleTurn{
protected String name;
private void start(){
System.out.println(name+"のターン!");
}
protected abstract void attack();
private void end(){
System.out.println(name+"のターンは終了した!");
}
//敵のターン
public void turn(){
start();
attack();
end();
}
}
//スライムの攻撃クラス
public class SlimeTurn extends BattleTurn{
protected String name = "スライム";
protected void attack(){
System.out.println("ねばねば攻撃でダメージを食らった!");
}
}
//魔法使いの攻撃クラス
public class MagicianTurn extends BattleTurn{
protected String name = "魔法使い";
protected void attack(){
System.out.println("呪文による攻撃でダメージを与えた!");
}
}
public class Main{
public static void main(String args[]){
BattleTurn st = new SlimeTurn();
//スライムのターン! 体当たりの攻撃でダメージを食らった! スライムのターンは終了した!
st.turn();
BattleTurn mt = new MagicianTurn();
//魔法使いのターン! 呪文による攻撃でダメージを与えた! 魔法使いのターンは終了した!
mt.turn();
}
}
Factory Methodパターン
Factory Methodパターンとは先程紹介した、Template Methodパターンを実体生成に応用した応用になります。Factoryは工場でMethodは方法のような意味があります。今回は武器工場のプログラムをサンプルとして書いてみました。
Productクラスは製品を表し、武器(Weapon)を抽象化した概念になります。
Factoryクラスは、工場を表し、武器工場(WeaponFactory)を抽象化した概念になります。
Mainクラスで、「弓」「剣」「斧」を武器工場を利用して作成し、武器として使う、といった処理を行っています。
//製品クラス
public abstract class Product{
//製品を使う
public abstract void use();
}
//工場クラス
public abstract class Factory{
public final Product create(String name,int value){
//製品を作る
Product p = createProduct(name,value);
return p;
}
}
//武器クラス
public Weapon extends Product{
//武器の名前と威力
private String name;
private int power;
public Weapon(String name,int power){
this.name = name;
this.power = power;
}
public void use(){
System.out.println(name+"を使って"+power+"のダメージ");
}
}
//武器工場クラス
public WeaponFactory extends Factory{
protected createProduct(String name,int power){
return new Weapon(String name,int power);
}
}
public void Main(){
public void class main(String args[]){
Factory factory = new WeaponFactory();
Product bow = factory.create("弓",10);
Product sword = factory.create("剣",20);
Product ax = factory.create("斧",30);
//弓を使って10のダメージ
bow.use();
//剣を使って20のダメージ
sword.use();
//斧を使って30のダメージ
ax.use();
}
}
Singletonパターン
プログラムを動かす場合、一般的には一つのクラスからたくさんのインスタンスが作られます。冒険者クラスをひな形に、剣士インスタンス、盗賊インスタンス、魔法使いインスタンス、僧侶インスタンスなどなどが作られます。ですが、このクラスからはインスタンスはたった一つしか作らないと決めているクラスがある場合もあります。RPGのワールドマップ(世界地図)なんかは、1つのゲームの中にたった1枚しか存在しないことが多いと思います。
もちろん、裏世界、地底世界、天空世界、平行世界、エリアマップなんかがあるゲームもありますが。
//世界にたった一つの世界地図クラス
public class WorldMap{
private static WorldMap worldMap = new WorldMap();
private WorldMap(){
System.out.println("世界地図を作成しました");
}
public static WorldMap getInstance();
public String[][] map = [ ["街","草","草","森"]
["森","草","草","草"]
["森","森","村","草"]
["森","森","草","森"]];
}
public class Main{
public static void main(String[] args){
//世界地図その1を作成する
WorldMap wm1 = WorldMap.getInstance();
//世界地図その1の座標(2,2)を村から城に変更する
wm1.map[2][2] = "城";
//世界地図その2を作成する
WorldMap wm2 = WorldMap.getInstance();
//世界地図その2の座標(2,2)を参照すると、世界地図その1と同じインスタンスを
//参照するため村ではなく城になっている
System.out.println(wm2[2][2]);
}
}
Prototypeパターン
モンスター一覧を作っていて、敵のスライムをクラスのインスタンスから作成するとき Enemy enemy = new Slime(); という式を用いてスライムクラスから作成します。しかし、敵の種類が、200種類を超えたりした場合に、200クラスも作ってたらソースコードが膨大な量になることがあります。その場合、クラスからインスタンスを生成するのではなく、インスタンスをコピーして新しいインスタンスを作成します。他にも、生成させたいインスタンスが複雑でクラスからのインスタンス生成が難しい場合、インスタンスを生成するときのフレームワークを特定のクラスに依存させたくない場合、このパターンを使います。
//ゲームキャラクタークラス
public interface GameCharacter extends Clonable{
public abstract void criticalAttack();
public abstract GameCharacter createClone();
}
//ゲームキャラクター一覧を管理するクラスです
public class GameCharacterManager{
//全キャラ一覧
private Map<String,GameCharacter> allStars = new HashMap<>();
//ゲームキャラクターを登録します
public void register(String name,GameCharacter gc){
allStars.put(name,gc);
}
//プロトタイプからゲームキャラクターを作成します
public GameCharacter create(String protoName){
GameCharacter gc = allStars.get(protoName);
return gc.createClone();
}
}
//ヒーロークラスはゲームキャラクタークラスを継承します
public class Hero implements GameCharacter{
//職業
private String job;
public Hero(String job){
this.job = job;
}
//ヒーローは会心の攻撃をします
public void criticalAttack(){
System.out.println(job + "は、会心の一撃");
}
public GameCharacter createClone(){
GameCharacter gc = null;
try{
gc = (GameCharacter) clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return gc;
}
}
//敵クラスはゲームキャラクタークラスを継承します
public class Enemy implements GameCharacter{
//種族
private String breed;
public Hero(String breed){
this.breed = breed;
}
//敵は痛恨の攻撃をします
public void criticalAttack(){
System.out.println(breed + "は、痛恨の一撃");
}
public GameCharacter createClone(){
GameCharacter gc = null;
try{
gc = (GameCharacter) clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return gc;
}
}
public class Main{
public static void main(String[] args){
GameCharacterManager manager = new GameCharacterManager();
Hero ninja = new Hero("忍者");
Enemy slime = new Enemy("スライム");
Enemy dragon = new Enemy("ドラゴン");
manager.register("正義の忍者",ninja);
manager.register("悪のスライム",slime);
manager.register("悪のドラゴン",dragon);
GameCharacter gc1 = manager.create("正義の忍者");
GameCharacter gc2 = manager.create("悪のスライム");
GameCharacter gc3 = manager.create("悪のドラゴン");
//忍者は会心の一撃
gc1.criticalAttack();
//スライムは痛恨の一撃
gc2.criticalAttack();
//ドラゴンは痛恨の一撃
gc3.criticalAttack();
}
}
Builderパターン
Director(監督)のもとBuilder(大工)が建築物を建てるイメージです。 オブジェクトの建築もできますが、今回のサンプルで建築するのは、キャラクターのセリフです。「レイ」という名前をセットして、「ここはアスカシティー」「あやしいやつ」と、セリフをセットして自動的に、
男性のセリフのビルダーならば、
レイ「ここはアスカシティーだ。あやしいやつだ。」
女性のセリフのビルダーならば、
レイ「ここはアスカシティーだわ。あやしいやつだわ」
というセリフの文字列を組み立ててくれます。
Builderクラス内のsetNameメソッドやaddDialogメソッドの返り値をvoidの代わりにBuilderにしておくと、
builder.setName("レイ").addDialog("ここは、アスカシティー");
のようにメソッドをつなげて書くことができるようになります。
こういう書き方をメソッドチェーンといい、Builderパターンでは必ずではないですが、比較的よく使われます。
public abstract class Builder{
//名前をセット
public abstract Builder setName(String name);
//セリフを追加
public abstract Builder addDialog(String dialog);
//今まで集まった情報に基づいてセリフをビルド!
public abstract String build();
}
//セリフを組み立てる監督クラス
public class Director{
private Builder builder
public Director(Builder builder){
this.builder = builder;
}
//構築メソッド
public String construct(){
return builder.setName("レイ")
.addDialog("ここは、アスカシティー")
.addDialog("あやしいやつ")
.build();
}
}
//男性のセリフ組み立てるクラス
public class MaleBuilder extends Builder(){
private name;
private StringBuilder sb = new StringBuilder();
public Builder setName(String name){
this.name = name;
}
public Builder addDialog(String dialog){
sb.append(dialog+"だ。");
}
public String build(){
return name +"「"+ sb.toString() + "」";
}
}
//女性のセリフ組み立てるクラス
public class FemaleBuilder extends Builder(){
private name;
private StringBuilder sb = new StringBuilder();
public Builder setName(String name){
this.name = name;
}
public Builder addDialog(String dialog){
sb.append(dialog+"だわ。");
}
public String build(){
return name +"「"+ sb.toString() + "」";
}
}
public class Main{
public static void main(String args[]){
//男性キャラクター作成
Director director1 = new Director(new MaleBuilder());
//レイ「ここはアスカシティーだ。あやしいやつだ。」
System.out.println(director1.build());
//女性キャラクター作成
Director director2 = new Director(new FemaleBuilder());
//レイ「ここはアスカシティーだわ。あやしいやつだわ。」
System.out.println(director2.build());
}
}
Abstract Factoryパターン
AbstractFactoryを日本語にすると抽象的な工場になります。抽象的な工場で抽象的な製品を作り出すのが、このパターンの特徴です。ここでは、ゲームキャラクター(スライム、ゴブリン)とパーティ(敵パーティ)の両方の概念を抽象化した製品クラスと、それらを製造することのできる工場が登場します。
このプログラムにおいては、今はまだ、モンスターによる敵パーティを実体化できるだけですが、将来的に、勇者と味方パーティのクラスを追加実装したくなったときのために、あらかじめ、製造工程を抽象化しておくと、プログラムを拡張しやすくなって便利です。
//ゲームキャラクターやパーティーなど工場で作られるもの全般を抽象化した製品クラス
public abstract class AbstractProduct{
protected String name;
void arrive();
}
//ゲームキャラクターの抽象クラス
public abstract class AbstractGameCharacter extends AbstractProduct{
//◯◯があらわれた!というメッセージを出力する
public void arrive(){
System.out.println(name+"があらわれた!");
}
}
//パーティの抽象クラス
public abstract class AbstractParty extends AbstractProduct{
protected String name;
protected List<AbstractGameCharacter> list = new ArrayList<>();
//キャラクターをパーティリストに加える
public void add(AbstractGameCharacter agc){
list.add(agc);
}
//「◯◯が立ちふさがった!」というメッセージの後に、パーティメンバーを1匹ずつ登場させる
public void arrive(){
System.out.println(name+"が、立ちふさがった!");
for(AbstractGameCharacter agc:list){
agc.arrive();
}
}
}
//ゲームキャラクターの抽象クラスを継承したスライムクラス
public class Slime extends AbstractGameCharacter{
public Slime(){
this.name = "スライム";
}
}
//ゲームキャラクターの抽象クラスを継承したゴブリンクラス
public class Goblin extends AbstractGameCharacter{
public Goblin(){
this.name = "ゴブリン";
}
}
//パーティの抽象クラスを継承した敵パーティクラス
public class EnemyParty extends AbstractParty{
public EnemyParty(){
this.name = "敵一行";
}
}
//製造工場全般を示す抽象クラス
public abstract class AbstractFactory{
//パーティーの実体を返す
public AbstractParty craeteParty();
//モンスターなどのキャラクターの名前を引数に受け取るとモンスターの実体を返す
public AbstractGameCharacter createGameCharacter(String name){
switch(name){
case "スライム":
return new Slime();
case "ゴブリン":
return new Goblin();
default:
return null;
}
}
}
//敵キャラクター製造工場クラス
public class EnemyFactory extends AbstractFactory{
//敵パーティを返す
public AbstractParty createParty(){
return new EnemyParty();
}
}
public class Main{
public static void main(String args[]){
//敵製造工場を実体化する
AbstractFactory factory = new EnemyFactory();
//敵製造工場から、敵パーティ、スライム、ゴブリンを製造する
AbstractParty party = factory.createParty();
AbstractGameCharacter slime = factory.createGameCharacter("スライム");
AbstractGameCharacter goblin = factory.createGameCharacter("ゴブリン");
//スライムとゴブリンを敵パーティに加える
party.add(slime);
party.add(goblin);
//敵一行が立ちふさがった! スライムがあらわれた! ゴブリンがあらわれた!
party.arrive();
}
}
Bridgeパターン
Bridgeとは英語で橋を意味します。このデザインパターンの場合、「機能のクラス階層」と「実装のクラス階層」を分離して橋渡ししているため、ブリッジパターンと呼んでいます。サンプルプログラムでは、戦闘コマンドを例にして説明します。
はやぶさ戦士というキャラクターが攻撃をするというイベントを実装したいです。はやぶさ戦士という新しいキャラクターの行動を実装するので「実装のクラス階層」を継承する形ではやぶさ戦士の行動を実装します。
さて、はやぶさって言うくらいだから、行動が素早そうで1つのターンに3回くらい攻撃しそうな感じがしませんか?なので、3回攻撃を実装してみましょう。このとき、複数回攻撃という「機能」を実装する形になるので、「機能のクラス階層」を継承する形で複数回行動を実装します。
このように「機能のクラス階層」と「実装のクラス階層」をわけておけば、キャラクターが眠ってしまったときには行動できない機能がほしければ「機能のクラス階層」を拡張し、新キャラ、ドラゴンの行動の実装が欲しくなったときに「実装のクラス階層」を拡張すれば良いので、新機能追加対応がしやすくなります。
//戦闘ターンの機能クラス
public abstract class BattleTurn{
private BattleTurnImpl impl;
public CommandImpl(CommandImpl impl){
this.impl = impl;
}
public void preCommand(){
impl.preCommand();
}
public void command(){
impl.command();
}
public void turn(){
preCommand();
command();
}
}
//複数回行動をする戦闘ターンの機能拡張クラス
public class BattleMultiTurn extends Battle{
public BattleMultiTurn(CommandImpl impl){
super(impl);
}
public void multiTurn(int times){
preCommand();
for(int i = 0;i < times;i++){
command();
}
}
}
//戦闘ターンの実装クラスの抽象クラス
public abstract class BattleTurnImpl{
public abstract void preCommand();
public abstract void command();
}
//はやぶさ戦士の戦闘ターンの実装クラス
public class HayabusaBattleTurnImpl{
public void preCommand(){
System.out.println("はやぶさ戦士のターン!");
}
public void command(){
System.out.println("敵に10のダメージ!");
}
}
public class Main{
public static void main(String args[]){
BattleTurn bt1 = new BattleTurn(new HayabusaBattleTurnImpl());
BattleMultiTurn bt2 = new BattleTurn(new HayabusaBattleTurnImpl());
//はやぶさ戦士のターン!敵に10のダメージ!
bt1.turn();
//はやぶさ戦士のターン!敵に10のダメージ!敵に10のダメージ!敵に10のダメージ!
bt2.multiTurn(3);
}
}
Strategyパターン
Strategyとは戦略という意味で、ことRPGにおいては、敵と戦うときの作戦、方策などを指します。以下のプログラムは「うさぎ」キャラクターと「亀」というキャラクターが自動で交互に攻撃をし合います。うさぎには「睡眠戦略」という頭脳を搭載させ、亀には「勤勉戦略」という頭脳を搭載させます。睡眠戦略は3回に1度、勤勉戦略は毎ターン行動します。さてさて、この戦い、勝つのはうさぎと亀のどちらでしょうか?//戦略インターフェース
public interface Strategy{
public void action(Player attacker,Player defender);
}
//睡眠戦略クラス
public class SleepyStrategy implements Strategy{
private int cnt = 0;
private int dmg;
public SleepyStrategy(int dmg){
this.dmg = dmg;
}
//メソッドを呼び出されたら3回に1度だけ攻撃、2度は睡眠を行う
public void action(Player attacker,Player defender){
if(cnt%3 == 0){
attacker.damage(dmg);
System.out.println(attacker.getName()+"の攻撃で"+defender.getName()+"は"+dmg+"のダメージ");
}else{
System.out.println(attacker.getName()+"は、ぐっすり眠っている");
}
cnt++;
}
}
//勤勉戦略クラス
public class HardWorkStrategy implements Strategy{
public HardWorkStrategy(int dmg){
this.dmg = dmg;
}
//メソッドを呼び出されたら必ず攻撃を行う
public void action(Player attacker,Player defender){
attacker.damage(dmg);
System.out.println(attacker.getName()+"の攻撃で"+defender.getName()+"は"+dmg+"のダメージ");
}
}
//プレイヤー
public class Player{
private name;
private hp;
private Strategy strategy;
public Player(String name,int hp,strategy){
this.name = name;
this.hp = hp;
this.strategy = strategy;
}
//戦略に基づいて攻撃行動を行う
public void attack(Player player){
strategy.action(this,player);
}
public String getName(){
return name;
}
//ダメージを受けてHPが減る
public void damage(int dmg){
this.hp -= dmg;
}
//HPが1以上ならtrue 0以下ならfalse
public boolean isAlive(){
return hp > 0;
}
//キャラクターの負けのメッセージ
public void down(){
System.out.println(name+"の負け!");
}
}
public class Main {
public static void main(String args[]){
System.out.println("バトルがはじまった!");
//HP30で攻撃力5で、睡眠戦略をとる「うさぎ」というプレイヤー
Player rabbit = new Player("うさぎ",30,new SleepyStrategy(5));
//HP20で攻撃力3で、勤勉戦略をとる「亀」というプレイヤー
Player turtle = new Player("亀",20,new HardWorkerStrategy(3));
//ゲーム終了するまで無限ループ
while(true){
//うさぎの攻撃
rabbit.attack(turtle);
//亀のHPが0以下になったらゲーム終了
if(!turtle.isAlive()){
turtle.down();
break;
}
//亀の攻撃
turtle.attack(rabbit);
//うさぎのHPが0以下になったらゲーム終了
if(!rabbit.isAlive()){
rabbit.down();
break;
}
}
}
}
Compositeパターン
コンピュータのファイルシステムには「ディレクトリ」という概念と「ファイル」という概念があります。ディレクトリの中にファイルが入っていたり、ディレクトリの中にディレクトリが入っていたりします。RPGの道具一覧にこのパターンを適用されることがあります。荷物袋があり、その中には、斧やピアスのようなアイテムも入っていれば、薬入れのような別の種類の荷物入れが入っていることもあります。
このような容器と中居を同一視し、再帰的な構造を作るデザインパターンをCompositeパターンと言います。
//アイテムと入れ物を抽象化した登録クラス
public abstract class Entry{
public abstract String getName();
public abstract int getCount();
}
//アイテムクラス
public class MyItem extends Entry{
private String name;
private int count;
public MyItem(String name, int count){
this.name = name;
this.count = count;
}
public String toString(){
return this.name + "が" + "個ある";
}
}
//入れ物クラス
public class MyBag extends Entry{
private String name;
private List<Entry> entries = new ArrayList<>();
public MyBag(String name){
this.name = name;
}
public Entry add(Entry entry){
entries.add(entry);
return this;
}
protected void toString(){
StringBuilder sb = new StringBuilder();
sb.append(this.name + "が、ある。中身は ¥n");
for(Entry entry:entries){
sb.append(entry.toString()+"¥n");
}
sb.append(this.name+"の中身は以上!¥n");
return sb.toString();
}
}
public class Main{
public static void main(String args[]){
Entry myBag = new MyBag("荷物袋");
Entry ax = new MyItem("剛力の斧",1);
Entry pierce = new MyItem("金のピアス",1);
Entry medicineBag = new MyBag("薬入れ");
Entry portion = new MyItem("ポーション",5);
Entry antidote = new MyItem("解毒剤"3");
//薬入れの中にポーションと解毒剤を入れる
medicineBag.add(portion).add(antidote);
//荷物袋に剛力の斧と金のピアスと薬入れを入れる
myBag.add(ax).add(pierce).add(medicineBag);
//荷物袋がある。中身は
//剛力の斧が1個ある
//金のピアスが1個ある
//薬入れがある。中身は
//ポーションが5個ある
//解毒剤が3個ある
//薬入れの中身は以上!
//荷物袋の中身は以上!
System.out.println(myBag);
}
}
Decoratorパターン
Decoratorは装飾者を意味します。元のオブジェクトに対して、どんどん装飾を施していくパターンです。今回は武器を厨二病ネーミングでかっこよく装飾してみましょう。ただのナイフだけじゃ味気ないので竜のパワーを秘めたドラゴニックナイフにしてみたり、ブーメランをファイアーブーメランにしてみたり、ソードをドラゴニックとファイアーの両方で修飾したドラゴニックファイアーソードにしてみます。
//武器クラス
public abstract class Weapon{
public abstract String getName();
public void show(){
System.out.println(getName());
}
}
//武器装飾クラス
public abstract WeaponDecorator extends Weapon{
protected Weapon weapon;
protected Decorator(Weapon weapon){
this.weapon = weapon;
}
}
//武器具象化クラス
public class ConcreteWeapon extends Weapon{
private String name;
public ConcreteWeapon(String name){
this.name = name;
}
@Override
public String getName(){
return name;
}
}
//ドラゴニック装飾クラス
public DragonicWeapon extends WeaponDecorator{
public Doragonic(Weapon weapon){
this.weapon = weapon;
}
public String getName(){
return "ドラゴニック" + weapon.getName();
}
}
//ファイアー装飾クラス
public FireWeapon extends WeaponDecorator{
public Fire(Weapon weapon){
this.weapon = weapon;
}
public String getName(){
return "ファイアー" + weapon.getName();
}
}
public class Main{
public static void main(){
//こん棒
new ConcreteWeapon("こん棒").show();
//ドラゴニックナイフ
new DragonicWeapon(new ConcreteWeapon("ナイフ")).show();
//ファイアーブーメラン
new FireWeapon(new ConcreteWeapon("ブーメラン")).show();
//ドラゴニックファイアーソード
new DragonicWeapon(new FireWeapon(new ConcreteWeapon("ソード"))).show();
}
}
Visitorパターン
visitorは訪問者という意味です。上で紹介したCompositeパターンで紹介したようなディレクトリとファイルからなるツリー式の構造にはたくさんの要素が格納されており、各要素に対して、なんらかの処理を行うことになるでしょう。このとき、その処理のコードをMain関数に書くという方法もありますが、ここでは分離して書きます。分離して書くことで、新しい処理を後から差分で追加していったときに、訪問者クラスを修正する形で、データと処理の分離をすることができるようになります。ここでは、「闇竜」というキャラクターが魔空大陸にある2つのスポットに訪問するというサンプルプログラムを書きます。
//大陸クラスとスポットクラスの元になる登録クラス
public abstract class Entry{
protected String name;
public abstract String getName();
public abstract void accept(Visitor v);
public String getName(){
return name;
}
public String toString(){
return getName();
}
}
//大陸クラス
public Continent extends Entry{
private List<Spot> entries = new ArrayList<>();
private int idx = 0;
public Continent(String name){
this.name = name;
}
public Entry add(Entry entry){
entries.add(entry);
}
public Spot next(){
return entries.get(idx++);
}
public boolean hasNext(){
return idx < entries.size();
}
public void accept(Visitor v){
v.visit(this);
}
}
//スポットクラス
public Spot extends Entry{
public Spot(String name){
this.name = name;
}
}
//訪問者
public class Visitor{
private String name;
public Visitor(String name){
this.name = name;
}
//訪問するメソッド
public visit(Continent continent){
while (continent.hasNext()){
Spot spot = continent.next();
System.out.println(name+"は、"+continent.getName()+"大陸の"+spot.getName()+"に訪問した。");
}
}
}
public class Main{
public static void main(String args[]){
//「魔空大陸」を定義してその中に「悪魔の門」と「魔王城」を配置する
Continent deathContinent = new Continent("魔空");
Spot demonsGate = new Spot("悪魔の門");
Spot devilCastle = new Spot("魔王城");
deathContinent.add(demonsGate);
devilCastle.add(devilCastle);
//訪問者「闇竜」
Visitor darkDragon = new Visitor("闇竜");
//闇竜は魔空大陸の悪魔の門に訪問した。
//闇竜は魔空大陸の魔王城に訪問した。
darkDragon.visit(deathContinent);
}
}
Chain of Responsibilityパターン
日本語に直すと「たらい回し」になるのが、Chain of Responsibilityパターン。 ここの例では、ダメージ計算のたらい回しをサンプルにします。ダイスを投げて2から12の範囲の値が出るものとし、各値を次の手順でたらい回しで判定していきます。
①判定を行わない無判定、自動的に②へ
②値が7である場合、クリティカル攻撃クラス実行、それ以外は③へ
③値が3の倍数の場合、魔法による攻撃クラス実行、それ以外は④へ
④値が2の倍数の場合、通常攻撃クラス、それ以外は攻撃失敗
public class Dice{
private int number;
public Dice(int number){
this.number = number;
}
public int getNumber(){
return number;
}
}
public abstract class Action{
private Support next;
public Support setNext(){
this.next = next;
return next;
}
public final void calc(Dice dice){
if(judge(dice)){
done(dice);
}else if(next != null){
next.calc(dice);
}else{
System.out.println("攻撃は失敗した");
}
}
protected abstract boolean judge(Dice dice);
protected abstract void done(dice);
}
//行動を起こさないクラス
public NoAction extends Action{
protected boolean judge(Dice dice){
return false;
}
protected void done(Dice dice){
}
}
//クリティカル攻撃クラス
public class CriticalHit{
//ダイスが7の場合
protected boolean judge(){
return dice.getNumber() == 77;
}
//クリティカルダメージ10!
protected void done(Dice dice){
System.out.println("クリティカルヒット!10のダメージ");
}
}
//魔法攻撃クラス
public class MagicalAttack{
//ダイスが3の倍数の場合
protected boolean judge(){
return dice.getNumber()%3 == 0
}
//魔法によるダメージ
protected void done(Dice dice){
System.out.println("魔法による攻撃!5のダメージ!");
}
}
//通常攻撃クラス
public class NormalAttack{
//ダイスが偶数の場合
protected boolean judge(){
return dice.getNumber()%2 == 0;
}
//3から5の範囲のダメージ
protected void done(Dice dice){
int dmg = dice.getNumber()%3 + 3;
System.out.println("攻撃で"+dmg+"のダメージ!");
}
}
public class Main{
public static void main(String args[]){
Action noAction = new NoAction();
Action critical = new CriticalHit();
Action magical = new MagicalAttack();
Action normal = new NormalAttack();
//無判定→クリティカル判定→魔法判定→通常攻撃判定
noAction.setNext(critical).setNext(magical).set(normal);
//ダイスが偶数なので「通常攻撃で5のダメージ!」
noAction.calc(new Dice(2));
//ダイスが3の倍数なので「魔法による攻撃!5のダメージ!」
noAction.calc(new Dice(3));
//ダイスが偶数なので「通常攻撃で4のダメージ!」
noAction.calc(new Dice(4));
//ダイスが奇数のケースなので「攻撃は失敗した」
noAction.calc(new Dice(5));
//ダイスが3の倍数なので「魔法による攻撃!「5のダメージ!」
noAction.calc(new Dice(6));
//ダイスが7なので「クリティカルヒット!10のダメージ」
noAction.calc(new Dice(7));
//ダイスが偶数なので「攻撃で5のダメージ!」
noAction.calc(new Dice(8));
//ダイスが3の倍数なので「魔法による攻撃!5のダメージ!」
noAction.calc(new Dice(9));
//ダイスが偶数なので「攻撃で4のダメージ!」
noAction.calc(new Dice(10));
//ダイスが奇数なので「攻撃は失敗した」
noAction.calc(new Dice(11));
//ダイスが3の倍数なので「魔法による攻撃!5のダメージ!」
noAction.calc(new Dice(12));
}
}
Facadeパターン
Facadeとは「窓口」のことです。複雑に絡み合ったクラスが複数ある場合は、適切に制御しなければいけません。その窓口を用意するというパターンです。例では、「冒険者のレコードデータを宿帳から呼び出すクラス」と「レコードデータからキャラクターを生成するクラス」を作成します。そして、この2つのクラスを呼び出し、「冒険者を呼び出すセリフを生成する窓口になるクラス」を作成しています。セリフ生成がクラス窓口になることでMainクラスがシンプルな記述だけで済むようになります。
//冒険者の宿帳クラス
public class AdventurerRegist{
private static Map<String,String> map = new HashMap<>();
static{
map.put("ライネス","ライネス,男,戦士,せっかち");
map.put("マリア","マリア,女,僧侶,真面目");
map.put("クリス","クリス,男,魔法使い,おっちょこちょい");
}
public static String getProperties(String name){
//「ライネス」を変数で受け取ると、「ライネス,男,戦士,せっかち」を返す
String props = map.get(name);
return props;
}
}
//ゲームキャラクター
public class GameCharacter{
private String name;
private String sex;
private String job;
private String personality;
public GameCharacter(String name,String sex,String job,String personality){
this.name = name;
this.sex = sex;
this.job = job;
this.personality = personality;
}
//以下getterを省略
...
}
//キャラクター生成クラス
public class CharacterMaker{
public static GameCharacter makeCharacter(String props){
String[] ary = props.split(",");
GameCharacter gc = new GameCharacter(ary[0],ary[1],ary[2],ary[3]);
return gc;
}
}
//メンバーの呼び出しクラス
public class CallMember{
public void call(String name){
String props = AdventureResist.getProperties(name);
GameCharacter gc = CharacterMaker.makeCharacter(props);
System.out.println(gc.getName()"さんをお呼びですね?");
System.out.println("この方の職業は"+gc.getJob()+"で、性格は"+gc+"です。");
}
}
public class Main{
public static void main(String args[]){
CallMember cm = new CallMember();
cm.call("ライネス");
}
}
Mediatorパターン
Mediatorは、仲介者を意味します。仲介者は複雑に絡み合ったオブジェクト同士の仲介を行います。RPGでは定番のモンスター同士が戦うモンスター闘技場を作りたいとします。このとき、モンスタークラスを用意するだけでなく、モンスター同士の戦い、攻撃があたったはずれたを審判する仲介者が必要になってきます。//モンスター闘技場の調停役のクラス
public class BattleMediator{
private Map<String,Monster> monsters = new HashMap<>();
//闘技場にモンスターを追加する
public void addMonster(Monster monster){
monsters.put(monster.getName(),monster);
}
//攻撃する
public void attack(Monster monster,String name){
if(monsters.containsKey(name)){
System.out.println(monster.getName()+"は"+name+"を攻撃しようとしたが、そんな敵はいない!");
}else{
System.out.println(monster.getName()+"の攻撃"で+"name"はダメージ!);
monsters.get(name).damage();
}
}
}
//モンスタークラス
public class Monster{
private BattleMediator mediator;
private String name;
private String hp = 10;
public Monster(mediator,name){
this.mediator = mediator;
this.name = name;
this.madiator.addMonster(this);
}
public String getName(){
return name;
}
//攻撃する
public void attack(String name){
mediator.attack(this,name);
}
//ダメージを受けてHPが1減る
public void damage(){
hp--;
}
}
public class Main{
//闘技場調停役を定義
BattleMediator battle = new BattleMediator();
//モンスター「ゴブリン」と「スライム」を定義
Monster goblin = new Monster(battle,"ゴブリン");
Monster sline = new Monster(battle,"スライム");
//ゴブリンの攻撃でスライムはダメージ!
goblin.attack("スライム");
//ゴブリンはゴーストを攻撃しようとしたがそんな敵はいない!
slime.attack("ゴースト");
}
Observerパターン
Obserberは「観察者」を意味します。オブジェクトの状態を監視し、状態が変化したときに//城
public class Castle{
private List<Hero> heroes = new ArrayList<>();
//最初はタイムリミットが100秒
private int timeLimit = 100;
//城に入る
public void walkIn(Hero hero){
heroes.add(hero);
System.out.println(hero.getName()+"は城に入った!");
}
//城から出る
public void walkOut(Hero hero){
heroes.remove(hero);
System.out.println(hero.getName()+"は城から出た!");
}
//時間が20秒経過する
public void timeGoesBy(){
timeLimit -= 20;
//タイムリミットが50秒以内になったら
if(timeLimit < 50){
for(Hero hero:heroes){
System.out.println("城はもうすぐ焼け落ちるから"+hero.getName()+"は早く脱出しよう!");
}
}
}
}
//ヒーロー
public class Hero{
private String name;
public Hero(String name){
this.name = name;
}
public String getName(){
return name;
}
}
public class Main{
public static void main(){
Castle castle = new Castle();
Hero luke = new Hero("ルーク");
//ルークは城に入った!
castle.walkIn(luke);
Hero leia = new Hero("レイア");
//レイアは城に入った!
castle.walkIn(leia);
//20秒経過×2回
castle.timeGoesBy();
castle.timeGoesBy();
//レイアは城から出た!
castle.walkOut(leia);
//城は焼け落ちるからルークは早く城から出たほうが良い!
castle.timeGoesBy();
}
}
Mementoパターン
セーブ機能 ロード機能 バトル機能 回復機能public class Memento{
//お金
private int money;
//ヒットポイント
private int hp;
Memento(int money, int hp){
this.money = money;
this.hp = hp;
}
public int getMoney(){
return money;
}
public int getHp(){
return hp;
}
}
public class Gamer{
private int money = 0;
private int hp = 10;
private static int MAXHP = 10;
//戦闘によってダメージを受け、お金を稼ぐ
public void battle(int damage, int money){
hp -= samage;
this.money += money;
}
//HPが0以下ならば死亡フラグを返す
public boolean isDead(){
return hp <= 0;
}
//データをセーブする
public Memento createMemento(){
return new Memento(money, hp);
}
//セーブデータをロードする
public void restoreMemento(Memento memento){
this.money = memento.getMoney();
this.hp = memento.getHp();
}
}