25,000 語で 23 のデザインパターン (複数の画像 + コード) を説明
目次
- 創作パターン
- 構造パターン
- 行動モデル
- 要約する
序文
私は、読者がすぐに読めて、ひと目で理解でき、読んだ後も使える、さまざまなパターンを混乱させない、デザインパターンを紹介する記事を書きたいと常々思っていました。
デザインパターンとは、実際の業務で書かれた様々なコードを高度に抽象的にまとめたもので、最も有名なものはGang of Four ( GoF )分類であり、デザインパターンを23の古典的なパターンに分類しています。それぞれ、創造モード、構造モード、行動モードの 3 つのカテゴリに分類されます。
最初に共有する重要な設計原則がいくつかあり、これらの原則は全文に貫かれています。
- 実装ではなくインターフェイスに対するプログラム。これは非常に重要であり、エレガントでスケーラブルなコードの最初のステップであるため、これ以上言う必要はありません。
- 単一責任の原則。各クラスには 1 つの機能のみを含める必要があり、その機能はクラスによって完全にカプセル化される必要があります。
- 変更の場合は閉じられ、拡張の場合は開かれます。修正を終了するということは、一生懸命コードを書いて、実装すべき機能や修正すべきバグが完成したという意味で、他人が言うだけでは変更できないので、わかりやすいです。つまり、作成したコードに基づいて拡張するのは簡単です。
作成パターンは単純ですが、あまり興味がありませんが、構造パターンと行動パターンはより興味深いものです。
**
**
創作パターン
作成パターンの役割はオブジェクトを作成することです。オブジェクトの作成に関して最もよく知られているのは、新しいオブジェクトを作成し、関連する属性を設定することです。ただし、多くのシナリオでは、特にクラスを定義して他の開発者に提供する必要がある場合、オブジェクトを作成するためのより使いやすい方法をクライアントに提供する必要があります。
シンプルなファクトリーパターン
名前と同じくらいシンプルで、非常にシンプルです。コードに移動するだけです。
public class FoodFactory {
public static Food makeFood(String name) {
if (name.equals("noodle")) {
Food noodle = new LanZhouNoodle();
noodle.addSpicy("more");
return noodle;
} else if (name.equals("chicken")) {
Food chicken = new HuangMenChicken();
chicken.addCondiment("potato");
return chicken;
} else {
return null;
}
}
}
その中で、LanZhouNoodle と HuangMenChicken は両方とも Food から継承しています。
簡単に言えば、単純なファクトリ パターンは通常次のようになります。ファクトリ クラス XxxFactory には静的メソッドがあり、異なるパラメータに従って同じ親クラス (または同じインターフェイスを実装) から派生した異なるインスタンス オブジェクトを返します。
私たちは単一責任の原則を重視しており、クラスは 1 つの機能のみを提供し、FoodFactory の機能はさまざまな種類の食品の生産のみを担当します。
工場パターン
シンプルファクトリーモデルは非常にシンプルなので、ニーズを満たせるのであればわざわざ作る必要はないと思います。ファクトリ パターンを導入する必要がある理由は、2 つ以上のファクトリを使用する必要があることが多いためです。
public interface FoodFactory {
Food makeFood(String name);
}
public class ChineseFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new ChineseFoodA();
} else if (name.equals("B")) {
return new ChineseFoodB();
} else {
return null;
}
}
}
public class AmericanFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new AmericanFoodA();
} else if (name.equals("B")) {
return new AmericanFoodB();
} else {
return null;
}
}
}
このうち、 ChineseFoodA 、 ChineseFoodB 、 AmericanFoodA 、 AmericanFoodB はすべて Food から派生したものです。
クライアントの呼び出し:
public class APP {
public static void main(String[] args) {
// 先选择一个具体的工厂
FoodFactory factory = new ChineseFoodFactory();
// 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
Food food = factory.makeFood("A");
}
}
タイプ A の食品を作るには、すべて makeFood("A") を呼び出しますが、異なる工場で生産される製品はまったく異なります。
最初のステップでは、適切なファクトリを選択する必要があります。その後の 2 番目のステップは、基本的に単純なファクトリと同じです。
重要なのは、最初のステップで必要なファクトリーを選択する必要があるということです。たとえば、LogFactory インターフェイスがあり、実装クラスには FileLogFactory と KafkaLogFactory が含まれており、これらはそれぞれファイルと Kafka へのログの書き込みに対応します。明らかに、クライアントの最初のステップでは、FileLogFactory と KafkaLogFactory のどちらをインスタンス化するかを決定する必要があります。すべての操作の後で。
単純ではありますが、読者がより明確に見えるように、すべてのコンポーネントを画像上に描画します。
抽象的な工場パターン
製品ファミリーに関しては、抽象的なファクトリー パターンを導入する必要があります。
典型的な例は、コンピューターの構築です。最初に抽象ファクトリー パターンを紹介するのではなく、それを実装する方法を見てみましょう。
コンピュータは多くのコンポーネントで構成されているため、CPU とマザーボードを抽象化し、CPU は CPUFactory によって生産され、マザーボードは MainBoardFactory によって生産され、図のように CPU とマザーボードを組み合わせます。次の図:
このときのクライアントの呼び出しは次のとおりです。
// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();
// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();
// 组装 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);
CPU工場とマザーボード工場を分けて見ると、先ほど述べた工場モデルになります。この方法は拡張も簡単です。コンピュータにハードディスクを追加する場合、既存のファクトリを変更せずに、HardDiskFactory と対応する実装を追加するだけで済みます。
ただし、この方法には問題があります。つまり、Intel 製の CPU と AMD 製のマザーボードに互換性がない場合、クライアントはそれらが互換性がないことを認識していないため、このコードはエラーになりやすくなります。間違ってランダムな組み合わせが表示されてしまいます。
以下は、製品を構成する一連のアクセサリのコレクションを表す製品ファミリーの概念です。
この種の製品ファミリーの場合、それをサポートするには抽象ファクトリー パターンが必要です。CPU 工場、マザーボード工場、ハードディスク工場、ディスプレイ工場などを定義することはなくなり、コンピューター工場を直接定義し、各コンピューター工場がすべての機器の生産を担当するため、互換性の問題があってはなりません。
このとき、クライアントにとっては、CPUメーカー、マザーボードメーカー、ハードディスクメーカーなどを直接選択する必要がなくなり、すべての生産を責任を持って保証できるブランド工場を直接選択できるようになります。互換性があり使用可能です。
public static void main(String[] args) {
// 第一步就要选定一个“大厂”
ComputerFactory cf = new AmdFactory();
// 从这个大厂造 CPU
CPU cpu = cf.makeCPU();
// 从这个大厂造主板
MainBoard board = cf.makeMainBoard();
// 从这个大厂造硬盘
HardDisk hardDisk = cf.makeHardDisk();
// 将同一个厂子出来的 CPU、主板、硬盘组装在一起
Computer result = new Computer(cpu, board, hardDisk);
}
もちろん、抽象工場の問題も明らかで、例えばディスプレイを追加したい場合、すべての工場を修正し、すべての工場にディスプレイの製造方法を追加する必要があります。これは、変更にはクローズされ、拡張にはオープンであるという設計原則に少し違反しています。
シングルトンパターン
シングルトン パターンは最もよく使用され、最も多くの間違いを犯します。
ハングリーマンモードは最もシンプルです:
public class Singleton {
// 首先,将 new Singleton() 堵死
private Singleton() {};
// 创建私有静态实例,意味着这个类第一次使用的时候就会进行创建
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
// 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
// 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
public static Date getDate(String mode) {return new Date();}
}
飢えた男モードの欠点は多くの人が知っていますが、実稼働プロセスでそのような状況に遭遇することは稀だと思います。シングルトン クラスを定義し、そのインスタンスは必要ありませんが、1 つまたは複数の静的クラスを配置します。使用されるメソッドはこのクラスに詰め込まれます。
フルマンモードは最もエラーが発生しやすいモードです。
public class Singleton {
// 首先,也是先堵死 new Singleton() 这条路
private Singleton() {}
// 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
// 加锁
synchronized (Singleton.class) {
// 这一次判断也是必须的,不然会有并发问题
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
二重チェックとは、インスタンスが null であるかどうかを 2 回チェックすることを指します。
ここでは揮発性が必要であり、読者の注意を引くことを願っています。
書き方が分からず、 getInstance() メソッドのシグネチャに直接 synchronized を追加している人も多く、言うまでもなくパフォーマンスが悪すぎます。
ネストされたクラスは最も古典的なものであるため、将来的には誰もが使用するでしょう。
public class Singleton3 {
private Singleton3() {}
// 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
private static class Holder {
private static Singleton3 instance = new Singleton3();
}
public static Singleton3 getInstance() {
return Holder.instance;
}
}
多くの人は、このネストされたクラスを静的内部クラスと呼ぶことに注意してください。厳密に言えば、内部クラスとネストされたクラスは異なり、アクセスできる外部クラスの権限も異なります。
最後に, 列挙について話しましょう. 列挙は非常に特殊です. 列挙はクラスがロードされるときにその中のすべてのインスタンスを初期化し, JVM はインスタンスが再びインスタンス化されないことを保証するため, 本質的にシングルトンです.
シングルトンの実装に列挙型が使用されることはほとんどありませんが、RxJava のソース コードでは、多くの場所で列挙型がシングルトンの実装に使用されています。
ビルダーモード
よく見かける XxxBuilder クラスは、通常、ビルダー パターンの産物です。実際にはビルダー モードには多くのバリエーションがありますが、クライアントでは通常は同じモードを使用します。
Food food = new FoodBuilder().a().b().c().build();
Food food = Food.builder().a().b().c().build();
ルーチンとしては、まず新しいビルダーを作成し、次に一連のメソッドを呼び出し、最後に build() メソッドを再度呼び出すと、必要なオブジェクトがそこに存在します。
通常のビルダー モデルに戻ります。
class User {
// 下面是“一堆”的属性
private String name;
private String password;
private String nickName;
private int age;
// 构造方法私有化,不然客户端就会直接调用构造方法了
private User(String name, String password, String nickName, int age) {
this.name = name;
this.password = password;
this.nickName = nickName;
this.age = age;
}
// 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,
// 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
// 下面是和 User 一模一样的一堆属性
private String name;
private String password;
private String nickName;
private int age;
private UserBuilder() {
}
// 链式调用设置各个属性值,返回 this,即 UserBuilder
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。
// 当然,可以在 “复制” 之前做点检验
public User build() {
if (name == null || password == null) {
throw new RuntimeException("用户名和密码必填");
}
if (age <= 0 || age >= 150) {
throw new RuntimeException("年龄不合法");
}
// 还可以做赋予”默认值“的功能
if (nickName == null) {
nickName = name;
}
return new User(name, password, nickName, age);
}
}
}
核心は、まずすべてのプロパティを Builder に設定し、次にbuild() メソッド中にこれらのプロパティを実際に生成されたオブジェクトにコピーすることです。
クライアント呼び出しを見てください。
public class APP {
public static void main(String[] args) {
User d = User.builder()
.name("foo")
.password("pAss12345")
.age(25)
.build();
}
}
正直なところ、ビルダー モードの連鎖記述方法は非常に魅力的ですが、「役に立たない」ビルダー コードをたくさん書いた後では、このモードは無駄だと感じています。ただし、多くの属性があり、一部が必須で一部がオプションである場合、このモードではコードがより明確になります。呼び出し元にBuilder の構築メソッドで必須フィールドを提供するよう強制できます。また、コードは、ユーザーの構築メソッドよりも build() メソッドで各パラメータを検証する方が洗練されています。
余談ですが、読者には lombok を使用することを強くお勧めします。 lombok を使用すると、上記のコードの多くは次のようになります。
@Builderclass User { private String name; private String password; private String nickName; private int age;}
どうですか、節約した時間を使って何か他のことができますか?
もちろん、チェーン書き込みのみが必要で、ビルダー モードを必要としない場合は、非常に簡単な方法があります。User の getter メソッドは変更せず、すべての setter メソッドでthisを返せるようにすると、次のようになります。移行:
User user = new User().setName("").setPassword("").setAge(20);
このように使っている人も多いですが、筆者はこの書き方は非常に品がないので推奨していないと考えています。
プロトタイプパターン
これが私が話したい創造パターンの最後のデザインパターンです。
プロトタイプ モードは非常にシンプルです。プロトタイプインスタンスがあり、このプロトタイプ インスタンスに基づいて新しいインスタンスが生成されます。つまり、「クローン作成」です。
Object クラスには clone() メソッドがあり、新しいオブジェクトを生成するために使用されます。もちろん、このメソッドを呼び出したい場合、Java では最初にクラスに Cloneable インターフェイスを実装する必要があります。このインターフェイスはメソッドを定義しませんが、そうではありません。その場合、clone() のときに、CloneNotSupportedException がスローされます。
protected native Object clone() throws CloneNotSupportedException;
Java のクローン作成はシャロー クローン作成であり、オブジェクト参照が見つかると、クローン化されたオブジェクトと元のオブジェクト内の参照は同じオブジェクトを指します。ディープ クローン作成を実装する通常の方法は、オブジェクトをシリアル化してから逆シリアル化することです。
ここでプロトタイプモードを理解すれば十分だと思いますが、このコードやあのコードがプロトタイプモードであるといろいろ言っても意味がありません。
作成パターンの概要
作成パターンは一般に比較的単純です。その機能は、さまざまなタスクの最初のステップであるインスタンス オブジェクトを生成することです。オブジェクト指向のコードを記述するため、最初のステップは当然ながらオブジェクトを作成することです。
単純な工場モデルは最も単純です。工場モデルは単純な工場モデルに基づいて工場を選択する次元を追加し、最初のステップは適切な工場を選択することです。抽象的な工場モデルには製品ファミリーの概念があります。各製品に互換性の問題がある場合は、抽象ファクトリー パターンを使用します。シングルトン モードは言うまでもなく、同じオブジェクトがグローバルに使用されることを保証するために、一方ではセキュリティ上の理由から、他方ではリソースを節約するために、ビルダー モードは特別に設計されています。多くの属性を持つクラスでは、コードをより美しくするためにプロトタイプモードが使用されますが、少なくとも、Object クラスの clone() メソッドに関する知識を理解するだけで済みます。
構造パターン
前の作成パターンでは、オブジェクトを作成するためのいくつかの設計パターンを紹介しましたが、このセクションで紹介する構造パターンは、コードの構造を変更することで分離の目的を達成し、コードの保守と拡張を容易にすることを目的としています。
プロキシモード
最初に導入されるプロキシ モードは、最も一般的に使用されるモードの 1 つです。プロキシは、具体的な実装クラスの実装の詳細を隠すために使用され、通常は実際の実装の前後にロジックを追加するために使用されます。
これはプロキシであるため、実際の実装をクライアントから隠す必要があり、プロキシはクライアントからのすべてのリクエストを処理します。もちろん、プロキシは単なるプロキシであり、実際のビジネス ロジックを完成させるわけではなく、単なる表面層にすぎませんが、クライアントにとっては、クライアントのニーズの実際の実装として動作する必要があります。
エージェントという言葉を理解すると、このモデルは実際には単純です。
public interface FoodService {
Food makeChicken();
Food makeNoodle();
}
public class FoodServiceImpl implements FoodService {
public Food makeChicken() {
Food f = new Chicken()
f.setChicken("1kg");
f.setSpicy("1g");
f.setSalt("3g");
return f;
}
public Food makeNoodle() {
Food f = new Noodle();
f.setNoodle("500g");
f.setSalt("5g");
return f;
}
}
// 代理要表现得“就像是”真实实现类,所以需要实现 FoodService
public class FoodServiceProxy implements FoodService {
// 内部一定要有一个真实的实现类,当然也可以通过构造方法注入
private FoodService foodService = new FoodServiceImpl();
public Food makeChicken() {
System.out.println("我们马上要开始制作鸡肉了");
// 如果我们定义这句为核心代码的话,那么,核心代码是真实实现类做的,
// 代理只是在核心代码前后做些“无足轻重”的事情
Food food = foodService.makeChicken();
System.out.println("鸡肉制作完成啦,加点胡椒粉"); // 增强
food.addCondiment("pepper");
return food;
}
public Food makeNoodle() {
System.out.println("准备制作拉面~");
Food food = foodService.makeNoodle();
System.out.println("制作完成啦")
return food;
}
}
クライアント呼び出し。インターフェイスをインスタンス化するにはプロキシを使用する必要があることに注意してください。
// 这里用代理类来实例化
FoodService foodService = new FoodServiceProxy();
foodService.makeChicken();
いいえ、プロキシ モードは「メソッド パッケージ化」または「メソッド拡張」を実行することであることがわかりました。アスペクト指向プログラミングでは、これは実際には動的プロキシのプロセスです。たとえば、Spring ではプロキシ クラスを自分たちで定義しませんが、Spring はプロキシを動的に定義し、@Before、@After、@Around で定義したコード ロジックをプロキシに動的に追加するのに役立ちます。
動的プロキシについて言えば、Spring には 2 種類の動的プロキシがあることが拡張できます。1 つは、クラスが UserService インターフェースや UserServiceImpl 実装などのインターフェースを定義する場合、JDK の動的プロキシが使用されることです。 java.lang.reflect.Proxy クラスのソース コードを見ることができます; もう 1 つは、インターフェイスを自分で定義していないことと、Spring が動的プロキシに CGLIB を使用することです。これはパフォーマンスの良い jar パッケージです。
アダプターパターン
プロキシ モードとアダプタ モードについて説明した後、これらは非常に似ているため、ここで比較することができます。
アダプター モードが行うことは、実装する必要があるインターフェイスがあるものの、既製のオブジェクトでは満足できないため、適応のためにアダプターの層を追加する必要があるということです。
一般に、アダプタ パターンには、デフォルト アダプタ パターン、オブジェクト アダプタ パターン、クラス アダプタ パターンの 3 種類があります。これらのいくつかを急いで区別しないでください。まず例を見てみましょう。
(1) デフォルトのアダプタモード
まず、最も単純なアダプター モードであるデフォルトのアダプター モード (デフォルト アダプター)を見てみましょう。
例として、Appache commons-io パッケージの FileAlterationListener を使用します。このインターフェイスでは、ファイルまたはフォルダーを監視するための多くのメソッドが定義されています。対応する操作が発生すると、対応するメソッドがトリガーされます。
public interface FileAlterationListener {
void onStart(final FileAlterationObserver observer);
void onDirectoryCreate(final File directory);
void onDirectoryChange(final File directory);
void onDirectoryDelete(final File directory);
void onFileCreate(final File file);
void onFileChange(final File file);
void onFileDelete(final File file);
void onStop(final FileAlterationObserver observer);
}
このインターフェイスの大きな問題は、抽象メソッドが多すぎることです。このインターフェイスを使用したい場合は、すべての抽象メソッドを実装する必要があります。フォルダー内のファイル作成イベントとファイル削除イベントを監視したいだけの場合は、 、しかし、それでもすべてのメソッドを実装する必要があるのは、明らかに私たちが望んでいることではありません。
したがって、上記のインターフェイスを実装するために使用される次のアダプタが必要ですが、すべてのメソッドは空のメソッドであるため、代わりに次のクラスを継承する独自のクラスを定義できます。
public class FileAlterationListenerAdaptor implements FileAlterationListener {
public void onStart(final FileAlterationObserver observer) {
}
public void onDirectoryCreate(final File directory) {
}
public void onDirectoryChange(final File directory) {
}
public void onDirectoryDelete(final File directory) {
}
public void onFileCreate(final File file) {
}
public void onFileChange(final File file) {
}
public void onFileDelete(final File file) {
}
public void onStop(final FileAlterationObserver observer) {
}
}
たとえば、次のクラスを定義できます。実装する必要があるのは、実装したいメソッドだけです。
public class FileMonitor extends FileAlterationListenerAdaptor {
public void onFileCreate(final File file) {
// 文件创建
doSomething();
}
public void onFileDelete(final File file) {
// 文件删除
doSomething();
}
}
もちろん、上記はアダプター モードの 1 つにすぎず、最も単純なモードでもあるため、これ以上言う必要はありません。次に「オーソドックス」なアダプターパターンを紹介します。
(2) オブジェクトアダプタモード
「Head First Design Patterns」の例を見てみましょう。ニワトリをアヒルとして使用できるように、ニワトリをアヒルに適応させる方法を確認するために少し変更しました。このインターフェースをダッキングすると、使用する適切な実装クラスがないため、アダプターが必要になります。
public interface Duck {
public void quack(); // 鸭的呱呱叫
public void fly(); // 飞
}
public interface Cock {
public void gobble(); // 鸡的咕咕叫
public void fly(); // 飞
}
public class WildCock implements Cock {
public void gobble() {
System.out.println("咕咕叫");
}
public void fly() {
System.out.println("鸡也会飞哦");
}
}
アヒルのインターフェースには、fly() と quare() という 2 つのメソッドがあります。ニワトリの Cock がアヒルのふりをしたい場合、fly() メソッドは既製ですが、ニワトリはアヒルのように鳴くことはできないので、メソッドはありません。 quack() メソッド。現時点では、以下を適応させる必要があります。
// 毫无疑问,首先,这个适配器肯定需要 implements Duck,这样才能当做鸭来用
public class CockAdapter implements Duck {
Cock cock;
// 构造方法中需要一个鸡的实例,此类就是将这只鸡适配成鸭来用
public CockAdapter(Cock cock) {
this.cock = cock;
}
// 实现鸭的呱呱叫方法
@Override
public void quack() {
// 内部其实是一只鸡的咕咕叫
cock.gobble();
}
@Override
public void fly() {
cock.fly();
}
}
クライアント呼び出しは次のように単純です。
public static void main(String[] args) { // 有一只野鸡 Cock wildCock = new WildCock(); // 成功将野鸡适配成鸭 Duck duck = new CockAdapter(wildCock); ...}
この時点で、アダプター モードが何であるかは誰もが知っています。アヒルが必要だが、ニワトリしかいないというだけのことであり、この時点ではアヒルとして機能するアダプターを定義する必要がありますが、アダプター内のメソッドは依然としてニワトリによって実装されています。
図を使って簡単に説明しましょう。
[外部リンク画像の転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-sX6OO6Ec-1683340130656)(data:image/svg+xml,%3C%3Fxml) version='1.0' エンコーディング='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www. w3.org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅= '1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg translate='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x ='249' y=' 126' 幅='1' 高さ='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
上の図を見ればわかりやすいと思いますので、これ以上の説明は省略します。次に、クラス適応モードがどのように機能するかを見てみましょう。
(3) クラスアダプタモード
早速、写真に直接行きましょう。
[外部リンク画像の転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-JwKJA0vi-1683340130672)(data:image/svg+xml,%3C%3Fxml) version='1.0' エンコーディング='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www. w3.org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅= '1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg translate='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x ='249' y=' 126' 幅='1' 高さ='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
この図を見れば誰でも簡単に理解できると思いますが、アダプターは継承というメソッドを通じて、必要なメソッドのほとんどを自動的に取得します。現時点では、クライアントはより使いやすく、Target t = new SomeAdapter();
直接。
(4) アダプタモードの概要
-
クラス適応とオブジェクト適応の類似点と相違点
1 つは継承を使用し、もう 1 つは合成を使用します。
クラス適応は静的実装に属し、オブジェクト適応は組み合わせの動的実装に属し、オブジェクト適応はもう 1 つのオブジェクトをインスタンス化する必要があります。
一般的に言えば、オブジェクト適応の方がよく使われます。
-
アダプター パターンとプロキシ パターンの類似点と相違点
これら 2 つのモードを比較することは、実際にはオブジェクト アダプター モードとプロキシ モードを比較することと同じであり、コード構造の点では非常に似ており、どちらも実装クラスの特定のインスタンスを必要とします。しかし、それらの目的は異なります。プロキシ モードは元のメソッドを強化することであり、アダプターは「ニワトリをアヒルにパッケージ化し、アヒルとして使用する」ことを提供するために適応させることであり、ニワトリとアヒルは存在します。彼らの間には相続関係はありませんでした。
ブリッジモード
ブリッジ パターンを理解することは、実際にはコードの抽象化と分離を理解することです。
まずブリッジが必要です。これは、提供されたインターフェイス メソッドを定義するインターフェイスです。
public interface DrawAPI { public void draw(int radius, int x, int y);}
次に、一連の実装クラス:
public class RedPen implements DrawAPI {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class GreenPen implements DrawAPI {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class BluePen implements DrawAPI {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
抽象クラスを定義します。このクラスのすべての実装クラスは DrawAPI を使用する必要があります。
public abstract class Shape {
protected DrawAPI drawAPI;
protected Shape(DrawAPI drawAPI) {
this.drawAPI = drawAPI;
}
public abstract void draw();
}
抽象クラスのサブクラスを定義します。
// 圆形
public class Circle extends Shape {
private int radius;
public Circle(int radius, DrawAPI drawAPI) {
super(drawAPI);
this.radius = radius;
}
public void draw() {
drawAPI.draw(radius, 0, 0);
}
}
// 长方形
public class Rectangle extends Shape {
private int x;
private int y;
public Rectangle(int x, int y, DrawAPI drawAPI) {
super(drawAPI);
this.x = x;
this.y = y;
}
public void draw() {
drawAPI.draw(0, x, y);
}
}
最後に、クライアントのデモを見てみましょう。
public static void main(String[] args) {
Shape greenCircle = new Circle(10, new GreenPen());
Shape redRectangle = new Rectangle(4, 8, new RedPen());
greenCircle.draw();
redRectangle.draw();
}
上記の手順は特に明確ではないことがおわかりいただけると思いますが、すべてを 1 つの図に統合しました。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-X5OYsYvf-1683340130676)(data:image/svg+xml,%3C%3Fxml バージョン) ='1.0' エンコーディング= 'UTF-8'%3F%3E%3Csvg 幅='1px' 高さ='1px' viewBox='0 0 1 1' バージョン='1.1' xmlns='http://www.w3 .org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅=' 1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transfer='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x= '249' y=' 126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
今度は、誰もが抽象化がどこにあるのか、そしてそれを分離する方法を知る必要があります。ブリッジ モデルの利点も明らかで、拡張が非常に簡単です。
このセクションでは、ここにある例を引用し、それらを修正します。
デコレータパターン
装飾モードを明確に説明するのは簡単ではありません。Java IOのいくつかのクラスが装飾パターンの代表的な応用例であることは読者の皆さんもご存じかもしれませんが、それらの関係性についてはよくわかっていないかもしれませんし、読んでも忘れてしまっているかもしれません。それの。
まず、簡単な図を見てみましょう。この図を見ると、階層構造が理解できれば十分です。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-CJdRx1yo-1683340130677)(data:image/svg+xml,%3C%3Fxml バージョン) ='1.0' エンコーディング= 'UTF-8'%3F%3E%3Csvg 幅='1px' 高さ='1px' viewBox='0 0 1 1' バージョン='1.1' xmlns='http://www.w3 .org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅=' 1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transfer='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x= '249' y=' 126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
デコレーション モードの開始点について話しましょう。図からわかるように、インターフェイスにComponent
は実際には2 つのConcreteComponentA
実装クラスがありますが、これら 2 つの実装クラスを強化したい場合は、デコレーション モードを使用し、特定の拡張の目的を達成するために実装クラスを装飾するデコレータ。ConcreteComponentB
デコレータについて名前から簡単に説明しましょう。装飾なので細かい機能を追加することが多いですが、気が向いたら細かい機能を複数追加することも可能です。最も単純な方法では、プロキシ モードは機能強化を実現できますが、プロキシが複数の機能を強化することは容易ではありません。複雑になる。
最初にいくつかの簡単な概念を理解します。図から、すべての具象デコレータConcreteDecorator * はすべてコンポーネント内のすべてのインターフェイスを実装しているため、コンポーネントとして使用できることがわかります。これらと Component 実装クラス ConcreteComponent* の違いは、それらが単なる装飾のためのデコレーターであることです。つまり、たとえ見た目は素晴らしくても、特定の実装では装飾のためのスキン層にすぎません。
この文章では、Component と Decorator がさまざまな名詞に混在していることに注意してください。混同しないようにしてください。
例を見てみましょう。まず装飾モードを明確にしてから、Java io での装飾モードのアプリケーションを紹介します。
最近、巷で人気の「ハッピーレモン」ですが、当社ではハッピーレモンのドリンクを紅茶、緑茶、コーヒーの3カテゴリーに分け、その3カテゴリーをベースに金柑などのフレーバーを多数追加しております。レモン紅茶、金柑レモンパール緑茶、マンゴー紅茶、マンゴー緑茶、マンゴーパール紅茶、ほうじパール紅茶、ほうじパールマンゴー緑茶、ココナッツ風味胚芽コーヒー、キャラメルココアコーヒーなど。長いメニューですが、よく見てください 次に、原材料は多くありませんが、組み合わせはたくさんあり、お客様の要望があれば、メニューに載っていない飲み物もたくさん作ることができます。
この例では、紅茶、緑茶、コーヒーが最も基本的な飲み物であり、金柑レモン、マンゴー、パール、ココナッツ、キャラメルなどのその他の飲み物はすべて装飾的なものです。もちろん、開発においては、実際にこれらのクラスをストアのように開発できます: LemonBlackTea、LemonGreenTea、MangoBlackTea、MangoLemonGreenTea... しかし、これは絶対に不可能であることがすぐにわかりました。そのため、おそらくすべてを組み合わせる必要があります。ゲストは紅茶にダブルレモンが必要ですか? レモン3個はどうでしょうか?
くだらない話はやめて、コードに進みましょう。
まず、飲料の抽象基本クラスを定義します。
public abstract class Beverage { // 返回描述 public abstract String getDescription(); // 返回价格 public abstract double cost();}
次に、紅茶、緑茶、コーヒーという 3 つの基本的な飲料実装クラスがあります。
public class BlackTea extends Beverage { public String getDescription() { return "红茶"; } public double cost() { return 10; }}public class GreenTea extends Beverage { public String getDescription() { return "绿茶"; } public double cost() { return 11; }}...// 咖啡省略
デコレータの基本クラスであるシーズニングを定義します。これは Beverage から継承する必要があります。
// 调料public abstract class Condiment extends Beverage {}
次に、デコレータに属するレモンやマンゴーなどの特定の調味料を定義しましょう。これらの調味料が Condiment クラスを継承する必要があることに疑いの余地はありません。
public class Lemon extends Condiment {
private Beverage bevarage;
// 这里很关键,需要传入具体的饮料,如需要传入没有被装饰的红茶或绿茶,
// 当然也可以传入已经装饰好的芒果绿茶,这样可以做芒果柠檬绿茶
public Lemon(Beverage bevarage) {
this.bevarage = bevarage;
}
public String getDescription() {
// 装饰
return bevarage.getDescription() + ", 加柠檬";
}
public double cost() {
// 装饰
return beverage.cost() + 2; // 加柠檬需要 2 元
}
}
public class Mango extends Condiment {
private Beverage bevarage;
public Mango(Beverage bevarage) {
this.bevarage = bevarage;
}
public String getDescription() {
return bevarage.getDescription() + ", 加芒果";
}
public double cost() {
return beverage.cost() + 3; // 加芒果需要 3 元
}
}
...// 给每一种调料都加一个类
クライアント呼び出しを見てください。
public static void main(String[] args) {
// 首先,我们需要一个基础饮料,红茶、绿茶或咖啡
Beverage beverage = new GreenTea();
// 开始装饰
beverage = new Lemon(beverage); // 先加一份柠檬
beverage = new Mongo(beverage); // 再加一份芒果
System.out.println(beverage.getDescription() + " 价格:¥" + beverage.cost());
//"绿茶, 加柠檬, 加芒果 价格:¥16"
}
マンゴー-パール-ダブルレモン-紅茶が必要な場合:
Beverage beverage = new Mongo(new Pearl(new Lemon(new Lemon(new BlackTea()))));
変態なのか?
以下の図を見るとわかりやすいかもしれません。
[外部リンク画像の転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-hO8ODsC6-1683340130678)(data:image/svg+xml,%3C%3Fxml) version='1.0' エンコーディング='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www. w3.org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅= '1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg translate='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x ='249' y=' 126' 幅='1' 高さ='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
この時点で、誰もが装飾モードについてすでに知っているはずです。
次に、Java IO のデコレーション モードについて説明します。以下の図で、InputStream から派生したいくつかのクラスを見てください。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-qL1JnQ7u-1683340130680)(data:image/svg+xml,%3C%3Fxml バージョン) ='1.0' エンコーディング= 'UTF-8'%3F%3E%3Csvg 幅='1px' 高さ='1px' viewBox='0 0 1 1' バージョン='1.1' xmlns='http://www.w3 .org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅=' 1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transfer='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x= '249' y=' 126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
InputStream は入力ストリームを表し、特定の入力ソースはファイル (FileInputStream)、パイプ (PipedInputStream)、配列 (ByteArrayInputStream) などであることがわかります。これらは、上記のミルク ティーの例における紅茶と緑茶に似ています。基本入力ストリームに属します。
FilterInputStreamはデコレーションモードのキーノードを継承しており、その実装クラスは一連のデコレータとなっています。例えば、BufferedInputStreamはバッファリングによる装飾を表現し、入力ストリームにバッファリング機能を持たせます。LineNumberInputStreamは行番号による装飾を表現しています。行番号と DataInputStream の装飾により、入力ストリームから Java の基本型の値に変換できます。
もちろん、Java IO でデコレータを使用する場合、次のようなインターフェイス指向のプログラミングには適していません。
InputStream inputStream = new LineNumberInputStream(new BufferedInputStream(new FileInputStream("")));
そのため、行番号を読み取るメソッドは LineNumberInputStream クラスで定義されているため、InputStream には行番号を読み取る機能がまだありません。
次のように使用する必要があります。
DataInputStream is = new DataInputStream(
new BufferedInputStream(
new FileInputStream("")));
したがって、設計パターンに厳密に準拠した純粋なコードを見つけることは依然として困難です。
ファサードモード
ファサード パターン (外観パターン、ファサード パターンとも呼ばれます) は、slf4j などの多くのソース コードで使用されており、ファサード パターンのアプリケーションとして理解できます。これは単純な設計パターンです。コード内で直接説明しましょう。
まず、インターフェイスを定義します。
public interface Shape {
void draw();
}
いくつかの実装クラスを定義します。
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Circle::draw()");
}
}
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Rectangle::draw()");
}
}
クライアントの呼び出し:
public static void main(String[] args) {
// 画一个圆形
Shape circle = new Circle();
circle.draw();
// 画一个长方形
Shape rectangle = new Rectangle();
rectangle.draw();
}
上記は私たちがよく書くコードです。円を描画する必要がある場合は、最初に円をインスタンス化する必要があります。長方形を描画するには、最初に長方形をインスタンス化してから、対応するdraw() メソッドを呼び出す必要があります。
次に、ファサード モードを使用してクライアントの通話をよりフレンドリーにする方法を見てみましょう。
まずファサードを定義しましょう。
public class ShapeMaker {
private Shape circle;
private Shape rectangle;
private Shape square;
public ShapeMaker() {
circle = new Circle();
rectangle = new Rectangle();
square = new Square();
}
/**
* 下面定义一堆方法,具体应该调用什么方法,由这个门面来决定
*/
public void drawCircle(){
circle.draw();
}
public void drawRectangle(){
rectangle.draw();
}
public void drawSquare(){
square.draw();
}
}
クライアントが現在どのように呼び出しているかを確認します。
public static void main(String[] args) {
ShapeMaker shapeMaker = new ShapeMaker();
// 客户端调用现在更加清晰了
shapeMaker.drawCircle();
shapeMaker.drawRectangle();
shapeMaker.drawSquare();
}
ファサード モードの利点は明白で、クライアントはインスタンス化の際にどの実装クラスを使用するかを意識する必要がなくなり、ファサード クラスが提供するメソッドのメソッド名がそのまま使用されるため、ファサードが提供するメソッドを直接呼び出すことができます。すでにクライアントに対して非常にフレンドリーです。
コンビネーションモード
複合パターンは階層構造でデータを表現するために使用され、個々のオブジェクトと複合オブジェクトへのアクセスを一貫させます。
例を見てみましょう。各従業員は、名前、部門、給与などの属性と、部下の従業員のコレクション (コレクションは空の場合があります) を持ち、部下の従業員は自分自身と同じ構造を持ち、また、名前や部門などの属性を持ち、その下位の従業員コレクションも持ちます。
public class Employee {
private String name;
private String dept;
private int salary;
private List<Employee> subordinates; // 下属
public Employee(String name,String dept, int sal) {
this.name = name;
this.dept = dept;
this.salary = sal;
subordinates = new ArrayList<Employee>();
}
public void add(Employee e) {
subordinates.add(e);
}
public void remove(Employee e) {
subordinates.remove(e);
}
public List<Employee> getSubordinates(){
return subordinates;
}
public String toString(){
return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]");
}
}
通常、そのようなクラスは add(node)、remove(node)、getChildren() メソッドを定義する必要があります。
これは実際には組み合わせモードです。このシンプルなモードについてはあまり紹介しません。読者は私がナンセンスなことを書くのを見るのを嫌うと思います。
フライウェイトモード
英語は Flyweight Pattern です。この単語を誰が最初に訳したのかわかりません。この訳は非常にわかりにくいと思います。無理やり結び付けてみましょう。フライウェイトとは、軽量という意味です。フライウェイトとは、コンポーネントを個別に共有すること、つまり、生成されたオブジェクトを再利用することを意味します。もちろん、このアプローチも軽量です。
オブジェクトを再利用する最も簡単な方法は、HashMap を使用して、新しく生成された各オブジェクトを保存することです。オブジェクトが必要になるたびに、まず HashMap にアクセスしてオブジェクトがあるかどうかを確認し、ない場合は新しいオブジェクトを生成して、このオブジェクトを HashMap に追加します。
この単純なコードについては説明しません。
構造パターンのまとめ
先ほど、プロキシ モード、アダプター モード、ブリッジ モード、デコレーション モード、ファサード モード、コンビネーション モード、およびフライウェイト モードについて説明しました。読者はこれらのモードを個別に説明できますか? これらのモードについて話すとき、頭の中に明確なイメージや処理フローがありますか?
プロキシ モードはメソッドの拡張です。アダプター モードは、チキンをアヒルにパッケージングすることでインターフェイスを適応させるために使用されます。ブリッジ モードは、良好なデカップリングを実現します。デコレーション モードは、名前からわかるように、装飾クラスや強化されたシナリオで言えば、ファサード モードは、クライアントがインスタンス化プロセスを意識する必要がなく、必要なメソッドを呼び出すだけで済むこと、結合モードは階層構造でデータを記述するために使用されること、フライウェイト モードは特定のシナリオ用であることです。作成されたオブジェクトをキャッシュしてパフォーマンスを向上させます。 。
行動モデル
この動作パターンはさまざまなクラス間の相互作用に焦点を当てており、責任を明確に分割することでコードをより明確にしています。
戦略パターン
戦略パターンはよくあるので最前線で紹介されています。比較的シンプルなので、くだらない話はせず、コードを使って話します。
以下に設計されたシナリオでは、図を描く必要があります。オプションの戦略は、赤ペン、緑ペン、または青ペンで描くことです。
まず、戦略インターフェイスを定義します。
public interface Strategy {
public void draw(int radius, int x, int y);
}
次に、いくつかの具体的な戦略を定義します。
public class RedPen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class GreenPen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class BluePen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
戦略を使用したクラス:
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeDraw(int radius, int x, int y){
return strategy.draw(radius, x, y);
}
}
クライアントのデモ:
public static void main(String[] args) {
Context context = new Context(new BluePen()); // 使用绿色笔来画
context.executeDraw(10, 0, 0);
}
誰でもはっきりとわかるように、それを写真に載せます。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-x7c89ttA-1683340130681)(data:image/svg+xml,%3C%3Fxml バージョン) ='1.0' エンコーディング= 'UTF-8'%3F%3E%3Csvg 幅='1px' 高さ='1px' viewBox='0 0 1 1' バージョン='1.1' xmlns='http://www.w3 .org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅=' 1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transfer='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x= '249' y=' 126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
このとき、構造モードのブリッジ モードを思い浮かべますか? これらは実際には非常によく似ています。比較のためにブリッジ モードの写真を撮りましょう。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-ORM33z5f-1683340130682) (data:image/svg+xml,%3C%3Fxml バージョン) ='1.0' エンコーディング= 'UTF-8'%3F%3E%3Csvg 幅='1px' 高さ='1px' viewBox='0 0 1 1' バージョン='1.1' xmlns='http://www.w3 .org/2000/svg ' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg ストローク='なし' ストローク幅=' 1' fill='none ' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transfer='translate(-249.000000, -126.000000)]' fill='%23FFFFFF'%3E%3Crect x= '249' y=' 126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
これらは非常に似ていますが、私に言わせれば、ブリッジ パターンは左側に抽象化のレイヤーを追加しているだけです。ブリッジ モードは結合度が低く、構造がより複雑です。
オブザーバーパターン
オブザーバー モードは私たちにとって非常に簡単です。オブザーバーは関心のあるトピックをサブスクライブし、トピックにデータ変更があるとオブザーバーに通知します。
まず、トピックを定義する必要があります。また、各トピックは、データが変更されたときに各オブザーバーに通知するために使用されるオブザーバー リストへの参照を保持する必要があります。
public class Subject {
private List<Observer> observers = new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
// 数据已变更,通知观察者们
notifyAllObservers();
}
// 注册观察者
public void attach(Observer observer) {
observers.add(observer);
}
// 通知观察者们
public void notifyAllObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
オブザーバー インターフェイスを定義します。
public abstract class Observer {
protected Subject subject;
public abstract void update();
}
実際には、オブザーバー クラスが 1 つしかない場合、インターフェイスを定義する必要はありませんが、一般的なシナリオではオブザーバー モードが使用されるため、イベントが発生したときに複数の異なるクラスが存在することを期待するだけです。対応する情報を処理する必要があります。たとえば、注文変更イベントが成功した場合、テキスト メッセージを送信するクラス、電子メールを送信するクラス、物流情報を処理するクラスに通知されることを期待します。
いくつかの特定のオブザーバー クラスを定義してみましょう。
public class BinaryObserver extends Observer {
// 在构造方法中进行订阅主题
public BinaryObserver(Subject subject) {
this.subject = subject;
// 通常在构造方法中将 this 发布出去的操作一定要小心
this.subject.attach(this);
}
// 该方法由主题类在数据变更的时候进行调用
@Override
public void update() {
String result = Integer.toBinaryString(subject.getState());
System.out.println("订阅的数据发生变化,新的数据处理为二进制值为:" + result);
}
}
public class HexaObserver extends Observer {
public HexaObserver(Subject subject) {
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
String result = Integer.toHexString(subject.getState()).toUpperCase();
System.out.println("订阅的数据发生变化,新的数据处理为十六进制值为:" + result);
}
}
クライアントの使い方も非常に簡単です。
public static void main(String[] args) {
// 先定义一个主题
Subject subject1 = new Subject();
// 定义观察者
new BinaryObserver(subject1);
new HexaObserver(subject1);
// 模拟数据变更,这个时候,观察者们的 update 方法将会被调用
subject.setState(11);
}
出力:
订阅的数据发生变化,新的数据处理为二进制值为:1011
订阅的数据发生变化,新的数据处理为十六进制值为:B
もちろん、jdk も同様のサポートを提供します。詳細については、java.util.Observable および java.util.Observer の 2 つのクラスを参照してください。
実際の制作プロセスでは、オブザーバー モードはメッセージ ミドルウェアで実装されることがよくありますが、スタンドアロン オブザーバー モードを実装する場合、同期および非同期の実装がある Guava の EventBus を使用することを著者は読者に推奨しています。デザインモードであり、それを拡張しません。
また、上記のコードにも多くのバリエーションがありますが、すべてのオブザーバーを格納する場所が必要であり、イベントが発生したときにオブザーバーをトラバースしてコールバック関数を呼び出すというコア部分だけを覚えておく必要があります。
責任連鎖モデル
通常、責任の連鎖では、最初に一方向のリンク リストを確立する必要があります。その後、呼び出し元はヘッド ノードを呼び出すだけでよく、その後は自動的に下に流れていきます。例えば、プロセスの承認が好例で、エンドユーザーがアプリケーションを提出するだけで、アプリケーションの内容情報に応じて自動的に責任連鎖が確立され、転送が開始されます。
ユーザーがイベントに参加すると賞品を受け取ることができるシナリオがありますが、イベントをリリースするには、最初にユーザーが新規ユーザーであるかどうかを確認するなど、多くのルールによる検証が必要です。 、今日の参加者数に制限があるかどうか、イベントの参加者数に制限があるかどうか、えー、ちょっと待ってください。設定されたルールをすべて通過した場合にのみ、ユーザーは賞品を受け取ることができます。
製品にこの要件がある場合、ほとんどの人が最初に考える必要があるのは、List を使用してすべてのルールを保存し、その後 foreach で各ルールを実行することだと思います。しかし、読者の皆さん、心配しないでください。責任連鎖モデルと私たちが説明したモデルの違いを見てみましょう。
まず、プロセス上のノードの基本クラスを定義する必要があります。
public abstract class RuleHandler {
// 后继节点
protected RuleHandler successor;
public abstract void apply(Context context);
public void setSuccessor(RuleHandler successor) {
this.successor = successor;
}
public RuleHandler getSuccessor() {
return successor;
}
}
次に、それぞれの特定のノードを定義する必要があります。
ユーザーが新規ユーザーかどうかを確認します。
public class NewUserRuleHandler extends RuleHandler {
public void apply(Context context) {
if (context.isNewUser()) {
// 如果有后继节点的话,传递下去
if (this.getSuccessor() != null) {
this.getSuccessor().apply(context);
}
} else {
throw new RuntimeException("该活动仅限新用户参与");
}
}
}
ユーザーの地域が参加できるかどうかを確認します。
public class LocationRuleHandler extends RuleHandler {
public void apply(Context context) {
boolean allowed = activityService.isSupportedLocation(context.getLocation);
if (allowed) {
if (this.getSuccessor() != null) {
this.getSuccessor().apply(context);
}
} else {
throw new RuntimeException("非常抱歉,您所在的地区无法参与本次活动");
}
}
}
賞品が受け取られたかどうかを確認します。
public class LimitRuleHandler extends RuleHandler {
public void apply(Context context) {
int remainedTimes = activityService.queryRemainedTimes(context); // 查询剩余奖品
if (remainedTimes > 0) {
if (this.getSuccessor() != null) {
this.getSuccessor().apply(userInfo);
}
} else {
throw new RuntimeException("您来得太晚了,奖品被领完了");
}
}
}
クライアント:
public static void main(String[] args) {
RuleHandler newUserHandler = new NewUserRuleHandler();
RuleHandler locationHandler = new LocationRuleHandler();
RuleHandler limitHandler = new LimitRuleHandler();
// 假设本次活动仅校验地区和奖品数量,不校验新老用户
locationHandler.setSuccessor(limitHandler);
locationHandler.apply(context);
}
コードは実際には非常に単純です。つまり、最初にリンク リストを定義し、次に任意のノードを通過した後、このノードに後続ノードがある場合は、それを渡します。
実行する必要があるルールを保存するためにリストを使用する方法との類似点と相違点については、読者が自分で考えるのに任せます。
テンプレートメソッドパターン
テンプレート メソッド パターンは、継承構造を持つコードで非常に一般的に使用されます。
通常、抽象クラスが存在します。
public abstract class AbstractTemplate {
// 这就是模板方法
public void templateMethod() {
init();
apply(); // 这个是重点
end(); // 可以作为钩子方法
}
protected void init() {
System.out.println("init 抽象层已经实现,子类也可以选择覆写");
}
// 留给子类实现
protected abstract void apply();
protected void end() {
}
}
テンプレート メソッドでは 3 つのメソッドが呼び出されます。そのうちの apply() は抽象メソッドであり、サブクラスはそれを実装する必要があります。実際、テンプレート メソッド内のいくつかの抽象メソッドは完全に無料です。3 つのメソッドをすべて抽象メソッドとして設定することもできます。サブクラスにやらせましょう。つまり、テンプレートメソッドは、第1ステップで何を行うか、第2ステップで何を行うか、第3ステップで何を行うかを定義するだけであり、それをどのように行うかについては、実装されます。サブクラスによって。
実装クラスを作成します。
public class ConcreteTemplate extends AbstractTemplate {
public void apply() {
System.out.println("子类实现抽象方法 apply");
}
public void end() {
System.out.println("我们可以把 method3 当做钩子方法来使用,需要的时候覆写就可以了");
}
}
クライアント通話のデモ:
public static void main(String[] args) {
AbstractTemplate t = new ConcreteTemplate();
// 调用模板方法
t.templateMethod();
}
コードは実際には非常にシンプルで、基本的には見れば理解できます。重要なのは、独自のコードでの使用方法を学ぶことです。
ステートモード
更新: 2017-10-19
くだらない話はしないので、簡単な例について話しましょう。商品在庫センターの最も基本的なニーズの 1 つは、在庫の削減と在庫の補充です。状態モードを使用して書き込む方法を見てみましょう。
重要なのは、コンテキストがどのような種類の操作を実行するかではなく、このコンテキストでどのような操作が実行されるかに焦点を当てることです。
状態インターフェイスを定義します。
public interface State {
public void doAction(Context context);
}
在庫解消のステータスを定義します。
public class DeductState implements State {
public void doAction(Context context) {
System.out.println("商品卖出,准备减库存");
context.setState(this);
//... 执行减库存的具体操作
}
public String toString() {
return "Deduct State";
}
}
補充ステータスを定義します。
public class RevertState implements State {
public void doAction(Context context) {
System.out.println("给此商品补库存");
context.setState(this);
//... 执行加库存的具体操作
}
public String toString() {
return "Revert State";
}
}
Context.setState(this) は前に使用しました。Context クラスを定義する方法を見てみましょう。
public class Context {
private State state;
private String name;
public Context(String name) {
this.name = name;
}
public void setState(State state) {
this.state = state;
}
public void getState() {
return this.state;
}
}
クライアントからの電話を見てみましょう。誰もが明らかになるでしょう。
public static void main(String[] args) {
// 我们需要操作的是 iPhone X
Context context = new Context("iPhone X");
// 看看怎么进行补库存操作
State revertState = new RevertState();
revertState.doAction(context);
// 同样的,减库存操作也非常简单
State deductState = new DeductState();
deductState.doAction(context);
// 如果需要我们可以获取当前的状态
// context.getState().toString();
}
読者は、上記の例で、現在のコンテキストがどのような状態にあるかを気にしない場合、Context が state 属性を維持する必要がなく、コードがはるかに単純になることに気づくかもしれません。
しかし、商品在庫の例は結局のところ一例にすぎず、現在の状況がどのような状態にあるのかを知る必要がある例はまだたくさんあります。
行動パターンのまとめ
行動パターンの部分では、戦略パターン、観察者パターン、責任連鎖パターン、テンプレートメソッドパターン、状態パターンが紹介されており、実際には古典的な行動パターンにはメモパターンやコマンドパターンなども含まれますが、それらの使用シナリオは比較的限定されていますとこの記事はスペースがかなり広いので紹介は省略します。
要約する
デザイン パターンを学習する目的は、コードをよりエレガントにし、保守しやすく、拡張しやすくすることです。今回この記事を書き終えることで、いろいろなデザインパターンを改めて見直すことができ、とてもやりがいがありました。記事の最大の受益者は一般的に著者自身だと思いますが、記事を書くためには知識を集約したり、さまざまな資料を調べたりする必要があり、また、自分が書いたものは最も記憶に残りやすいので、私が読者に与えるものと考えています。