はじめに
デザインパターンについて学ぶとき、
みたいな名著を読んで勉強することになると思いますが、これらに出てくるいわゆる GoF デザインパターンは 23 種もあって覚えるのは大変です。しかし、どれも結局やってることは 2 パターンに分類でき、各パターンがどちら(あるいは両方)に分類されるのか、を意識することで理解が進むのではという話をします。
GoF デザインパターンは「抽象レイヤの導入」という観点でみると 2 パターンしかない説
ソフトウェアの世界において、「あらゆる問題は別レベルのインダイレクションの導入で解決できる」 (We can solve any problem by introducing an extra level of indirection.) 的な格言があるようですが、GoF デザインパターンはこの格言の具体例です。**「抽象レイヤであるクラス/インターフェースの導入」**という観点でデザインパターンを見直すと、GoF デザインパターンがやっている抽象レイヤの導入目的は以下の 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
クラスがあるとします。
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()
で各要素を取得するというのを実装を見て確認し、ループを回さなくてはいけません。
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
に要素アクセスの動作をまとめます。要素アクセス操作が集約されるとともに、統一的なインターフェースでアクセスできるようになります。
public interface Iterator<E> {
boolean hasNext();
E next();
}
public interface BaseCollection<E> {
Iterator<E> iterator();
}
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);
}
}
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;
}
}
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()
を使用しているみたいな状況があるとします。
public interface Charger {
Power100V getPower100V();
}
しかし、新規導入するライブラリのクラス Generator
(発電機) は 200V 電力を供給する Generator.getPower200V()
メソッドしか持っていないとします。
public class Generator {
public Power200V getPower200V() {
return new Power200V(1000);
}
}
このような場合、Generator
に getPower100V()
のようなメソッドを追加することも考えられますが、既存のライブラリのコードというのはいじりたくないものです。
パターン適用
Transformer
(変圧器) クラスを導入し、インタフェースをこれまでの getPower100V()
に合わせた上で、
内部では Generator
に処理を委譲します。Transformer
経由で Generator
を利用することで、ユーザは従来通り getPower100V()
のインターフェースを使用することができます。
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());
}
}
public class After {
public static void main(String[] args) {
final Charger charger = new Transformer(new Generator());
final Power100V power = charger.getPower100V();
...
}
}
Bridge
実装を委譲するためのクラスを導入し、継承関係から分離する
状況
Book
インターフェースがあり、サブクラスとして EBook
と PaperBook
があるとします。
public interface Book {
void showType();
void showContent();
}
public abstract class EBook implements Book {
@Override
public void showType() {
System.out.println("# Electroical Book");
}
}
public abstract class PaperBook implements Book {
@Override
public void showType() {
System.out.println("# Paper Book");
}
}
本の内容として Novel
と Nonfiction
クラスを作りたい場合、EBook
と PaperBook
それぞれを継承して作る必要があります。
同じ実装のクラスを 2 つ作る必要があり冗長になってしまいます。
public class EBookNovel extends EBook {
@Override
public void showContent() {
System.out.println("I'm Novel.");
}
}
public class EBookNonfiction extends EBook {
@Override
public void showContent() {
System.out.println("I'm Nonfiction.");
}
}
public class PaperBookNovel extends PaperBook {
@Override
public void showContent() {
System.out.println("I'm Novel.");
}
}
public class PaperBookNonfiction extends PaperBook {
@Override
public void showContent() {
System.out.println("I'm Noonfiction.");
}
}
パターン適用
本の内容に関する showContent()
の実装は、BookImpl
インターフェースを導入してそれに委譲し、継承関係から分離します。こうすることで、EBook
と PaperBook
それぞれに対し、Novel と Nonfiction を表すクラスを作成する必要はなくなります。
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();
}
}
public class Novel implements BookImpl {
@Override
public void showContent() {
System.out.println("I'm Novel.");
}
}
public class Nonfiction implements BookImpl {
@Override
public void showContent() {
System.out.println("I'm Nonfiction.");
}
}
public class PaperBook extends Book {
public PaperBook(BookImpl impl) {
super(impl);
}
@Override
public void showType() {
System.out.println("# Paper Book");
}
}
public class EBook extends Book {
public EBook(BookImpl impl) {
super(impl);
}
@Override
public void showType() {
System.out.println("# Electronic Book");
}
}
Builder
インスタンス生成の中間層となるクラスを導入する
状況
HTML や Markdown 形式の文を生成する場合を考えます。ヘッダとパラグラフだけの単純な文を生成しています。
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
インターフェースを導入しコードをまとめることにします。
public interface Builder {
void makeHeader(String header);
void makeParagraph(String paragraph);
}
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();
}
}
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();
}
}
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.");
}
}
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
「戦略」用のインターフェースを導入し、実装部分の集約・インターフェース統一をする。
状況
サッカーチームを表すクラスが、それぞれ何らかの戦略を実装している状況を考えます。
public class PosessionSoccerTeam {
public void execute() {
System.out.println("Posession strategy.");
}
}
public class CounterSoccerTeam {
public void execute() {
System.out.println("Counter strategy.");
}
}
これでは、戦略が増えるたびにクラスを増やす必要があります。戦略部分はインターフェースを規定し分離できそうです。
パターン適用
Strategy
インターフェースを導入し、戦略部分の処理は委譲します。SoccerTeam
は Strategy.execute()
を呼ぶだけでよくなり、戦略のたびにクラスを作る必要はなくなります。
public interface Strategy {
void execute();
}
public class Possession implements Strategy {
@Override
public void execute() {
System.out.println("Posession strategy.");
}
}
public class Counter implements Strategy {
@Override
public void execute() {
System.out.println("Counter strategy.");
}
}
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 に制限することが可能です。
public class Singleton {
private static Singleton singleton = new Singleton();
// 外部からの new を禁止
private Singleton() {
}
// 唯一のインスタンスを返す
public static Singleton getInstance() {
return singleton;
}
}
Prototype
インスタンス登録とそのコピー生成のためのクラスを導入する
状況
Monster
インターフェースを実装した Slime
インスタンスが大量に必要 & 新規インスタンスの生成コストが高いので既存インスタンスのコピーでインスタンスを作りたい、という状況があるとします。
最初のみインスタンスを作り、後は createClone()
でインスタンスを生成すればよいのですが、インスタンス種類が増えると管理が大変そうです。
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
で、生成済インスタンスが登録されている場合はそれを返し、登録されていない場合のみインスタンス生成して返すという処理をします。
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();
}
}
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 進ダンプするという処理を、それぞれ CsvParser
と StringDumper
クラスで行うという状況があるとします。
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 クラスとして導入します。
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()
を呼び出すだけで一連の処理ができます。
public class After {
public static void main(String[] args) {
CsvHexDumper.hexdumpCsvFile("test.csv");
}
}
Mediator
インスタンス間の通信を仲介するクラスを導入
状況
Pedestrian
(歩行者) クラスと Car
クラスがあり、Car
は Pedestrian
からの停止要求がない状況で進むことができ、Pedestrian
は Car
が止まった状態ならば進める、といったインスタンス間の相互の通信が必要な状況があるとします。
Car
と Pedestrian
が互いにインスタンスを参照し、その状態を通信し合うような実装を考えてみます。
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.");
}
}
}
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
インターフェースとその実装を導入し、コードを整理します。これらでインスタンス間の通信を仲介します。
public interface TrafficLight {
void waiting();
void crossed();
}
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
経由で通信することで、インスタンス間通信の複雑性が緩和されます。
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.");
}
}
}
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 を保存しておいて、後でその状態を復元したいという状況があるとします。
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 の例 も参照)
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;
}
}
}
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 文で状態に応じて処理を分ければよいですが、分岐が多くて分かりにくくなる場合があります。
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
の実装クラスに分離・集約します。
public interface State {
void createAccount(Context context);
void login(Context context);
void logout(Context context);
}
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()
に委譲し、状態の変化は実装クラスの切り替えとすることで、状態ごとの操作がクラスごとに分離されます。
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
クラスがたくさん必要である状況を考えます。
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 をしないことが可能になります。
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;
}
}
}
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() するということを考えます。
public interface Dumper {
public String getName();
public void dump();
}
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() {
...
}
}
public static void main(String[] args) {
final Dumper dumper = new HeavyDumper("Akebono");
...
String str = dumper.getName();
...
dumper.dump();
}
HeavyDumper
の機能が真に必要なのは dump()
呼び出し時であるという場合でも、インスタンス生成時に重い処理が走ります。
パターン適用
Proxy クラスである ProxyDumper
を導入し、dump()
呼び出しで必要となる時点まで、HeavyDumper
のインスタンス生成を遅らせます。
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();
}
}
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