デザインパターンの美しさ 59-テンプレートモード (後編): テンプレートモードとコールバック関数の違いと関係は?

59 | テンプレート モード (下記): テンプレート モードと Callback コールバック関数の違いと接続は何ですか?

最後のクラスでは、テンプレート モードの原理、実装、および適用について学びました。フレームワーク開発でよく使われる機能拡張ポイントを提供することで、フレームワークの利用者は、フレームワークのソースコードを変更することなく、拡張ポイントに基づいてフレームワークの機能をカスタマイズできます。さらに、テンプレート モードはコードの再利用にも役立ちます。

再利用と拡張は、テンプレート モードの 2 つの主要な機能ですが、実は、テンプレート モードと同じ役割を果たすことができるもう 1 つの技術的概念、コールバック(Callback) があります。今日は、コールバックの原理、実装、適用、およびテンプレート モードとの違いと接続について見ていきます。

早速、今日から本格的に勉強を始めましょう!

コールバックの原理の分析

通常の関数呼び出しと比較すると、コールバックは双方向の呼び出し関係です。クラスAはあらかじめある関数FをクラスBに登録しておき、クラスAがクラスBの関数Pを呼び出すと、クラスBはクラスAが登録した関数Fを呼び出します。ここでの F 関数は「コールバック関数」です。A が B を呼び出し、B が A を呼び出し、この呼び出しメカニズムを「コールバック」と呼びます。

クラス A はどのようにコールバック関数をクラス B に渡しますか? プログラミング言語が異なれば、実装方法も異なります。C 言語は関数ポインタを使用できますが、Java はコールバック関数をラップするクラス オブジェクトを使用する必要があり、これを略してコールバック オブジェクトと呼びます。ここでは、例として Java 言語を使用して説明します。コードは次のようになります。

public interface ICallback {
  void methodToCallback();
}

public class BClass {
  public void process(ICallback callback) {
    //...
    callback.methodToCallback();
    //...
  }
}

public class AClass {
  public static void main(String[] args) {
    BClass b = new BClass();
    b.process(new ICallback() { //回调对象
      @Override
      public void methodToCallback() {
        System.out.println("Call back me.");
      }
    });
  }
}

上記は Java 言語でのコールバックの典型的なコード実装です。コードの実装から、テンプレート パターンと同様に、コールバックにも再利用と拡張の機能があることがわかります。コールバック関数を除いて、BClass クラスの process() 関数のロジックは再利用できます。ICallback と BClass がフレーム コードで、AClass がフレームを使用するクライアント コードである場合、ICallback を介して process() 関数をカスタマイズできます。つまり、フレームは展開することができます。

実際、コールバックはコード設計だけでなく、より高度なアーキテクチャ設計でも使用できます。たとえば、支払い機能は三者支払いシステムを通じて実装されます. ユーザーが支払い要求を開始した後、通常は支払い結果が返されるまでブロックされませんが、コールバック インターフェイスを登録します (コールバック関数に似ています, 一般的にコールバック URL) を三者支払システムに送信し、三者支払システムの実行が完了した後、コールバック インターフェイスを介して結果がユーザーに返されます。

コールバックは、同期コールバックと非同期コールバック (または遅延コールバック) に分けることができます。同期コールバックは、関数が戻る前にコールバック関数を実行することを指し、非同期コールバックは、関数が戻った後にコールバック関数を実行することを指します。上記のコードは、実際には同期コールバックの実装であり、process() 関数が戻る前に、コールバック関数 methodToCallback() が実行されます。上記の支払いの例は、非同期コールバックの実装です. 支払いが開始されると、コールバック インターフェイスが呼び出されるのを待たずに直接戻ります. アプリケーション シナリオの観点からは、同期コールバックはテンプレート モードに似ており、非同期コールバックはオブザーバー モードに似ています。

適用例 1: JdbcTemplate

Spring は、JdbcTemplate、RedisTemplate、RestTemplate などの多くの Template クラスを提供します。それらはすべて xxxTemplate と呼ばれますが、テンプレート モードに基づいて実装されるのではなく、正確には同期コールバックである必要があるコールバックに基づいて実装されます。同期コールバックは、アプリケーション シナリオのテンプレート パターンに非常に似ているため、命名に関して、これらのクラスは Template (テンプレート) という単語をサフィックスとして使用します。

これらの Template クラスの設計思想は非常に似ているため、分析の例として JdbcTemplate のみを取り上げます。他の Template クラスについては、ソース コードを読んで自分で分析できます。

前の章では、Java がさまざまなタイプのデータベース操作をカプセル化する JDBC クラス ライブラリを提供することについても何度も言及しました。ただし、JDBC を直接使用してコードを記述してデータベースを操作するのは、まだ少し複雑です。たとえば、次の段落は、JDBC を使用してユーザー情報を照会するコードです。

public class JdbcDemo {
  public User queryUser(long id) {
    Connection conn = null;
    Statement stmt = null;
    try {
      //1.加载驱动
      Class.forName("com.mysql.jdbc.Driver");
      conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "xzg", "xzg");

      //2.创建statement类对象,用来执行SQL语句
      stmt = conn.createStatement();

      //3.ResultSet类,用来存放获取的结果集
      String sql = "select * from user where id=" + id;
      ResultSet resultSet = stmt.executeQuery(sql);

      String eid = null, ename = null, price = null;

      while (resultSet.next()) {
        User user = new User();
        user.setId(resultSet.getLong("id"));
        user.setName(resultSet.getString("name"));
        user.setTelephone(resultSet.getString("telephone"));
        return user;
      }
    } catch (ClassNotFoundException e) {
      // TODO: log...
    } catch (SQLException e) {
      // TODO: log...
    } finally {
      if (conn != null)
        try {
          conn.close();
        } catch (SQLException e) {
          // TODO: log...
        }
      if (stmt != null)
        try {
          stmt.close();
        } catch (SQLException e) {
          // TODO: log...
        }
    }
    return null;
  }

}

queryUser() 関数には、ドライバーのロード、データベース接続の作成、ステートメントの作成、接続のクローズ、ステートメントのクローズ、例外処理など、ビジネスとは関係のない多くのプロセス関連のコードが含まれています。異なる SQL 実行要求の場合、これらのプロセスのコードは同じで再利用できるため、毎回再入力する必要はありません。

この問題に対応して、Spring は JdbcTemplate を提供します。これは、データベース プログラミングを簡素化するために JDBC をさらにカプセル化します。JdbcTemplate を使用してユーザー情報をクエリするには、ユーザーの SQL ステートメントのクエリ、クエリ結果と User オブジェクト間のマッピング関係など、このビジネスに関連するコードを記述するだけで済みます。他のプロセス固有のコードは JdbcTemplate クラスにカプセル化されているため、毎回書き直す必要はありません。上記の例を JdbcTemplate で書き直したところ、次のようにコードがはるかに単純になりました。

public class JdbcTemplateDemo {
  private JdbcTemplate jdbcTemplate;

  public User queryUser(long id) {
    String sql = "select * from user where id="+id;
    return jdbcTemplate.query(sql, new UserRowMapper()).get(0);
  }

  class UserRowMapper implements RowMapper<User> {
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
      User user = new User();
      user.setId(rs.getLong("id"));
      user.setName(rs.getString("name"));
      user.setTelephone(rs.getString("telephone"));
      return user;
    }
  }
}

JdbcTemplate の最下層はどのように実装されていますか? そのソースコードを見てみましょう。JdbcTemplate には多くのコードがあるため、関連するコードのみをコピーして以下に貼り付けました。このうち、JdbcTemplate は、コールバック機構によって変更されていない実行プロセスを抽出し、テンプレート メソッド execute() に入れ、変数部分をコールバック StatementCallback として設計し、これをユーザーがカスタマイズします。query() 関数は、execute() 関数を 2 次カプセル化したものであり、インターフェースをより使いやすくします。

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

@Override
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
 Assert.notNull(sql, "SQL must not be null");
 Assert.notNull(rse, "ResultSetExtractor must not be null");
 if (logger.isDebugEnabled()) {
  logger.debug("Executing SQL query [" + sql + "]");
 }

 class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
  @Override
  public T doInStatement(Statement stmt) throws SQLException {
   ResultSet rs = null;
   try {
    rs = stmt.executeQuery(sql);
    ResultSet rsToUse = rs;
    if (nativeJdbcExtractor != null) {
     rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
    }
    return rse.extractData(rsToUse);
   }
   finally {
    JdbcUtils.closeResultSet(rs);
   }
  }
  @Override
  public String getSql() {
   return sql;
  }
 }

 return execute(new QueryStatementCallback());
}

@Override
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
 Assert.notNull(action, "Callback object must not be null");

 Connection con = DataSourceUtils.getConnection(getDataSource());
 Statement stmt = null;
 try {
  Connection conToUse = con;
  if (this.nativeJdbcExtractor != null &&
    this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
   conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
  }
  stmt = conToUse.createStatement();
  applyStatementSettings(stmt);
  Statement stmtToUse = stmt;
  if (this.nativeJdbcExtractor != null) {
   stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
  }
  T result = action.doInStatement(stmtToUse);
  handleWarnings(stmt);
  return result;
 }
 catch (SQLException ex) {
  // Release Connection early, to avoid potential connection pool deadlock
  // in the case when the exception translator hasn't been initialized yet.
  JdbcUtils.closeStatement(stmt);
  stmt = null;
  DataSourceUtils.releaseConnection(con, getDataSource());
  con = null;
  throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
 }
 finally {
  JdbcUtils.closeStatement(stmt);
  DataSourceUtils.releaseConnection(con, getDataSource());
 }
}

応用例2:setClickListener()

クライアント開発ではコントロールのイベントリスナーを登録することが多いですが、例えばAndroidアプリ開発でButtonコントロールのクリックイベントのリスナーを登録するコードは以下の通りです。

Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    System.out.println("I am clicked.");
  }
});

コード構造の観点から見ると、イベント リスナーはコールバックに非常に似ています。つまり、コールバック関数 (onClick()) を含むオブジェクトを別の関数に渡します。アプリケーション シナリオの観点からは、オブザーバー モードに非常に似ています, つまり、オブザーバー (OnClickListener) が事前に登録されています. ユーザーがボタンをクリックすると、クリック イベントがオブザーバーに送信され、対応する onClick( ) 関数が実行されます。

前述したように、コールバックは同期コールバックと非同期コールバックに分けられます。ここでのコールバックは非同期コールバックです. setOnClickListener() 関数にコールバック関数を登録した後は、コールバック関数が実行されるのを待つ必要はありません. これはまた、非同期コールバックがオブザーバー パターンに似ているという、前に述べたことを裏付けるものでもあります。

アプリケーション例 3: addShutdownHook()

フックは「フック」に翻訳できますが、それとコールバックの違いは何ですか?

ネット上ではフックをコールバックと思っている人がいますが、両者は同じことを言っていますが、表現が異なります。Hook は Callback のアプリケーションだと考える人もいます。Callback は文法メカニズムの記述に重点を置いていますが、Hook はアプリケーション シナリオの記述に重点を置いています。個人的には後者の意見に賛成です。ただし、これは重要ではありません。シーンに遭遇したときにコードを認識して使用できればよいだけです。

フックの古典的なアプリケーション シナリオは、Tomcat と JVM のシャットダウン フックです。次に、JVM を例に取りましょう。JVM は、JVM がシャットダウンするフックを登録できる Runtime.addShutdownHook(Thread hook) メソッドを提供します。アプリケーションが閉じられると、JVM は自動的にフック コードを呼び出します。コード例は次のとおりです。

public class ShutdownHookDemo {

  private static class ShutdownHook extends Thread {
    public void run() {
      System.out.println("I am called during shutting down.");
    }
  }

  public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new ShutdownHook());
  }

}

以下に示すように、addShutdownHook() のコード実装を見てみましょう。ここでは、関連するコードのみを示します。

public class Runtime {
  public void addShutdownHook(Thread hook) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(new RuntimePermission("shutdownHooks"));
    }
    ApplicationShutdownHooks.add(hook);
  }
}

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;
    static {
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            hooks = null;
        }
    }

    static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }

        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }
}

コードから、フックに関連するロジックが ApplicationShutdownHooks クラスにカプセル化されていることがわかります。アプリケーションが閉じられると、JVM はこのクラスの runHooks() メソッドを呼び出して、複数のスレッドを作成し、複数のフックを同時に実行します。フックを登録した後、フックが完了するのを待つ必要がないため、これも非同期コールバックです。

テンプレート パターンとコールバック

コールバックの原則、実装、適用はすべてここで終了です。次に、アプリケーション シナリオとコード実装の観点から、テンプレート モードとコールバックを比較してみましょう。

アプリケーション シナリオの観点からは、同期コールバックはテンプレート モードとほぼ同じです。それらはすべて大規模なアルゴリズム スケルトンにあり、特定のステップを自由に置き換えて、コードの再利用と拡張の目的を達成します。非同期コールバックはテンプレート モードとはかなり異なり、オブザーバー モードに似ています。

コード実装の観点からは、コールバック パターンとテンプレート パターンはまったく異なります。コールバックは構成関係に基づいて実装されます.オブジェクトを別のオブジェクトに渡すことはオブジェクト間の関係です.テンプレートモードは継承関係に基づいて実装されます.サブクラスはクラス間の関係である親クラスの抽象メソッドをオーバーライドします. . .

また、構成は継承よりも優れていることも前述しました。実際、ここにも例外はありません。コードの実装に関しては、コールバックはテンプレート モードよりも柔軟です。これは、主に次の点に反映されています。

  • 単一継承のみをサポートする Java のような言語では、テンプレート パターンに基づいて記述されたサブクラスは親クラスを継承し、継承する機能がなくなります。
  • コールバックでは、事前にクラスを定義することなく、匿名クラスを使用してコールバック オブジェクトを作成できますが、テンプレート パターンでは、実装ごとに異なるサブクラスを定義します。
  • クラスで複数のテンプレート メソッドが定義されていて、各メソッドに対応する抽象メソッドがある場合、テンプレート メソッドの 1 つしか使用しない場合でも、サブクラスはすべての抽象メソッドを実装する必要があります。コールバックはより柔軟で、使用するテンプレート メソッドにコールバック オブジェクトを挿入するだけで済みます。

前のクラスのクラスディスカッションのトピックを覚えていますか? これを見れば、あなたは答えを持っているはずですよね?

キーレビュー

では、本日の内容は以上です。集中する必要があることをまとめて一緒に確認しましょう。

今日は、コールバックに焦点を当てます。テンプレート パターンと同じ役割があります。コードの再利用と拡張です。一部のフレームワーク、クラス ライブラリ、コンポーネントなどの設計でよく使用されます。

通常の関数呼び出しと比較すると、コールバックは双方向の呼び出し関係です。クラスAはあらかじめある関数FをクラスBに登録しておき、クラスAがクラスBの関数Pを呼び出すと、クラスBはクラスAが登録した関数Fを呼び出します。ここでの F 関数は「コールバック関数」です。A が B を呼び出し、B が A を呼び出し、この呼び出しメカニズムを「コールバック」と呼びます。

コールバックは、同期コールバックと非同期コールバックに細分できます。アプリケーション シナリオの観点からは、同期コールバックはテンプレート モードに似ており、非同期コールバックはオブザーバー モードに似ています。コールバック モードとテンプレート モードの違いは、アプリケーション シナリオよりもコードの実装にあります。コールバックは構成関係に基づいて実装され、テンプレート モードは継承関係に基づいて実装され、コールバックはテンプレート モードよりも柔軟です。

クラスディスカッション

Callback と Hook の違いを理解していますか? あなたがよく知っているプログラミング言語に、対応する文法上の概念はありますか? コールバックかフックか?

メッセージを残して、あなたの考えを私と共有してください。何かを得た場合は、この記事を友達と共有してください。

おすすめ

転載: blog.csdn.net/fegus/article/details/130498834