デザインパターンってなんだろう?

docs

以前、オブジェクト指向の基本的な考え方について話したよね。クラスやオブジェクト、カプセル化、継承、ポリモーフィズムといった概念が、プログラムをより分かりやすく、再利用しやすくするためにいかに重要か理解してもらえたかな?

クラスとオブジェクトの概念 – オブジェクト指向の入り口 | ToolDocs

今回は、そのオブジェクト指向の考え方をさらに一歩進めて、「デザインパターン」について解説していくよ! デザインパターンは、プログラミングの世界で「こんな問題には、こんな風に解決するのが良いよ!」っていう、先人たちの知恵と経験が詰まった設計のパターン集みたいなものなんだ。


デザインパターンってなに?

「デザインパターン」って聞くと、なんだか難しそうに感じるかもしれないけど、心配いらないよ。簡単に言うと、ソフトウェア開発で頻繁に現れる問題を解決するための、再利用可能なベストプラクティスの集まりのことなんだ。

例えるなら、料理のレシピ本みたいなものだよ。おいしい料理を作るには、材料をただ混ぜるだけじゃなくて、適切な手順や方法があるよね? デザインパターンもそれと同じで、良いプログラムを作るための「良いやり方」がパターンとしてまとめられているんだ。

じゃあ、なんでデザインパターンを使うと嬉しいんだろう? 大きく分けて次の3つのメリットがあるよ。

  1. 共通言語として使える:デザインパターンを知っていると、他のプログラマーと「あのパターンのように実装しよう」といった共通の言葉で話せるようになる。これは、チームでの開発効率をぐっと上げるんだ。
  2. 再利用性が高まる:一度うまく機能した設計パターンを何度も使えるから、コードをゼロから書く手間が省けるし、バグも減らしやすくなる。
  3. 保守性・拡張性が向上する:パターンを使って設計されたコードは、新しい機能を追加したり、変更したりするのが楽になることが多いんだ。

具体例を見てみよう!〜GoFのデザインパターン〜

デザインパターンにはたくさん種類があるんだけど、その中でも特に有名なのが「GoF(Gang of Four)のデザインパターン」と呼ばれる23種類のパターンだよ。これらは大きく分けて3つのカテゴリに分類されるんだ。

  • 生成に関するパターン(Creational Patterns):オブジェクトの生成の仕方に関するパターン。
  • 構造に関するパターン(Structural Patterns):クラスやオブジェクトの構成に関するパターン。
  • 振る舞いに関するパターン(Behavioral Patterns):オブジェクト間のコミュニケーションや責任の割り当てに関するパターン。

今回は、それぞれのカテゴリから代表的なパターンを1つずつ、簡単な例を交えながら紹介していくね。

1. 生成に関するパターン:シングルトンパターン(Singleton Pattern)

シングルトンパターン」は、あるクラスのインスタンスが常に1つしか存在しないことを保証したいときに使うパターンだよ。

例えば、アプリケーション全体でただ1つだけ必要な設定情報や、データベースへの接続を管理するクラスなんかをイメージしてみて。これらが複数存在すると、データの整合性が取れなくなったり、無駄なリソースを消費したりする可能性があるよね?

そんなときにシングルトンパターンを使うと、確実にインスタンスが1つに保たれるんだ。

Java

public class Singleton {
    // 自身のインスタンスを保持するstaticなフィールド
    private static Singleton instance = null;

    // コンストラクタをprivateにして、外部からのnewを禁止
    private Singleton() {
        System.out.println("Singletonインスタンスが生成されました。");
    }

    // 唯一のインスタンスを取得するstaticなメソッド
    public static Singleton getInstance() {
        if (instance == null) {
            // インスタンスがまだ生成されていなければ、ここで生成
            instance = new Singleton();
        }
        return instance;
    }

    public void showMessage() {
        System.out.println("Hello from Singleton!");
    }
}

public class Main {
    public static void main(String[] args) {
        // Singletonクラスのインスタンスを取得(常に同じインスタンスが返される)
        Singleton s1 = Singleton.getInstance();
        s1.showMessage();

        Singleton s2 = Singleton.getInstance();
        s2.showMessage();

        // s1とs2は同じインスタンスなので、参照も同じ
        System.out.println(s1 == s2); // trueが出力される
    }
}

この例では、Singletonクラスのコンストラクタをprivateにすることで、外部から自由にnewすることができないようにしているんだ。そして、getInstance()というstaticなメソッドを通してのみインスタンスを取得できるようにしている。こうすることで、何度getInstance()を呼び出しても、最初に生成されたインスタンスが常に返されるようになるんだよ。

2. 構造に関するパターン:アダプターパターン(Adapter Pattern)

アダプターパターン」は、既存のクラスのインターフェース(API)が、別のクラスが期待するインターフェースと合わない場合に、そのギャップを埋めるためのパターンだよ。

身近な例で考えてみよう。日本の電化製品は100Vのコンセントを使うけど、海外旅行に行くと220Vのコンセントだったりするよね。そんなとき、現地のコンセントに日本の電化製品を繋ぐために「変換アダプター」を使うよね? それがまさにアダプターパターンだよ!

プログラムの世界では、例えば、古いライブラリの機能を使いたいけど、新しいシステムが要求するメソッド名や引数の形式が違う、なんてときに役立つんだ。

Java

// 新しいシステムが期待するインターフェース
interface NewSystemTarget {
    void request();
}

// 既存の古いシステムで使われているクラス
class OldSystemAdaptee {
    public void specificRequest() {
        System.out.println("古いシステムが特定の処理を実行します。");
    }
}

// アダプタークラス:OldSystemAdapteeをNewSystemTargetに適合させる
class Adapter implements NewSystemTarget {
    private OldSystemAdaptee oldSystemAdaptee;

    public Adapter(OldSystemAdaptee oldSystemAdaptee) {
        this.oldSystemAdaptee = oldSystemAdaptee;
    }

    @Override
    public void request() {
        // 新しいシステムからの要求を、古いシステムの処理に変換して呼び出す
        oldSystemAdaptee.specificRequest();
    }
}

public class Main {
    public static void main(String[] args) {
        // 古いシステムのインスタンス
        OldSystemAdaptee oldSystem = new OldSystemAdaptee();

        // アダプターを介して新しいシステムとして扱う
        NewSystemTarget target = new Adapter(oldSystem);

        // 新しいシステムのインターフェースを使って処理を呼び出す
        target.request();
    }
}

この例では、OldSystemAdapteeという古いシステムがspecificRequest()というメソッドを持っているけど、新しいシステムはNewSystemTargetインターフェースのrequest()メソッドを期待している。そこで、AdapterクラスがNewSystemTargetを実装しつつ、内部でOldSystemAdapteeのインスタンスを持って、request()が呼ばれたらspecificRequest()を呼び出すことで、インターフェースのギャップを埋めているんだ。

3. 振る舞いに関するパターン:ストラテジーパターン(Strategy Pattern)

ストラテジーパターン」は、アルゴリズム(処理の手順)をカプセル化し、実行時にそれらを交換できるようにするパターンだよ。

例えば、商品の価格計算をするとして、セール期間中か、VIP会員か、通常価格か、などによって割引率が変わるような場合を考えてみよう。もしこれをif-else文でべた書きしてしまうと、新しい割引方法が増えるたびにコードを修正する必要が出てきて、どんどん複雑になってしまうよね。

ストラテジーパターンを使うと、それぞれの割引方法を独立したクラスとして定義し、実行時に必要な割引方法を切り替えられるようになるんだ。

Java

// 割引戦略のインターフェース
interface DiscountStrategy {
    double applyDiscount(double price);
}

// 割引戦略の実装:通常割引
class NormalDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.95; // 5%割引
    }
}

// 割引戦略の実装:VIP割引
class VipDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.8; // 20%割引
    }
}

// 割引戦略を適用するコンテキストクラス
class PriceCalculator {
    private DiscountStrategy discountStrategy;

    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double calculatePrice(double originalPrice) {
        if (discountStrategy == null) {
            return originalPrice; // 戦略が設定されていなければ割引なし
        }
        return discountStrategy.applyDiscount(originalPrice);
    }
}

public class Main {
    public static void main(String[] args) {
        PriceCalculator calculator = new PriceCalculator();
        double originalPrice = 10000;

        // 通常割引を適用
        calculator.setDiscountStrategy(new NormalDiscount());
        System.out.println("通常割引適用後の価格: " + calculator.calculatePrice(originalPrice));

        // VIP割引を適用
        calculator.setDiscountStrategy(new VipDiscount());
        System.out.println("VIP割引適用後の価格: " + calculator.calculatePrice(originalPrice));

        // 新しい割引方法を追加するのも簡単
        // 例えば、期間限定セール割引などを追加できる
    }
}

この例では、DiscountStrategyというインターフェースを定義し、NormalDiscountVipDiscountといった具体的な割引方法がそれを実装している。PriceCalculatorクラスは、どのDiscountStrategyを使うかを外部から設定できるようになっていて、実行時に動的に割引方法を切り替えることができるんだ。これなら、新しい割引方法を追加したいときも、既存のコードに影響を与えることなく追加できるよね!


デザインパターンを学ぶには

今回は3つのパターンを紹介したけど、これ以外にもたくさんのデザインパターンがあるんだ。すぐに全部を覚える必要はないけど、少しずつでも「こんなときにこのパターンを使うと良いんだな」っていう感覚を掴んでいくのが大事だよ。

デザインパターンを学ぶ上でおすすめの方法は、

  • 有名なパターンの概念を理解する:まずは今回紹介したような、よく使われるパターンの概要を掴む。
  • サンプルコードを読む・書く:実際にコードを読んでみたり、自分で書いてみたりすることで、理解が深まる。
  • 自分のコードに応用してみる:小さなプロジェクトでもいいから、学んだパターンを意識して実装してみる。

最初は難しく感じるかもしれないけど、デザインパターンを学ぶことは、より良いプログラムを書くための強力な武器になるはずだよ!


まとめ

今回は「デザインパターン」について解説したよ。

  • デザインパターンは、ソフトウェア開発で頻繁に現れる問題を解決するための、再利用可能なベストプラクティスの集まり
  • 共通言語、再利用性の向上、保守性・拡張性の向上といったメリットがある。
  • GoFデザインパターンは、生成、構造、振る舞いの3つのカテゴリに分けられる。
    • シングルトンパターン:インスタンスが1つであることを保証する。
    • アダプターパターン:既存のインターフェースと期待されるインターフェースのギャップを埋める。
    • ストラテジーパターン:アルゴリズムを実行時に切り替える。

デザインパターンを学ぶことで、君のプログラミングスキルは間違いなくレベルアップするはずだ! ぜひ、今後の学習に役立ててみてね。


次回の記事も読んでくれると嬉しいな!

コメント

タイトルとURLをコピーしました