Javaデザインパターン:生成パターンの解説

  • ファクトリーパターン

メール送信機能を実装したいとします。最初はQQメールを使おうと思い、プロジェクトのメッセージ通知モジュールでクラスを定義しました。

package com.example.messaging;

/**
 * @author developer
 * @date 2024/5/20
 * @description メール送信サービス
 */
public class QQMailService {

    public boolean sendMail(String content){
        System.out.println("QQメール経由でメールを送信します");
        System.out.println("内容:"+content);
        return true;
    }
}
package com.example.order;

import com.example.messaging.QQMailService;

/**
 * @author developer
 * @date 2024/5/20
 * @description 注文サービス
 */
public class OrderService {

    public void createOrder(){
        System.out.println("注文の作成を開始します");
        System.out.println("注文の作成に成功しました");
        // ユーザーにメールで通知
        QQMailService mailService = new QQMailService();
        mailService.sendMail("注文XXXが倉庫で注文されました");
    }
}
package com.example.user;

import com.example.messaging.QQMailService;

/**
 * @author developer
 * @date 2024/5/20
 * @description ユーザーサービス
 */
public class UserService {

    public void registerUser(){
        System.out.println("ユーザー登録を開始します");
        System.out.println("ユーザー登録に成功しました");
        QQMailService mailService = new QQMailService();
        mailService.sendMail("こんにちはXXX、貴重なVIPメンバーになりました");
    }
}

他のモジュールでもメール送信が必要になるかもしれませんが、ここでは2つだけ示します。このように使うと問題ないように見えますが、ある日QQメールの使用ができなくなって、网易メールに切り替える必要があるとします。OK、新しいWYMailServiceを追加して、すべてのnew QQMailServiceをnew WYMailServiceに変更する必要があります。私たちは仕方なく変更します。数日後、上司がまた雅虎メールに変更すると言ったらどうでしょうか。この時点で変更が大変になります。この時点で先人の経験であるファクトリーパターンを思い出しました。以下のように変更できます。

package com.example.messaging;

/**
 * @author developer
 * @date 2024/5/20
 * @description メールファクトリーインターフェース
 */
public interface MailFactory {

    boolean sendMail(String content);

    static MailFactory getFactory(){
        return mailService;
    }

    static MailFactory mailService = new WYMailService();
}
package com.example.order;

import com.example.messaging.MailFactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description 注文サービス
 */
public class OrderService {

    public void createOrder(){
        System.out.println("注文の作成を開始します");
        System.out.println("注文の作成に成功しました");
        // ユーザーにメールで通知
        //QQMailService mailService = new QQMailService();
        MailFactory mailService = MailFactory.getFactory();
        mailService.sendMail("注文XXXが倉庫で注文されました");
    }
}
package com.example.user;

import com.example.messaging.MailFactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description ユーザーサービス
 */
public class UserService {

    public void registerUser(){
        System.out.println("ユーザー登録を開始します");
        System.out.println("ユーザー登録に成功しました");
        //QQMailService mailService = new QQMailService();
        MailFactory mailService = MailFactory.getFactory();
        mailService.sendMail("こんにちはXXX、貴重なVIPメンバーになりました");
    }
}

よくなりましたね。後でメールサービスを変更する場合は、MailFactory内のgetFactoryメソッドを変更するだけで、他のモジュールのコードを変更する必要がなくなります。少し楽になりましたか?

この方法は、ある程度開閉原則(既存のコードをできるだけ変更せず、拡張コードを追加して要件を満たす)に合致するだけでなく、new XXMailServiceを一箇所で制御できるというもう一つの機能もあります。初期化パラメータを設定し、使用する場所では気にする必要がありません。Integer.valueOfメソッドの実装を参考にできます。

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

これはキャッシュから取得し、キャッシュにない場合はnewしますが、使用する人は気にする必要はありません。

  • 抽象ファクトリーパターン

まず、抽象ファクトリーパターンとファクトリーパターンの違いについて説明しましょう。ファクトリーパターンはオブジェクトを作成するインターフェースを提供し、具体的な製品がどのように実装されるかはサブクラスが決定します。抽象ファクトリーパターンは、関連するオブジェクトのシリーズを作成するために使用されます。抽象ファクトリーパターンには具体的な実装がなく、その製品はすべて抽象的です。少し複雑に聞こえますが、簡単に理解すると、抽象ファクトリーパターンのメソッドも抽象的で、実際のファクトリで抽象ファクトリーパターンの抽象メソッドを実装し、オーバーライドする必要があるということです。

コードを見てみましょう。今、クラウドストレージを実装したいとします。市場には2つのベンダーがあり、一方はアップロードとダウンロードが速いですが、データセキュリティが少し低いです。もう一方はアップロードとダウンロードが平均的ですが、データセキュリティが非常に高いです。ユーザーが設定した好みに応じて、データを最初のベンダーにアップロードするか、第二のベンダーにアップロードするかを決定する必要があるかもしれません。

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description 抽象ファクトリーパターン
 */
public interface CloudStorageFactory {
    ImageProcessor createImageProcessor();
    VideoProcessor createVideoProcessor();

    static CloudStorageFactory getFactory(String type){
        if("highSpeed".equals(type)){
            return new HighSpeedFactory();
        } else if("secure".equals(type)){
            return new SecureFactory();
        } else {
            throw new IllegalArgumentException("未知のタイプ");
        }
    }
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description 画像処理インターフェース
 */
public interface ImageProcessor {
    void processImage();
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description 動画処理インターフェース
 */
public interface VideoProcessor {
    void processVideo();
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description 高速画像処理実装
 */
public class HighSpeedImageProcessor implements ImageProcessor {
    @Override
    public void processImage() {
        System.out.println("高速アップロード画像処理メソッド");
    }
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description 高速動画処理実装
 */
public class HighSpeedVideoProcessor implements VideoProcessor {
    @Override
    public void processVideo() {
        System.out.println("高速アップロード動画処理メソッド");
    }
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description 高速ファクトリー実装
 */
public class HighSpeedFactory implements CloudStorageFactory {

    @Override
    public ImageProcessor createImageProcessor() {
        return new HighSpeedImageProcessor();
    }

    @Override
    public VideoProcessor createVideoProcessor() {
        return new HighSpeedVideoProcessor();
    }
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description セキュア画像処理実装
 */
public class SecureImageProcessor implements ImageProcessor {
    @Override
    public void processImage() {
        System.out.println("高セキュリティアップロード画像処理メソッド");
    }
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description セキュア動画処理実装
 */
public class SecureVideoProcessor implements VideoProcessor {
    @Override
    public void processVideo() {
        System.out.println("高セキュリティアップロード動画処理メソッド");
    }
}

---------------------------------------------------------

package com.example.afactory;

/**
 * @author developer
 * @date 2024/5/20
 * @description セキュアファクトリー実装
 */
public class SecureFactory implements CloudStorageFactory {
    @Override
    public ImageProcessor createImageProcessor() {
        return new SecureImageProcessor();
    }

    @Override
    public VideoProcessor createVideoProcessor() {
        return new SecureVideoProcessor();
    }
}

抽象ファクトリーパターンは、ファクトリと一連の製品の作成を利用から分離し、いつでも別の実際のファクトリと別の実際の製品シリーズに切り替えることができるようにするために使用されます。抽象ファクトリーパターンは、どのようなファクトリが必要か、ファクトリがどのような製品を必要とするかを定義するだけで、具体的なファクトリの実装方法はHighSpeedFactoryでもSecureFactoryでも実装できます。製品の具体的な実装はHighSpeedImageProcessorでもSecureImageProcessorでも実装できます。他のモジュールが使用する場合は、抽象ファクトリーパターンとその抽象製品を使用するだけで済みます。

  • ビルダーパターン

ビルダーパターンは、オブジェクトを作成する際に、多くの部品を組み合わせて拡張機能を実現したり、多くのパラメータを追加したりする問題を解決するために使用されます。以下に簡単な暗号化メソッドを実装します。

package com.example.builder;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * @author developer
 * @date 2024/5/20
 * @description 署名生成クラス
 */
public class Signature {

    public static class Builder{
        private String httpMethod;
        private String queryParams;
        private String requestBody;

        public Builder addMethod(String m){
            httpMethod = m;
            return this;
        }

        public Builder addQueryParams(String q){
            queryParams = q;
            return this;
        }

        public Builder addBody(String b){
            requestBody = b;
            return this;
        }
        
        public String build(){
            String baseStr = new String(Base64.getEncoder().encode((this.httpMethod + this.queryParams + this.requestBody).getBytes(StandardCharsets.UTF_8)));
            return baseStr;
        }
    }


}



package com.example.builder;

/**
 * @author developer
 * @date 2024/5/20
 * @description メインクラス
 */
public class Main {
    public static void main(String[] args) {
        String signature = new Signature.Builder()
                .addMethod("GET")
                .addBody("")
                .addQueryParams("state=1")
                .build();
    }
}

このチェーン呼び出しは皆さんお馴染みのStringBuilder.appendと同じですね。実際、オブジェクトのset属性の方法もビルダーパターンの一種です。setメソッドがthisを返す場合、チェーン呼び出しできます。

  • プロトタイプパターン

個人的にはあまり使わないと思います。文字通り、あるオブジェクトを原型として新しいオブジェクトを作成することです。つまり、cloneメソッドを実装し、ディープコピーを行うことです。

package com.example.prototype;

/**
 * @author developer
 * @date 2024/5/20
 * @description ユーザークラス
 */
public class User {
    private String userName;
    private int userAge;

    public User(String name,int age){
        this.userName = name;
        this.userAge = age;
    }

    public User cloneUser(){
        User newUser = new User(this.userName,this.userAge);
        return newUser;
    }
}

Arrays.copyOfのソースコードも見てみましょう。

    public static int[] copyOf(int[] original, int newLength) {
        int[] copy = new int[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
  • シングルトン

シングルトンパターンはまだよく使われます。シングルトンパターンを使用する場合は、シングルトンクラス内に状態データを記録しないように注意してください。たとえば、クラス内のメソッドの呼び出し回数を記録したい場合、int counter = 0をクラスで定義し、メソッドの最後にcounter++を実行します。シングルスレッドでは問題ありませんが、マルチスレッドの場合、このcounterの値が正しくない可能性があります。なぜなら、各スレッドがこのメソッドを呼び出すたびにこの変数の値をコピーするため、2つのスレッドが同時に呼び出すと、両方ともcounter=2をコピーし、スレッド1がcounter++を実行して値3を設定し、スレッド2も読み取ったcounter=2でcounter++を実行して値3を設定し戻すと、メソッドが3回しか呼び出されなかったと考えますが、実際には4回呼び出されています。

シングルトンの実装方法にはいくつかありますが、ここでは4つ紹介します:イーガンパターン、ラザンパターン、静的ブロック、列挙型

package com.example.singleton;

/**
 * @author developer
 * @date 2024/5/20
 * @description シングルトンサービス
 */
public class SingletonService {
    // イーガンパターン
    private SingletonService(){}
    private static SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }
}

イーガンパターンはクラスで静的変数を定義して初期化します。この変数はクラスのロードとともに初期化され、JVMがクラスのロードプロセスがシングルスレッドであることを保証します。しかし、この方法にも問題があります。リフレクションを使用してこのprivateコンストラクタを呼び出すことができます。シリアライズとデシリアライズの例を以下に示します:

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<SingletonService> declaredConstructor = SingletonService.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        SingletonService ss1 = declaredConstructor.newInstance();
        SingletonService ss2 = SingletonService.getInstance();
        SingletonService ss3 = SingletonService.getInstance();
        System.out.println(System.identityHashCode(ss1)); // 1975012498
        System.out.println(System.identityHashCode(ss2)); // 1808253012
        System.out.println(System.identityHashCode(ss3)); // 1808253012
    }

ss1はシングルトンパターンを使用せずに作成されたもの、ss2とss3はシングルトンパターンを使用して作成されたものです。

public class SingletonService {
    // ラザンパターン(基本版)
    private SingletonService(){}
    private static SingletonService instance = null;

    public static SingletonService getInstance(){
        if(instance == null){
            instance = new SingletonService();
        }
        return instance;
    }
}


public class SingletonService {
    // ラザンパターン(同期版)
    private SingletonService(){}
    private static SingletonService instance = null;

    public static synchronized SingletonService getInstance(){
        if(instance == null){
            instance = new SingletonService();
        }
        return instance;
    }
}

public class SingletonService {
    // ラザンパターン(ダブルチェックロック版)
    private SingletonService(){}
    private static SingletonService instance = null;

    public static SingletonService getInstance(){
        if(instance == null){
            synchronized (SingletonService.class) {
                if(instance == null) {
                    instance = new SingletonService();
                }
            }
        }
        return instance;
    }
}

ラザンパターンの3つの方法の利点は、このクラスが使用されるまでオブジェクトを作成しないことですが、スレッドセーフの問題があります。後の2つの方法ではロックが追加されていますが、パフォーマンスの問題が発生するため、通常はこの方法は使用しません。もちろん、この方法でもリフレクションによるprivateコンストラクタの呼び出しの問題が依然として存在します。

public class SingletonService {
    // 静的ブロック
    private SingletonService(){}
    private static SingletonService instance = null;

    static {
        instance = new SingletonService();
    }
    public static SingletonService getInstance(){
        return instance;
    }
}

静的ブロックはイーガンパターンと似ており、クラスロードのメカニズムでスレッドセーフを保証します。静的ブロックでもリフレクションによるprivateコンストラクタの呼び出しの問題が依然として存在します。

public enum SingletonEnum {
    INSTANCE;
    private String config = "default";
    public String getConfig(){
        return config;
    }
}


    public static void main(String[] args) {
        SingletonEnum.INSTANCE.getConfig();
    }

列挙型はprivateコンストラクタをリフレクションで呼び出すことを回避できる唯一の方法であるため、一般的に推奨されます。

まとめとして、日常の開発プロセスでは、自分でシングルトンを実装することはほとんどなく、SpringフレームワークでSingletonのbeanを作成することが多くなっています。また、SpringはデフォルトでbeanをSingletonモードに設定しています。

タグ: Java デザインパターン ファクトリーパターン 抽象ファクトリーパターン ビルダーパターン

7月19日 19:58 投稿