search
LoginSignup
6

More than 3 years have passed since last update.

posted at

updated at

GoF デザインパターンは「抽象レイヤの導入」に過ぎないという話

はじめに

デザインパターンについて学ぶとき、

みたいな名著を読んで勉強することになると思いますが、これらに出てくるいわゆる GoF デザインパターンは 23 種もあって覚えるのは大変です。しかし、どれも結局やってることは 2 パターンに分類でき、各パターンがどちら(あるいは両方)に分類されるのか、を意識することで理解が進むのではという話をします。

GoF デザインパターンは「抽象レイヤの導入」という観点でみると 2 パターンしかない説

ソフトウェアの世界において、「あらゆる問題は別レベルのインダイレクションの導入で解決できる」 (We can solve any problem by introducing an extra level of indirection.) 的な格言があるようですが、GoF デザインパターンはこの格言の具体例です。「抽象レイヤであるクラス/インターフェースの導入」という観点でデザインパターンを見直すと、GoF デザインパターンがやっている抽象レイヤの導入目的は以下の 2 つあるいは両方です。

  1. 何らかの処理をさせる中間層(クラス/メソッド) を導入
  2. 同種のコードを分離・集約するためのインターフェースを導入

当たり前といえば当たり前なのですが、極論すればGoF デザインパターンというのはこの 2 パターンの組み合わせでしかないと考えることができます。各パターンがどちらに分類されるものなのかを意識すれば、理解が進むかもしれません。

各パターンが 1, 2 のどちらに(あるいは両方に)該当するかをまとめたのが以下の表です。

Pattern 1. 中間層のクラス/メソッド導入 2 分離・集約のインターフェース導入
Iterator
Adapter
Template Method
Factory Method
Singleton
Prototype
Builder
Abstract Factory
Bridge
Strategy
Composite
Decorator
Visitor
Chain of Responsibility
Facade
Mediator
Observer
Memento
State
Flyweight
Proxy
Command
Interpreter

以上で言いたいことは言い切ったのですが、おまけとして以下で各パターンを、「抽象レイヤ導入」という観点から説明してみます。

Iterator

要素アクセス用インターフェースを導入する。

状況

自作コレクションの JunkList クラスがあるとします。

JunkList.java
public class JunkList<E> {
    private Object[] elements = new Object[1024];
    private int index = 0;

    public void add(E e) {
        this.elements[index] = e;
        ++index;
    }

    @SuppressWarnings("unchecked")
    public E getElement(int index) {
        return (E)(this.elements[index]);
    }

    public int getIndex() {
        return this.index;
    }
}

全要素を走査する場合、要素へのアクセス方法とサイズの取得方法を、実装を見て確認する必要があります。
この場合、getIndex() で要素数を取得して、getElement() で各要素を取得するというのを実装を見て確認し、ループを回さなくてはいけません。

Before.java
public class Before {
    public static void main(String[] args) {
        JunkList<String> junkList = new JunkList<>();
        junkList.add("Element1");
        junkList.add("Element2");
        junkList.add("Element3");
        ...
        ...
        // 全要素にアクセス
        for (int i = 0; i < junkList.getIndex(); ++i) {
            System.out.println(junkList.getElement(i));
        }
    }
}

パターン適用

Iterator インターフェースを導入し、JunkList 用の Iterator である JunkListIterator に要素アクセスの動作をまとめます。要素アクセス操作が集約されるとともに、統一的なインターフェースでアクセスできるようになります。

Iterator.java
public interface Iterator<E> {
    boolean hasNext();
    E next();
}
BaseCollection.java
public interface BaseCollection<E> {
    Iterator<E> iterator();
}
JunkList.java
public class JunkList<E> implements BaseCollection<E> {
    private Object[] elements = new Object[1024];
    private int index = 0;

    public void add(E e) {
        this.elements[index] = e;
        ++index;
    }

    @SuppressWarnings("unchecked")
    public E getElement(int index) {
        return (E)(this.elements[index]);
    }

    public int getIndex() {
        return this.index;
    }

    @Override
    public Iterator<E> iterator() {
        return new JunkListIterator<E>(this);
    }
}
JunkListIterator.java
public class JunkListIterator<E> implements Iterator<E> {
    private final JunkList<E> junkList;
    private int index;

    public JunkListIterator(JunkList<E> junkList) {
        this.junkList = junkList;
    }

    @Override
    public boolean hasNext() {
        return (this.index < this.junkList.getIndex());
    }

    @Override
    public E next() {
        E element = this.junkList.getElement(this.index);
        ++(this.index);
        return element; 
    }
}
After.java
public class Main {
    public static void main(String[] args) {
        JunkList<String> junkList = new JunkList<>();
        junkList.add("Element1");
        junkList.add("Element2");
        junkList.add("Element3");
        ...
        // 全要素にアクセス
        Iterator<String> itr = junkList.iterator();
        while (itr.hasNext()) {
            System.out.println(itr.next());
        }
    }
}

Adapter

インターフェース変更のための中間層となるクラスを導入する。

状況

既存コードでは、Charger (充電器) インターフェースの Charger.getPower100V() を使用しているみたいな状況があるとします。

Charger.java
public interface Charger {
    Power100V getPower100V();
}

しかし、新規導入するライブラリのクラス Generator (発電機) は 200V 電力を供給する Generator.getPower200V() メソッドしか持っていないとします。

Generator.java
public class Generator {
    public Power200V getPower200V() {
        return new Power200V(1000);
    }
}

このような場合、GeneratorgetPower100V() のようなメソッドを追加することも考えられますが、既存のライブラリのコードというのはいじりたくないものです。

パターン適用

Transformer (変圧器) クラスを導入し、インタフェースをこれまでの getPower100V() に合わせた上で、
内部では Generator に処理を委譲します。Transformer 経由で Generator を利用することで、ユーザは従来通り getPower100V() のインターフェースを使用することができます。

Transformer.java
public class Transformer implements Charger {
    private final Generator generator;

    public Transformer(Generator generator) {
        this.generator = generator;
    }

    @Override
    public Power100V getPower100V() {
        final Power200V power200V = this.generator.getPower200V();
        return new Power100V(power200V.getWatt());
    }
}
After.java
public class After {
    public static void main(String[] args) {
        final Charger charger = new Transformer(new Generator());
        final Power100V power = charger.getPower100V();
        ...
    }
}

Bridge

実装を委譲するためのクラスを導入し、継承関係から分離する

状況

Book インターフェースがあり、サブクラスとして EBookPaperBook があるとします。

Book.java
public interface Book {
    void showType();
    void showContent();
}
EBook.java
public abstract class EBook implements Book {
    @Override
    public void showType() {
        System.out.println("# Electroical Book");
    }
}
PaperBook.java
public abstract class PaperBook implements Book {
    @Override
    public void showType() {
        System.out.println("# Paper Book");
    }
}

本の内容として NovelNonfiction クラスを作りたい場合、EBookPaperBook それぞれを継承して作る必要があります。
同じ実装のクラスを 2 つ作る必要があり冗長になってしまいます。

EBookNovel.java
public class EBookNovel extends EBook {
    @Override
    public void showContent() {
        System.out.println("I'm Novel.");
    }
}
EBookNonfiction.java
public class EBookNonfiction extends EBook {
    @Override
    public void showContent() {
        System.out.println("I'm Nonfiction.");
    }
}
PaperBookNovel.java
public class PaperBookNovel extends PaperBook {
    @Override
    public void showContent() {
        System.out.println("I'm Novel.");
    }
}
PaperBookNonfiction.java
public class PaperBookNonfiction extends PaperBook {
    @Override
    public void showContent() {
        System.out.println("I'm Noonfiction.");
    }
}

パターン適用

本の内容に関する showContent() の実装は、BookImpl インターフェースを導入してそれに委譲し、継承関係から分離します。こうすることで、EBookPaperBook それぞれに対し、Novel と Nonfiction を表すクラスを作成する必要はなくなります。

Book.java
public abstract class Book {
    private final BookImpl impl;

    public Book(BookImpl impl) {
        this.impl = impl;
    }

    public abstract void showType();

    public void showContent() {
        this.impl.showContent();
    }
}
Novel.java
public class Novel implements BookImpl {                                                                                                                                                                                                                                           
    @Override
    public void showContent() {
        System.out.println("I'm Novel.");
    }
}
Nonfiction.java
public class Nonfiction implements BookImpl {
    @Override
    public void showContent() {
        System.out.println("I'm Nonfiction.");
    }
}
PaperBook.java
public class PaperBook extends Book {                                                                                                                                                                                                                                            
    public PaperBook(BookImpl impl) {
        super(impl);
    }

    @Override
    public void showType() {
        System.out.println("# Paper Book");
    }
}
EBook.java
public class EBook extends Book {                                                                                                                                                                                                                                               
    public EBook(BookImpl impl) {        
        super(impl);
    }

    @Override
    public void showType() {
        System.out.println("# Electronic Book");
    }
}

Builder

インスタンス生成の中間層となるクラスを導入する

状況

HTML や Markdown 形式の文を生成する場合を考えます。ヘッダとパラグラフだけの単純な文を生成しています。

Before.java
public class Before {
    public static void main(String[] args) {
        {
            final StringBuilder sb = new StringBuilder();
            sb.append("<h1>Header</h1>\n");
            sb.append("<p>Paragraph</p>\n");
            System.out.println(sb.toString());
        }
        {
            final StringBuilder sb = new StringBuilder();
            sb.append("# Header\n");
            sb.append("Paragraph\n");
            System.out.println(sb.toString());
        }
    }
}

これでも問題はないですが、タグ部分と内容部分の系統の異なるものを同列に扱っている点が気になります。HTML/Markdown どちらの処理も、抽象的な視点から見ると、ヘッダとパラグラフの内容順に並べています。それに基づいてインターフェースとクラスを導入することでコードを整理できそうです。

パターン適用

Builder インターフェースを導入しコードをまとめることにします。

Builder.java
public interface Builder {
    void makeHeader(String header);
    void makeParagraph(String paragraph);
}
HTMLBuilder.java
public class HTMLBuilder implements Builder {
    private final StringBuilder sb = new StringBuilder();

    @Override
    public void makeHeader(String header) {
        this.sb.append(String.format("<h1>%s</h1>\n", header));
    }

    @Override
    public void makeParagraph(String paragraph) {
        this.sb.append(String.format("<p>%s</p>\n", paragraph));
    }

    public String getResult() {
        return this.sb.toString();
    }
}
MarkdownBuilder.java
public class MarkdownBuilder implements Builder {
    private final StringBuilder sb = new StringBuilder();

    @Override
    public void makeHeader(String header) {
        this.sb.append(String.format("# %s\n", header));
    }

    @Override
    public void makeParagraph(String paragraph) {
        this.sb.append(String.format("%s\n", paragraph));
    }

    public String getResult() {
        return this.sb.toString();
    }
}
Director.java
public class Director {
    private final Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public void construct() {
        this.builder.makeHeader("Header");
        this.builder.makeParagraph("This is Paragraph.");
    }
}
After.java
public class After {
    public static void main(String[] args) {
        // HTML
        {
            final HTMLBuilder builder = new HTMLBuilder();
            final Director director = new Director(builder);
            director.construct();
            System.out.println(builder.getResult());
        }

        // Markdown
        {
            final MarkdownBuilder builder = new MarkdownBuilder();
            final Director director = new Director(builder);
            director.construct();
            System.out.println(builder.getResult());
        }
    }
}

HTML/Markdown のタグ部分は HTMLBuilder/MarkdownBuilder のクラスに分離・集約されました。また、内容部分は Director.construct() 内で設定するように分離することができました。

Strategy

「戦略」用のインターフェースを導入し、実装部分の集約・インターフェース統一をする。

状況

サッカーチームを表すクラスが、それぞれ何らかの戦略を実装している状況を考えます。

PosessionSoccerTeam.java
public class PosessionSoccerTeam {
    public void execute() {
        System.out.println("Posession strategy.");   
    }
}
CounterSoccerTeam.java
public class CounterSoccerTeam {
    public void execute() {
        System.out.println("Counter strategy.");   
    }
}

これでは、戦略が増えるたびにクラスを増やす必要があります。戦略部分はインターフェースを規定し分離できそうです。

パターン適用

Strategy インターフェースを導入し、戦略部分の処理は委譲します。SoccerTeamStrategy.execute() を呼ぶだけでよくなり、戦略のたびにクラスを作る必要はなくなります。

Strategy.java
public interface Strategy {
    void execute();
}
Posession.java
public class Possession implements Strategy {
    @Override
    public void execute() {
         System.out.println("Posession strategy.");       
    }
}
Counter.java
public class Counter implements Strategy {
    @Override
    public void execute() {
        System.out.println("Counter strategy.");
     }
}
SoccerTeam.java
public class SoccerTeam {
    private final Strategy strategy;

    public SoccerTeam(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        this.strategy.execute();
    }
}

Singleton

「インスタンス取得」のメソッドを導入し、インスタンス数を 1 つに制限する

状況

インスタンスの生成に new が使える場合、インスタンスは何個でも生成可能です。インスタンス数を 1 に制限したいという状況があるとします。

パターン適用

コンストラクタを private にし、getIntance() という「インスタンスの取得」という抽象操作を意味するメソッドを導入します。そのメソッド経由で、生成済みの唯一のインスタンスを返すようにすれば、インスタンス数を 1 に制限することが可能です。

Singleton.java
public class Singleton {
    private static Singleton singleton = new Singleton();

    // 外部からの new を禁止
    private Singleton() {
    }

    // 唯一のインスタンスを返す
    public static Singleton getInstance() {
        return singleton;
    }
}

Prototype

インスタンス登録とそのコピー生成のためのクラスを導入する

状況

Monster インターフェースを実装した Slime インスタンスが大量に必要 & 新規インスタンスの生成コストが高いので既存インスタンスのコピーでインスタンスを作りたい、という状況があるとします。
最初のみインスタンスを作り、後は createClone() でインスタンスを生成すればよいのですが、インスタンス種類が増えると管理が大変そうです。

Before.java
public class Before {
    public static void main(String[] args) {
        ...
        final Monster normalSlime = new Slime("Normal");
        final Monster metalSlime  = new Slime("Metal");
        ...

        final Monster normalSlime2 = normalSlime.createClone();
        final Monster normalSlime3 = normalSlime.createClone();
        final Monster metalSlime2  = metalSlime.createClone();
        ...
    }

パターン適用

インスタンス管理のための MonsterManager クラスを導入することで、コードが整理され理解しやすなります。
MonsterManager で、生成済インスタンスが登録されている場合はそれを返し、登録されていない場合のみインスタンス生成して返すという処理をします。

MonsterManager.java
public class MonsterManager {
    private final Map<String, Monster> monsterMap = new HashMap<>();

    public void addPrototype(String name, Monster monster) {
        this.monsterMap.put(name, monster);
    }

    public Monster createMonster(String name) throws CloneNotSupportedException {
        final Monster monster = this.monsterMap.get(name);
        if (monster == null) {
            throw new IllegalArgumentException("Invalid monster name");
        }

        return monster.createClone();
    }
}
After.java
public class After {
    public static void main(String[] args) {
        final MonsterManager manager = new MonsterManager();
        manager.addPrototype("NormalSlime", new Slime("Normal"));
        manager.addPrototype("MetalSlime",  new Slime("Metal"));
        ..
        final Monster normalSlime  = manager.createMonster("NormalSlime");
        final Monster metalSlime   = manager.createMonster("MetalSlime");
        ...
    }
}

Facade

複数の処理をまとめたメソッドを導入する

状況

CSV ファイル読み込み -> 各データを 16 進ダンプするという処理を、それぞれ CsvParserStringDumper クラスで行うという状況があるとします。

Before.java
public class Before {
    public static void main(String[] args) {
        final CsvParser csvParser = new CsvParser();
        csvParser.parse("test.csv");

        for (final List<String> row : csvParser.getRows()) {
            for (final String col : row) {
                System.out.println(StringDumper.hexdump(col));
            }
        }
    }
}

この読み込み -> ダンプの処理が典型的な使用例であるというような場合、それらをまとめて行うメソッドを導入すると使い勝手が良さそうです。

パターン適用

処理をまとめたメソッド hexdumpCsvFile を持つ CsvHexDumper を Facade クラスとして導入します。

CsvHexDumper.java
public class CsvHexDumper {
    // static メソッド専用クラスとする
    private CsvHexDumper() {}

    public static void hexdumpCsvFile(String filePath) {
        CsvParser csvParser = new CsvParser();
        csvParser.parse(filePath);

        for (final List<String> row : csvParser.getRows()) {
            for (final String col : row) {
                System.out.println(StringDumper.hexdump(col));
            }
        }
    }
}

ユーザはこの CsvHexDumper.hexdumpCsvFile() を呼び出すだけで一連の処理ができます。

After.java
public class After {
    public static void main(String[] args) {
        CsvHexDumper.hexdumpCsvFile("test.csv");
    }
}

Mediator

インスタンス間の通信を仲介するクラスを導入

状況

Pedestrian (歩行者) クラスと Car クラスがあり、CarPedestrian からの停止要求がない状況で進むことができ、PedestrianCar が止まった状態ならば進める、といったインスタンス間の相互の通信が必要な状況があるとします。
CarPedestrian が互いにインスタンスを参照し、その状態を通信し合うような実装を考えてみます。

Car.java
public class Car {
    private Pedestrian pedestrian;
    private boolean stopFlag = false;

    public void setPedestrian(Pedestrian pedestrian) {
        this.pedestrian = pedestrian;
    }

    public void stopRequested() {
        this.stopFlag = true;
        this.pedestrian.notifyCarStopped();
    }

    public void notifyCrossFinished() {
        this.stopFlag = false;
    }

    public void proceed() {
        if (this.stopFlag) {
            System.out.println("Car stops.");
        } else {
            System.out.println("Car proceeds.");
        }
    }
}
Pedestrian.java
public class Pedestrian {
    private Car car;
    private boolean stopFlag = true;

    public void setCar(Car car) {
        this.car = car;
    }

    public void notifyCarStopped() {
        this.stopFlag = false;
    }

    public void stopRequest() {
        this.car.stopRequested();
    }

    public void proceed() {
        if (this.stopFlag) {
            System.out.println("Pedestrian stops.");
        } else {
            System.out.println("Pedestrian proceeds.");
            this.car.notifyCrossFinished();
            this.stopFlag = true;
        }
    }
}

インスタンス数が増え相互通信数が増えると、処理が複雑になり破綻しそうな匂い(?)がします。

パターン適用

Mediator として TrafficLight インターフェースとその実装を導入し、コードを整理します。これらでインスタンス間の通信を仲介します。

TrafficLight.java
public interface TrafficLight {
    void waiting();
    void crossed();
}
TrafficLightImpl.java
public class TrafficLightImpl implements TrafficLight {
    private Car        car;
    private Pedestrian pedestrian;

    public void setCar(Car car) {
        this.car = car;
    }

    public void setPedestrian(Pedestrian pedestrian) {
        this.pedestrian = pedestrian;
    }

    @Override
    public void waiting() {
        if (this.car != null) {
            this.car.setStopFlag(true);
        }

        this.pedestrian.setStopFlag(false);
    }

    @Override
    public void crossed() {
        if (this.car != null) {
            this.car.setStopFlag(false);
        }

        this.pedestrian.setStopFlag(true);
    }
}

Car, Pedestrian ともに、TrafficLight 経由で通信することで、インスタンス間通信の複雑性が緩和されます。

Car.java
public class Car implements Participant {
    private boolean stopFlag = false;

    @Override 
    public void setStopFlag(boolean stopFlag) {
        this.stopFlag = stopFlag;
    }

    @Override
    public void proceed() {
        if (this.stopFlag) {
            System.out.println("Car stops.");
        } else {
            System.out.println("Car proceeds.");
        }
    }
}
Pedestrian.java
public class Pedestrian implements Participant {
    private final TrafficLight trafficLight;
    private boolean stopFlag = true;

    public Pedestrian(TrafficLight trafficLight) {
        this.trafficLight = trafficLight;
    }

    @Override 
    public void setStopFlag(boolean stopFlag) {
        this.stopFlag = stopFlag;
    }

    @Override
    public void proceed() {
        if (this.stopFlag) {
            System.out.println("Pedestrian stops.");
        } else {
            System.out.println("Pedestrian proceeds.");
            this.trafficLight.crossed();
        }
    }

    public void stopRequest() {
        this.trafficLight.stopRequested();
    }
}

Memento

インスタンス状態の保存/復元用クラスを導入する

状況

レベルと HP を持つ Hero クラスがあり、ある時点でのレベルと HP を保存しておいて、後でその状態を復元したいという状況があるとします。

Before.java
public class Before {
    public static void main(String[] args) {
        Hero hero = new Hero();
        final int level = hero.getLevel();
        final int hp    = hero.getHP();
        ...
        hero.levelUp();
        hero.receiveDamage(5);
        ...
        hero.restore(level, hp);
        ...
    }
}

レベルと HP はセットで扱うので、インスタンスにまとめたほうが分かりやすそうです。

パターン適用

レベルと HP をまとめた Memento クラスを導入し、状態の保存・復元は Memento の作成・読込に対応させます。
このとき、生成した Memento 情報を参照できないようにしたい(復元に使うだけにしたい) 場合は、package を分けたり、内部クラスを用いたりなどします。ここでは、内部クラスを用いた例を示します ( Wikipedia の も参照)

Hero.java
public class Hero {
    private int level = 1;
    private int hp    = 10;

    public void levelUp() {
        ++(this.level);
    }

    public void receiveDamage(int point) {
        this.hp -= point;
    }

    public void healDamange(int point) {
        this.hp += point;
    }

    public Memento createMemento() {
        return new Memento(this.level, this.hp);
    }

    public void restoreMemento(Memento memento) {
        this.level = memento.getLevel();
        this.hp    = memento.getHP();
    }

    @Override
    public String toString() {
        return String.format("Level=%d,HP=%d", this.level, this.hp);
    }

    public static class Memento {
        private final int level;
        private final int hp;

        public Memento(int level, int hp) {
            this.level = level;
            this.hp    = hp;
        }

        private int getLevel() {
            return this.level;
        }

        private int getHP() {
            return this.hp;
        }
    }
}
After.java
public class After {
    public static void main(String[] args) {
        final Hero hero = new Hero();
        final Hero.Memento memento = hero.createMemento(); // この memento の内容は main() から参照不可

        hero.levelUp();
        hero.receiveDamage(3);
        System.out.println(hero);
        ... 
        hero.restoreMemento(memento);
        System.out.println(hero);
    }        
}

State

状態を表すインターフェースを導入し、状態ごとの処理を分離する

状況

Web サイトへのアカウント作成 -> Login -> Logout というようなことをする状況があるとします。
アカウント作成前にはログインできないとか、ログイン状態では再度ログインできない等といったような状態に応じた操作をするには、
状態を示す変数を持ち、 if 文や switch 文で状態に応じて処理を分ければよいですが、分岐が多くて分かりにくくなる場合があります。

Context.java
public class Context {
  public void createAccount() {
      switch (this.state) {
      case NO_ACCOUNT: {
          System.out.println("Created Account.");
          this.state = EnumState.LOGOUT;
          break;
      }
      case LOGIN: {
          System.err.println("Already logged in.");
          break;
      }
      case LOGOUT: {
          System.err.println("Account is already created.");
          break;
      }
      default: {
          break;
      }
      }
  }

  public void login() {
      ...
  }

  public void logout() {
      ...
  }
}

パターン適用

状態を示す State インターフェースを導入し、各状態での動作を State の実装クラスに分離・集約します。

State.java
public interface State {
    void createAccount(Context context);
    void login(Context context);
    void logout(Context context);
}
LoginState.java
public class LoginState implements State {
    @Override
     public void createAccount(Context context) {
        System.err.println("Account is already created.");
    }

    @Override
    public void login(Context context) {
        System.err.println("Already logged in.");
    }

    @Override
    public void logout(Context context) {
        System.out.println("Logged out.");
        context.setState(new LogoutState());
    }
}

createAccount(), login(), logout() の処理は State.createAccount(), State.login(), State.logout() に委譲し、状態の変化は実装クラスの切り替えとすることで、状態ごとの操作がクラスごとに分離されます。

Context.java
public class Context {
    private State state = new NoAccountState();

    public void setState(State state) {
        this.state = state;
    }

    public void createAccount() {
        this.state.createAccount(this);
    }

    public void login() {
        this.state.login(this);
    }

    public void logout() {
        this.state.logout(this);
    }
}

Flyweight

生成済みインスタンス管理クラスを導入し、インスタンスを共有させる

状況

メモリを多く消費する HeavyObject クラスがたくさん必要である状況を考えます。

Before.java
public class Before {
    public static void main(String[] args) {
        final HeavyObject heavyObject1 = new HeavyObject("Hello");
        final HeavyObject heavyObject2 = new HeavyObject("World");
        final HeavyObject heavyObject3 = new HeavyObject("Hello");
        final HeavyObject heavyObject4 = new HeavyObject("World");
        ...
    }
}

同内容のインスタンスも毎回生成し直しているので、無駄なリソース消費がありそうです。

パターン適用

インスタンスが共有可能な場合は、インスタンスが生成済みの場合にそれを返す HeavyObjectFactory を導入することで、
余分な new をしないことが可能になります。

HeavyObjectFactory.java
public class HeavyObjectFactory {
    private final Map<String, HeavyObject> objMap = new HashMap<>();

    public HeavyObjectFactory create(String str) {
        if (this.objMap.containsKey(str)) {
            return this.objMap.get(str);
        } else {
            final HeavyObject heavyObject = new HeavyObject(str);
            this.objMap.put(str, heavyObject);

            return  heavyObject;
        }
    }
}
After.java
public class After {
    public static void main(String[] args) {
        final HeavyObjectFactory factory = new HeavyObjectFactory();

        final HeavyObject heavyObject1 = factory.create("Hello");
        final HeavyObject heavyObject2 = factory.create("World");
        final HeavyObject heavyObject3 = factory.create("Hello");
        final HeavyObject heavyObject4 = factory.create("World");
        ...
    }
}

Proxy

インターフェースを同じくするクラスを挿入し、重い処理を遅らせる

状況

初期化処理が重い HeavyDumper があり、初期化後に他の処理をいろいろした後に dump() するということを考えます。

Dumper.java
public interface Dumper {
    public String getName();
    public void dump();
}
HeavyDumper.java
public class HeavyDumper implements Dumper {
    private final String name;

    public HeavyDumper(String name) {
        this.name = name;
        //
        // ... heavy process
        //
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void dump() {
        ...
    }
}
Before.java
public static void main(String[] args) {
    final Dumper dumper = new HeavyDumper("Akebono");
    ...
    String str = dumper.getName();
    ...
    dumper.dump();
}

HeavyDumper の機能が真に必要なのは dump() 呼び出し時であるという場合でも、インスタンス生成時に重い処理が走ります。

パターン適用

Proxy クラスである ProxyDumper を導入し、dump() 呼び出しで必要となる時点まで、HeavyDumper のインスタンス生成を遅らせます。

ProxyDumper.java
public class ProxyDumper implements Dumper {
    private final String name;
    private Dumper heavyDumper;

    public ProxyDumper(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void dump() {
        if (this.heavyDumper == null) {
            this.heavyDumper = new HeavyDumper(this.name);
        }
        this.heavyDumper.dump();
    }
}
After.java
public class After {
    public static void main(String[] args) {
        final Dumper dumper = new ProxyDumper("Akebono"); // ここでは HeavyDumper は生成されない
        ...
        String str = dumper.getName(); // ここでも HeavyDumper は未生成
        ...
        dumper.dump(); // ここで初めて HeavyDumper 生成
    } 
}

Composite

Interpreter

特定の処理に特化した「言語」を導入

パターン適用

(具体的な実装は省略)
文法規則をクラスで表現する、というパターンですが、他のパターンと比較すると若干異質な印象を受けます。しかし、「抽象レイヤの導入」という観点から考えれば、このパターンが登場する理由が分かる気がします。

これまで「抽象レイヤの導入」という観点からデザインパターンを説明してきましたが、その究極形が Interpreter パターンではないでしょうか。インターフェースやクラスを導入するだけでは飽き足らず(?)、何らかの処理に特化した言語を導入してしまおうというパターンです。

「オブジェクト指向における再利用のためのデザインパターン」が 1994 年に出版され、翌 1995 年に Java が登場しているのも、なんだか時代の流れを象徴しているような印象を受けます。

まとめ

デザインパターンを「抽象レイヤの導入」という観点から説明したら理解しやすいのでは、という試みでした。

参考

オブジェクト指向における再利用のためのデザインパターン
増補改訂版Java言語で学ぶデザインパターン入門
Memento pattern

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6