1. 序章

デザインパターンはソフトウェア開発の重要な部分です。 これらのソリューションは、繰り返し発生する問題を解決するだけでなく、開発者が一般的なパターンを認識することでフレームワークの設計を理解するのにも役立ちます。

このチュートリアルでは、Springフレームワークで使用される最も一般的な4つのデザインパターンを見ていきます。

  1. シングルトンパターン
  2. ファクトリメソッドパターン
  3. プロキシパターン
  4. テンプレートパターン

また、Springがこれらのパターンを使用して、開発者の負担を軽減し、ユーザーが面倒なタスクをすばやく実行できるようにする方法についても説明します。

2. シングルトンパターン

シングルトンパターンは、アプリケーションごとにオブジェクトのインスタンスが1つだけ存在することを保証するメカニズムです。 このパターンは、共有リソースを管理したり、ロギングなどの分野横断的なサービスを提供したりする場合に役立ちます。

2.1. シングルトンビーンズ

一般に、シングルトンはアプリケーションに対してグローバルに一意ですが、Springではこの制約が緩和されます。 代わりに、 Springは、シングルトンをSpringIoCコンテナーごとに1つのオブジェクトに制限します。 実際には、これは、Springがアプリケーションコンテキストごとにタイプごとに1つのBeanのみを作成することを意味します。

Springのアプローチは、アプリケーションが複数のSpringコンテナーを持つことができるため、シングルトンの厳密な定義とは異なります。 したがって、複数のコンテナがある場合、同じクラスの複数のオブジェクトが1つのアプリケーションに存在する可能性があります。

 

 

デフォルトでは、SpringはすべてのBeanをシングルトンとして作成します。

2.2. 自動配線シングルトン

たとえば、単一のアプリケーションコンテキスト内に2つのコントローラーを作成し、それぞれに同じタイプのBeanを注入できます。

まず、Bookドメインオブジェクトを管理するBookRepositoryを作成します。

次に、 LibraryController を作成します。これは、 BookRepository を使用して、ライブラリ内の本の数を返します。

@RestController
public class LibraryController {
    
    @Autowired
    private BookRepository repository;

    @GetMapping("/count")
    public Long findCount() {
        System.out.println(repository);
        return repository.count();
    }
}

最後に、 BookController を作成します。これは、IDで本を検索するなど、Book固有のアクションに焦点を当てています。

@RestController
public class BookController {
     
    @Autowired
    private BookRepository repository;
 
    @GetMapping("/book/{id}")
    public Book findById(@PathVariable long id) {
        System.out.println(repository);
        return repository.findById(id).get();
    }
}

次に、このアプリケーションを起動し、 /countおよび/book / 1:でGETを実行します。

curl -X GET http://localhost:8080/count
curl -X GET http://localhost:8080/book/1

アプリケーションの出力では、両方のBookRepositoryオブジェクトが同じオブジェクトIDを持っていることがわかります。

com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f
com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f

LibraryControllerBookControllerBookRepositoryオブジェクトIDは同じであり、Springが両方のコントローラーに同じBeanを注入したことを証明しています。

@ Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) annotation を使用して、Beanスコープをシングルトンからプロトタイプに変更することで、BookRepositoryBeanの個別のインスタンスを作成できます。

そうすることで、Springは、作成するBookRepositoryBeanごとに個別のオブジェクトを作成するように指示されます。 したがって、各コントローラーの BookRepository のオブジェクトIDを再度調べると、それらが同じではなくなっていることがわかります。

3. ファクトリメソッドパターン

ファクトリメソッドパターンには、目的のオブジェクトを作成するための抽象メソッドを持つファクトリクラスが含まれます。

多くの場合、特定のコンテキストに基づいてさまざまなオブジェクトを作成する必要があります。

たとえば、アプリケーションで車両オブジェクトが必要になる場合があります。 航海環境ではボートを作成したいのですが、航空宇宙環境では飛行機を作成したいと思います。

 

 

これを実現するために、目的のオブジェクトごとにファクトリ実装を作成し、具象ファクトリメソッドから目的のオブジェクトを返すことができます。

3.1. アプリケーションコンテキスト

Springは、依存性注入(DI)フレームワークのルートでこの手法を使用しています。

基本的に、Springは豆コンテナを豆を生産する工場として扱います。

したがって、SpringはBeanFactoryインターフェースをBeanコンテナーの抽象化として定義します。

public interface BeanFactory {

    getBean(Class<T> requiredType);
    getBean(Class<T> requiredType, Object... args);
    getBean(String name);

    // ...
]

各getBeanメソッドはファクトリメソッドと見なされ、beanのタイプや名前など、メソッドに指定された基準に一致するbeanを返します。

次に、SpringはBeanFactoryApplicationContextインターフェースで拡張します。これにより、追加のアプリケーション構成が導入されます。 Springはこの構成を使用して、XMLファイルやJavaアノテーションなどの外部構成に基づいてBeanコンテナーを起動します。

次に、AnnotationConfigApplicationContextなどのApplicationContextクラスの実装を使用して、BeanFactoryインターフェイスから継承されたさまざまなファクトリメソッドを介してBeanを作成できます。

まず、簡単なアプリケーション構成を作成します。

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ApplicationConfig {
}

次に、コンストラクター引数を受け入れない単純なクラスFooを作成します。

@Component
public class Foo {
}

次に、単一のコンストラクター引数を受け入れる別のクラスBarを作成します。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {
 
    private String name;
     
    public Bar(String name) {
        this.name = name;
    }
     
    // Getter ...
}

最後に、ApplicationContextAnnotationConfigApplicationContext実装を介してBeanを作成します。

@Test
public void whenGetSimpleBean_thenReturnConstructedBean() {
    
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Foo foo = context.getBean(Foo.class);
    
    assertNotNull(foo);
}

@Test
public void whenGetPrototypeBean_thenReturnConstructedBean() {
    
    String expectedName = "Some name";
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Bar bar = context.getBean(Bar.class, expectedName);
    
    assertNotNull(bar);
    assertThat(bar.getName(), is(expectedName));
}

getBean ファクトリメソッドを使用すると、クラスタイプと、 Bar の場合はコンストラクターパラメーターのみを使用して、構成済みBeanを作成できます。

3.2. 外部構成

外部構成に基づいてアプリケーションの動作を完全に変更できるため、このパターンは用途が広いです。

アプリケーションの自動配線オブジェクトの実装を変更したい場合は、使用するApplicationContextの実装を調整できます。

 

たとえば、AnnotationConfigApplicationContextClassPathXmlApplicationContextなどのXMLベースの構成クラスに変更できます。

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { 

    String expectedName = "Some name";
    ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
 
    // Same test as before ...
}

4. プロキシパターン

プロキシは私たちのデジタル世界で便利なツールであり、ソフトウェア(ネットワークプロキシなど)の外部で非常に頻繁に使用されます。 コードでは、プロキシパターンは、あるオブジェクト(プロキシ)が別のオブジェクト(サブジェクトまたはサービス)へのアクセスを制御できるようにする手法です

 

 

4.1. トランザクション

プロキシを作成するには、サブジェクトと同じインターフェイスを実装し、サブジェクトへの参照を含むオブジェクトを作成します。

その後、サブジェクトの代わりにプロキシを使用できます。

Springでは、基礎となるBeanへのアクセスを制御するためにBeanがプロキシされます。 トランザクションを使用する場合、このアプローチが見られます。

@Service
public class BookManager {
    
    @Autowired
    private BookRepository repository;

    @Transactional
    public Book create(String author) {
        System.out.println(repository.getClass().getName());
        return repository.create(author);
    }
}

BookManager クラスでは、createメソッドに@Transactionalアノテーションを付けます。 このアノテーションは、Springにcreateメソッドをアトミックに実行するように指示します。 プロキシがないと、Springは BookRepository beanへのアクセスを制御し、トランザクションの一貫性を確保できません。

4.2. CGLibプロキシ

代わりに、 Springは、BookRepository Bean をラップするプロキシを作成し、Beanをインストルメントしてcreateメソッドをアトミックに実行します。

BookManager#create メソッドを呼び出すと、次の出力が表示されます。

com.baeldung.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c

通常、標準のBookRepositoryオブジェクトIDが表示されると予想されます。 代わりに、EnhancerBySpringCGLIBオブジェクトIDが表示されます。

舞台裏では、SpringはBookRepositoryオブジェクトをEnhancerBySpringCGLIBオブジェクトとして内部にラップしています。 したがって、Springは BookRepository オブジェクトへのアクセスを制御します(トランザクションの一貫性を確保します)。

通常、Springは2種類のプロキシを使用します。

  1. CGLibプロキシ–クラスをプロキシするときに使用されます
  2. JDK動的プロキシ–インターフェースをプロキシするときに使用されます

基盤となるプロキシを公開するためにトランザクションを使用しましたが、 Springは、Beanへのアクセスを制御する必要があるすべてのシナリオでプロキシを使用します。

5. テンプレートメソッドパターン

多くのフレームワークでは、コードの重要な部分がボイラープレートコードです。

たとえば、データベースでクエリを実行する場合、同じ一連の手順を完了する必要があります。

  1. 接続を確立する
  2. クエリを実行する
  3. クリーンアップを実行します
  4. 接続を閉じます

これらの手順は、テンプレートメソッドパターンの理想的なシナリオです。

5.1. テンプレートとコールバック

テンプレートメソッドパターンは、アクションに必要なステップを定義し、定型的なステップを実装し、カスタマイズ可能なステップをabstractのままにする手法です。 サブクラスは、この抽象クラスを実装し、欠落しているステップの具体的な実装を提供できます。

データベースクエリの場合、テンプレートを作成できます。

public abstract DatabaseQuery {

    public void execute() {
        Connection connection = createConnection();
        executeQuery(connection);
        closeConnection(connection);
    } 

    protected Connection createConnection() {
        // Connect to database...
    }

    protected void closeConnection(Connection connection) {
        // Close connection...
    }

    protected abstract void executeQuery(Connection connection);
}

または、コールバックメソッドを指定して、欠落しているステップを提供することもできます。

コールバックメソッドは、サブジェクトがクライアントに目的のアクションが完了したことを通知できるようにするメソッドです

場合によっては、サブジェクトはこのコールバックを使用して、結果のマッピングなどのアクションを実行できます。

 

 

たとえば、 executeQuery メソッドを使用する代わりに、 execute メソッドにクエリ文字列と、結果を処理するためのコールバックメソッドを提供できます。

まず、 Results オブジェクトを受け取り、それをタイプTのオブジェクトにマップするコールバックメソッドを作成します。

public interface ResultsMapper<T> {
    public T map(Results results);
}

次に、 DatabaseQuery クラスを変更して、このコールバックを利用します。

public abstract DatabaseQuery {

    public <T> T execute(String query, ResultsMapper<T> mapper) {
        Connection connection = createConnection();
        Results results = executeQuery(connection, query);
        closeConnection(connection);
        return mapper.map(results);
    ]

    protected Results executeQuery(Connection connection, String query) {
        // Perform query...
    }
}

このコールバックメカニズムは、まさにSpringがJdbcTemplateクラスで使用するアプローチです。

5.2.  JdbcTemplate

JdbcTemplate クラスは、クエリStringおよびResultSetExtractorオブジェクトを受け入れるqueryメソッドを提供します。

public class JdbcTemplate {

    public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
        // Execute query...
    }

    // Other methods...
}

ResultSetExtractor は、 ResultSet オブジェクト(クエリの結果を表す)をタイプTのドメインオブジェクトに変換します。

@FunctionalInterface
public interface ResultSetExtractor<T> {
    T extractData(ResultSet rs) throws SQLException, DataAccessException;
}

Springは、より具体的なコールバックインターフェイスを作成することにより、ボイラープレートコードをさらに削減します。

たとえば、 RowMapper インターフェイスは、SQLデータの単一行をタイプTのドメインオブジェクトに変換するために使用されます。

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

RowMapperインターフェイスを期待されるResultSetExtractorに適合させるために、SpringはRowMapperResultSetExtractorクラスを作成します。

public class JdbcTemplate {

    public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
    }

    // Other methods...
}

行の反復を含むResultSetオブジェクト全体を変換するためのロジックを提供する代わりに、単一の行を変換する方法のロジックを提供できます。

public class BookRowMapper implements RowMapper<Book> {

    @Override
    public Book mapRow(ResultSet rs, int rowNum) throws SQLException {

        Book book = new Book();
        
        book.setId(rs.getLong("id"));
        book.setTitle(rs.getString("title"));
        book.setAuthor(rs.getString("author"));
        
        return book;
    }
}

このコンバーターを使用すると、 JdbcTemplate を使用してデータベースにクエリを実行し、結果の各行をマップできます。

JdbcTemplate template = // create template...
template.query("SELECT * FROM books", new BookRowMapper());

JDBCデータベース管理とは別に、Springは次のテンプレートも使用します。

6. 結論

このチュートリアルでは、SpringFrameworkで適用される最も一般的な4つのデザインパターンを確認しました。

また、Springがこれらのパターンを利用して、開発者の負担を軽減しながら豊富な機能を提供する方法についても検討しました。

この記事のコードは、GitHubにあります。