この記事では、主に IOC 制御反転を使用した Java の 3 つの設計パターンを詳しく紹介します。興味のある方は参考にしてください。
多くの開発者にとって、制御反転 (IoC) は、ほとんど理解されていないため、曖昧な概念です。現実世界のアプリケーションはありません。最良の場合、制御の反転 (IoC) は依存性注入 (DI) と同等であると考えることができます。実際、反転制御と依存関係注入は、両方の側が反転された依存関係管理制御のみを反映する場合にのみ同等とみなされます。ただし、依存性注入は実際には IoC のよく知られた形式です。ただし、実際には、IoC はさまざまなパターンを通じて実装できる比較的広範なソフトウェア設計パラダイムです。この記事では、依存関係注入、オブザーバー パターン、およびテンプレート メソッド パターンが制御の反転をどのように実装するかを紹介します。
さまざまな使用シナリオから要約された他の多くの設計パターンと同様に、IoC の実装も、開発者に適した同様の妥協点です。
一方で、コンポーネントは高度に分離され、アプリケーション ロジックが 1 つにカプセル化されます。これは、IoC を実装する直接的かつ自然な方法です。
一方で、上記の実装では少なくとも 1 つの間接層を構築する必要がありますが、これはユースケースによっては過剰設計になる可能性があります。
次に、これらのプロパティの間でトレードオフを行う方法を理解するのに役立つ、いくつかの具体的な実装を見てみましょう。
IOCパラダイムが明らかに
制御の反転は、特定の特徴を持つパターンです。以下に、Martin Fowler による古典的な IOC の例を示します。これは、コンソールからユーザー データを収集する機能を実装しています。
public static void main(String[] args) { while (true) { BufferedReader userInputReader = new BufferedReader( new InputStreamReader(System.in)); System.out.println("Please enter some text: "); try { System.out.println(userInputReader.readLine()); } catch (IOException e) { e.printStackTrace(); } } }
このユースケースでは、プロセス制御は main メソッドで実行されます。無限ループ呼び出しでは、ユーザー入力が読み取られ、読み取られた内容がコンソールに出力されます。 main メソッドは、ユーザー入力をいつ読み取るか、いつ出力するかを完全に制御します。
上記のプログラムの新しいバージョンを考えてみましょう。このバージョンでは、グラフィカル インターフェイスのテキスト ボックスを通じてユーザー入力を受け取る必要があります。アクション リスナーがバインドされているボタンもあります。この場合、ユーザーがボタンをクリックするたびに、入力されたテキストがリスナーによって収集され、パネルに出力されます。
このバージョンのプログラムでは、実際にはイベント リスナー モデル (この場合はフレームワーク) の制御下にあり、開発者が作成したコードを呼び出してユーザー入力を読み取り、出力します。簡単に言えば、フレームワークは開発者のコードを呼び出しますが、その逆は行いません。このフレームワークは実際には、カスタム コード スニペットを挿入するための一連のエントリ ポイントを開発者に提供する拡張可能な構造です。
この場合、制御は事実上逆転されています。
より一般的な観点から見ると、フレームワーク (インターフェイス実装、実装継承 (サブクラスとも呼ばれる) の形式) によって定義される各呼び出し可能な拡張ポイントは、明確に定義された形式の IoC です。
次の簡単なサーブレットの例を見てください:
public class MyServlet extends HttpServlet { protected void doPost( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // developer implementation here } protected void doGet( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // developer implementation here } }
ここで、HttpServlet クラス (フレームワークに属する) は、サブクラス MyServlet ではなく、プログラムを完全に制御する要素です。サーブレット コンテナによって作成された後、サーブレットの GET および POST HTTP リクエストが受信されると、 doGet() メソッドと doPost() メソッドのコードがそれぞれ自動的に呼び出されます。
サブクラスが基本クラスではなくコントロールの要素である一般的な継承アプローチと比較して、この例ではコントロールが反転されています。
実際、サーブレット メソッドはテンプレート メソッド パターンの実装です。これについては後で詳しく説明します。
拡張可能な API を提供することでオープン/クローズの原則に従うフレームワークを使用する場合、フレームワークを使用する開発者の役割は最終的には独自のカスタム クラスのセットを定義することになります。つまり、開発者はフレームワークによって提供されるクラスを実装するか、複数のクラスを実装することになります。インターフェイスを使用するか、既存の基本クラスから継承します。次に、クラスのインスタンスはフレームワークによって直接インスタンス化され、これらのインスタンスはフレームワークによって呼び出されます。
ここでファウラーの言葉を引用すると、開発者がフレームワークを呼び出すのではなく、フレームワークが開発者を呼び出します。したがって、IoC はハリウッドの原則とも呼ばれます。「私たちに電話しないでください。私たちが電話します。」です。
IOCの実装方法
この問題に関しては、制御の反転を実装するにはいくつかの異なる方法があることは明らかです。これらの一般的な実装方法を要約することもできます。
依存関係を注入して IOC を実装する
前に述べたように、依存関係の注入は IOC を実装する方法であり、最も一般的なオブジェクト指向設計方法です。ただし、考えてみてください。依存関係を注入すると、制御の反転効果がどのように達成されるのでしょうか。
この質問に答えるために、次のオリジナルの例を示します:
public interface UserQueue { void add(User user); void remove(User user); User get(); } public abstract class AbstractUserQueue implements UserQueue { protected LinkedList<User> queue = new LinkedList<>(); @Override public void add(User user) { queue.addFirst(user); } @Override public void remove(User user) { queue.remove(user); } @Override public abstract User get(); } public class UserFifoQueue extends AbstractUserQueue { public User get() { return queue.getLast(); } } public class UserLifoQueue extends AbstractUserQueue { public User get() { return queue.getFirst(); } }
UserQueue インターフェイスは、User オブジェクトをキューに保存するためのパブリック API を定義します (単純化と明確化のため、ここでは User は特定の実装では無視されます)。 AbstractUserQueue は、後続の継承クラスにいくつかのパブリック メソッド実装を提供します。最後の UserFifoQueue と UserLifoQueue は、それぞれ FIFO キューと LIFO キューを実装します。
这是,实现子类多态性的一种有效方式。但是这具体用什么来买我们好处呢?实际上,好处还是蛮多的。
通过创建一个依赖于UserQueue抽象类型(也称为DI术语中的服务)的客户端类,可以在运行时注入不同的实现,无需会重构使用客户端类的代码:
public class UserProcessor { private UserQueue userQueue; public UserProcessor(UserQueue userQueue) { this.userQueue = userQueue; } public void process() { // process queued users here } }
UserProcessor展示了,注入依赖确实是IOC的一种方式。
我们可以通过一些硬编码方式 如 new 操作,直接在构造函数中实例化在UserProcessor中获取对队列的依赖关系。但是,这是典型的代码硬编程,它引入了客户端类与其依赖关系之间的强耦合,并大大降低了可测性。耳边警钟声声想起啦!不是吗?是的,这样设计真的很挫。
该类在构造函数中声明对抽象类 UserQueue 的依赖。也就是说,依赖关系不再通过 在构造函数中使用 new 操作, 相反,通过外部注入的方式,要么使用依赖注入框架(如CDI和谷歌的Guice),要么使用factory或builders模式。
简而言之,使用DI,客户端类的依赖关系的控制,不再位于这些类中;而是在注入器中进行:
public static void main(String[] args) { UserFifoQueue fifoQueue = new UserFifoQueue(); fifoQueue.add(new User("user1")); fifoQueue.add(new User("user2")); fifoQueue.add(new User("user3")); UserProcessor userProcessor = new UserProcessor(fifoQueue); userProcessor.process(); }
上述方式达到了预期效果,而且对UserLifoQueue的注入也简单明了。显而易见,DI确实是实现IOC的一种方式(该例中,DI是实现IOC的一个中间层)。
观察者模式实现IOC
直接通过观察者模式实现IOC,也是一种常见的直观方式。广义上讲,通过观察者实现IOC,与前文提到的通过GUI界面中的action监听器方式类似。但是在使用action监听器情况下,只有在特定的用户事件发生时(点击鼠标,键盘或窗口事件等),才会发生调用。观察者模式通常用于在模型视图的上下文中,跟踪模型对象的状态的变迁。
在一个典型的实现中,一到多个观察者绑定到可观察对象(也称为模式术语中的主题),例如通过调用addObserver方法进行绑定。一旦定义了被观察者和观察者之间的绑定,则被观察者状态的变迁都会触发调用观察者的操作。
为了深入了解这个概念,给出如下例子:
@FunctionalInterface public interface SubjectObserver { void update(); }
值发生改变时,会触发调用上述这个很简单的观察者。真实情况下,通常会提供功能更丰富的API,如需要保存变化的实例,或者新旧值,但是这些都不需要观察action(行为)模式,所以这里举例尽量简单。
下面,给出一个被观察者类:
public class User { private String name; private List<SubjectObserver> observers = new ArrayList<>(); public User(String name) { this.name = name; } public void setName(String name) { this.name = name; notifyObservers(); } public String getName() { return name; } public void addObserver(SubjectObserver observer) { observers.add(observer); } public void deleteObserver(SubjectObserver observer) { observers.remove(observer); } private void notifyObservers(){ observers.stream().forEach(observer -> observer.update()); } }
User类中,当通过setter方法变更其状态事,都会触发调用绑定到它的观察者。
使用主题观察者和主题,以下是实例给出了观察方式:
public static void main(String[] args) { User user = new User("John"); user.addObserver(() -> System.out.println( "Observable subject " + user + " has changed its state.")); user.setName("Jack"); }
每当User对象的状态通过setter方法进行修改时,观察者将被通知并向控制台打印出一条消息。到目前为止,给出了观察者模式的一个简单用例。不过,通过这个看似简单的用例,我们了解到在这种情况下控制是如何实现反转的。
观察者模式下,主题就是起到”框架层“的作用,它完全主导何时何地去触发谁的调用。观察者的主动权被外放,因为观察者无法主导自己何时被调用(只要它们已经被注册到某个主题中的话)。这意味着,实际上我们可以发现控制被反转的”事发地“ – - – 当观察者绑定到主题时:
user.addObserver(() -> System.out.println( "Observable subject " + user + " has changed its state."));
上述用例,简要说明了为什么,观察者模式(或GUI驱动环境中的action监听器)是实现IoC的一种非常简单的方式。正是以这种分散式设计软件组件的形式,使得控制得以发生反转。
通过模板方法模式实现IoC
模板方法模式实现的思想是在一个基类中通过几个抽象方法(也称算法步骤)来定义一个通用的算法,然后让子类提供具体的实现,这样保证算法结构不变。
我们可以应用这个思想,定义一个通用的算法来处理领域实体:
public abstract class EntityProcessor { public final void processEntity() { getEntityData(); createEntity(); validateEntity(); persistEntity(); } protected abstract void getEntityData(); protected abstract void createEntity(); protected abstract void validateEntity(); protected abstract void persistEntity(); }
processEntity() 方法是个模板方法,它定义了处理实体的算法,而抽象方法代表了算法的步骤,它们必须在子类中实现。通过多次继承 EntityProcessor 并实现不同的抽象方法,可以实现若干算法版本。
虽然这说清楚了模板方法模式背后的动机,但人们可能想知道为什么这是 IoC 的模式。
一般的な継承では、サブクラスは基本クラスで定義されたメソッドを呼び出します。このモードでは、比較的実際の状況は次のようになります。サブクラスによって実装されたメソッド (アルゴリズム ステップ) は、基本クラスのテンプレート メソッドによって呼び出されます。したがって、コントロールは実際にはサブクラスではなく、基本クラスに存在します。
これも、階層構造を通じて実装された IoC の典型的な例です。この場合、テンプレート メソッドは、開発者が独自の実装セットを管理するために使用する、調整可能な拡張ポイントの単なる派手な名前です。
概要
Java エコシステム、特に依存関係注入を一般的に使用する多くのフレームワークでは、制御の反転が広く普及していますが、ほとんどの開発者にとって、このモデルはまだ曖昧であり、その適用も依存関係注入に限定されています。この記事では、IoC を実装するためのいくつかの実践的な方法を示すことで、この概念を説明します。
Dependency Injection: クライアントから依存関係を取得する制御は、これらのクラスには存在しなくなりました。これは、基礎となるインジェクター/DI フレームワークによって処理されます。
観察者パターン: 被験者が変わると、制御は観察者から被験者に渡されます。
テンプレート メソッド パターン: 制御は、アルゴリズム ステップを実装するサブクラスではなく、テンプレート メソッドを定義する基本クラスで発生します。
いつものように、IoC をいつどのように使用するかは、各ユースケースを分析することによって決定されます。IoC のために IoC を使用しないでください。
以上がJava が IOC 制御反転の 3 つの設計パターンをどのように使用するかの詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。