title: 23のデザインパターンまとめ
date: 2022-12-30 16:53:46
tags:
- 設計パターンの
カテゴリ: - デザイン モード
カバー: https://cover.png
機能: false
記事ディレクトリ
- 1.創造的
- 2. 構造
- 3.行動
-
- 3.1 オブザーバー/パブリッシュ-サブスクライブ パターン (オブザーバー デザイン パターン/パブリッシュ-サブスクライブ デザイン パターン)
- 3.2 テンプレートメソッドの設計パターン
- 3.3 戦略設計パターン
- 3.4 責任の連鎖の設計パターン
- 3.5 状態設計パターン
- 3.6 Iterator/Cursor パターン (Iterator Design Pattern/Cursor Design Pattern)
- 3.7 ビジターデザインパターン
- 3.8 Memento/Snapshot(スナップショット)モード(Mementoデザインパターン)
- 3.9 コマンド設計パターン
- 3.10 インタプリタの設計パターン
- 3.11 メディエータの設計パターン
デザイン パターンの詳細については、次の 3 つの記事を参照してください。
- デザインパターンの美しさまとめ(クリエイティブ系) - Fan223's Blog
- デザインパターンの美しさまとめ(構造型) - Fan223's Blog
- デザインパターンの美しさまとめ(行動型) - Fan223's Blog
これは主に上記の 3 つの記事の要約であり、23 の設計パターンの原理、概念、適用シナリオ、およびそれらの類似点と相違点を明らかにし、23 の設計パターンを区別し、全体的な理解を得るために使用されます。実装については、上記の 3 つの記事を参照してください。この章の内容を読む前に、上記の 3 つの記事を読むか、デザイン パターンについてある程度理解していることをお勧めします。また、知識の要約を作成するか、この章の任意のセクションの最後にあるリンクをクリックして移動することもできます。対応する詳細知識の部分に直接
1.創造的
創造的なデザイン パターンは、主に「オブジェクトの作成」の問題を解決します。
1.1 シングルトンの設計パターン
1.1.1 概要と実装
クラスは 1 つのオブジェクト (またはインスタンス) のみを作成できます。このクラスはシングルトン クラスです。このデザイン パターンはシングルトン デザイン パターンと呼ばれ、シングルトン パターンと呼ばれます。
アプリケーション シナリオ:
- リソース アクセス違反の解決
- グローバルにユニークなクラスを表します
実現方法:
1. ハングリー チャイニーズ スタイル: 遅延読み込みをサポートしていません
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {
}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
2. 遅延スタイル: 遅延読み込みがサポートされていますが、getInstance()
この関数の同時実行性が低くなり、パフォーマンスの問題が発生し、同時実行性が高くなり、サポートされていません。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {
}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
3. 二重検出: 遅延読み込みと高い同時実行性の両方をサポート
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {
}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized(IdGenerator.class) {
// 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
4. 静的内部クラス: 二重検出よりも単純な実装方法
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {
}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
5. 列挙: Java列挙型自体の特性を利用して、インスタンス作成のスレッドセーフとインスタンスの一意性を保証
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {
}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
1.1.2 複数のインスタンス
「シングルトン」とは、1 つのクラスで 1 つのオブジェクトしか作成できないことを意味し、「複数のインスタンス」とは、1 つのクラスで複数のオブジェクトを作成できることを意味しますが、その数には制限があります。たとえば、3 つのオブジェクトしか作成できません。
public class BackendServer {
private long serverNo;
private String serverAddress;
private static final int SERVER_COUNT = 3;
private static final Map<Long, BackendServer> serverInstances = new HashMap<>();
static {
serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
}
private BackendServer(long serverNo, String serverAddress) {
this.serverNo = serverNo;
this.serverAddress = serverAddress;
}
public BackendServer getInstance(long serverNo) {
return serverInstances.get(serverNo);
}
public BackendServer getRandomInstance() {
Random r = new Random();
int no = r.nextInt(SERVER_COUNT)+1;
return serverInstances.get(no);
}
}
マルチインスタンス パターンを理解する別の方法があります。同じタイプのオブジェクトは 1 つしか作成できず、異なるタイプの複数のオブジェクトを作成できます。ここで「タイプ」を理解する方法は?
次の例のコードでは、loggerName は上記の「タイプ」であり、同じ loggerName によって取得されるオブジェクト インスタンスは同じであり、異なる loggerName によって取得されるオブジェクト インスタンスは異なります。
public class Logger {
private static final ConcurrentHashMap<String, Logger> instances
= new ConcurrentHashMap<>();
private Logger() {
}
public static Logger getInstance(String loggerName) {
instances.putIfAbsent(loggerName, new Logger());
return instances.get(loggerName);
}
public void log() {
//...
}
}
//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");
このマルチインスタンス モードの理解は、ファクトリ モードと多少似ています。次の点で、ファクトリ パターンとは異なります。
- マルチインスタンス パターンによって作成されたオブジェクトは、すべて同じクラスのオブジェクトです。
- factory パターンは、異なるサブクラスのオブジェクトを作成します
実際、Flyweight モードに多少似ています。また、実は列挙型もマルチインスタンスモードと同等で、型は1つのオブジェクトにしか対応できず、クラスは複数のオブジェクトを作成できる
詳細が見れます:デザインパターンの美しさまとめ(クリエイティブ系)_ファン223さんのブログシングルトンモード
1.2 ファクトリーパターン(ファクトリーデザインパターン)
一般に、ファクトリ パターンは、単純ファクトリ、ファクトリ メソッド、および抽象ファクトリという、さらに 3 つの細分化されたタイプに分けられます。ただし、GoF の「デザイン パターン」では、単純なファクトリ パターンをファクトリ メソッド パターンの特殊なケースと見なしているため、ファクトリ パターンはファクトリ メソッドと抽象ファクトリにのみ分けられます。実は前者の分類方法の方が一般的
1.2.1 シンプルファクトリー
次のように、さまざまなサフィックス名に従って、さまざまなパーサー クラスが作成されます。ここでcreateParser()
メソッドが、新しいパーサーを作成する必要があり、この実装メソッドは単純なファクトリ パターンの最初の実装メソッドと呼ばれます。
public class RuleConfigParserFactory {
public static IRuleConfigParser createParser(String configFormat) {
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(configFormat)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(configFormat)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(configFormat)) {
parser = new YamlRuleConfigParser();
} else if ("properties".equalsIgnoreCase(configFormat)) {
parser = new PropertiesRuleConfigParser();
}
return parser;
}
}
パーサーを再利用できる場合は、メモリとオブジェクトの作成時間を節約するために、次のようにパーサーを事前に作成してキャッシュすることができます。この実装方法は、単純なファクトリ パターンの 2 番目の実装方法と呼ばれます。
public class RuleConfigParserFactory {
private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
cachedParsers.put("properties", new PropertiesRuleConfigParser());
}
public static IRuleConfigParser createParser(String configFormat) {
if (configFormat == null || configFormat.isEmpty()) {
// 或抛出 IllegalArgumentException
return null;
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}
1.2.2 ファクトリーメソッド(ファクトリーメソッド)
単純なファクトリ パターンの最初の実装方法には、一連の if 分岐ロジックがあります. 実際、if 分岐があまりない場合は、コード内に if 分岐があっても問題ありません。
ポリモーフィズムや設計パターンを使用して if 分岐の判断ロジックを置き換えることもできますが、欠点がないわけではなく、コードのスケーラビリティが向上し、オープンとクローズの原則に沿ったものになりますが、数も増加します。クラスと犠牲のコードの読みやすさ。ここで、ポリモーフィズムの考え方に従って、上記のコードをリファクタリングします
public interface IRuleConfigParserFactory {
IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new JsonRuleConfigParser();
}
}
public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new XmlRuleConfigParser();
}
}
public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new YamlRuleConfigParser();
}
}
public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new PropertiesRuleConfigParser();
}
}
これは、ファクトリ メソッド パターンの典型的なコード実装です。このように、パーサーを追加するときは、IRuleConfigParserFactory インターフェースを実装する Factory クラスを追加するだけで済みます。したがって、ファクトリ メソッド パターンは、単純なファクトリ パターンよりも開閉原理に沿っています。
しかし、これらのファクトリ クラスの使用には、次のような大きな問題があります。
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParserFactory parserFactory = null;
if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new JsonRuleConfigParserFactory();
} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new XmlRuleConfigParserFactory();
} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new YamlRuleConfigParserFactory();
} else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new PropertiesRuleConfigParserFactory();
} else {
throw new InvalidRuleConfigException("Rule config file fo support")rmat is not
上記のコード実装から、ファクトリ クラス オブジェクトの作成ロジックはload()
function。これは、単純なファクトリ パターンの最初の実装方法と非常によく似ています。この問題を解決するには?ファクトリ クラスの別の単純なファクトリ、つまりファクトリのファクトリを作成して、ファクトリ クラス オブジェクトを作成できます。
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
if (parserFactory == null) {
throw new InvalidRuleConfigException("Rule config file format is not support");
}
IRuleConfigParser parser = parserFactory.createParser();
String configText = "";
// 从ruleConfigFilePath文件中读取配置文本到configText中
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
// ...解析文件名获取扩展名,比如rule.json,返回json
return "json";
}
}
// 因为工厂类只包含方法,不包含成员变量,完全可以复用,不需要每次都创建新的工厂类对象,
// 所以,简单工厂模式的第二种实现思路更加合适
public class RuleConfigParserFactoryMap {
//工厂的工厂
private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
static {
cachedFactories.put("json", new JsonRuleConfigParserFactory());
cachedFactories.put("xml", new XmlRuleConfigParserFactory());
cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
cachedFactories.put("properties", new PropertiesRuleConfigParserFactory())
}
public static IRuleConfigParserFactory getParserFactory(String type) {
if (type == null || type.isEmpty()) {
return null;
}
IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
return parserFactory;
}
}
1.2.3 ファクトリ メソッド パターン VS シンプル ファクトリ パターン
オブジェクトの作成ロジックが単純な新しいものだけでなく、他のクラス オブジェクトを組み合わせてさまざまな初期化操作を実行するなど、より複雑な場合は、ファクトリ メソッド パターンを使用して、複雑な作成ロジックを複数のファクトリ クラスに分割することをお勧めします。各ファクトリ クラスが複雑になりすぎないようにします。ただし、単純なファクトリ パターンを使用し、すべての作成ロジックをファクトリ クラスに入れると、ファクトリ クラスが非常に複雑になります。
さらに、一部のシナリオでは、オブジェクトが再利用可能でない場合、ファクトリ クラスは毎回異なるオブジェクトを返す必要があります。単純なファクトリ パターンを使用して実装する場合、if 分岐ロジックを含む最初の実装メソッドのみを選択できます。煩わしい if-else 分岐ロジックを避けたい場合は、現時点ではファクトリ メソッド パターンを使用することをお勧めします。
1.2.4 抽象ファクトリー
上記の単純なファクトリとファクトリ メソッドでは、クラスの分類は 1 つだけです。ただし、上記のパーサーの例のように、クラスが 2 つの分類方法を持つ場合、構成ファイルの形式または解析されるオブジェクト (Rule ルール構成またはシステム システム構成) に従って分類できる場合は、以下に該当します。パーサー クラス
针对规则配置的解析器: 基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser
针对系统配置的解析器: 基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser
ファクトリメソッドを使って実装を続けると、パーサーごとにファクトリクラスを書く必要があります。つまり、8つのファクトリクラスを書く必要があります。将来、ビジネス構成用のパーサー (IBizConfigParser など) を追加する必要がある場合は、それに応じてさらに 4 つのファクトリ クラスを追加する必要があります。クラスが多すぎると、システムの保守が難しくなる可能性もあります。この問題を解決するには?
抽象工場は、この非常に特殊なシナリオのために生まれました。パーサー オブジェクトを 1 つだけ作成する代わりに、異なるタイプ (IRuleConfigParser、ISystemConfigParser など) の複数のオブジェクトの作成を担当する単一のファクトリを持つことができます。これにより、ファクトリ クラスの数を効果的に減らすことができます。
public interface IConfigParserFactory {
IRuleConfigParser createRuleParser();
ISystemConfigParser createSystemParser();
// 此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new JsonRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new JsonSystemConfigParser();
}
}
public class XmlConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new XmlRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new XmlSystemConfigParser();
}
}
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码
詳細が見れる:デザインパターンの美しさまとめ(創作記事)_ファン223さんのブログファクトリーパターン
1.3 Builder/Builder/Generator パターン (Builder デザインパターン)
通常の開発では、オブジェクトを作成する最も一般的な方法は、 new キーワードを使用してクラスのコンストラクターを呼び出して完了することです。しかし、どのような状況ではこの方法は適用できず、ビルダー モードを使用してオブジェクトを作成する必要がありますか?
まず、オブジェクトの無効な状態とは何かを見てみましょう. 次のように、長方形のクラスが定義されています。これは、最初に作成されてから設定され、最初の設定の前にオブジェクトが無効な状態になります。
Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid
この無効な状態の存在を回避するには、コンストラクターを使用してすべてのメンバー変数を一度に初期化する必要があります。コンストラクターのパラメーターが多すぎると、コードの可読性と使いやすさが低下します。コンストラクターを使用すると、パラメーターの順序を間違えたり、間違ったパラメーター値を渡したりして、非常に隠れたバグが発生しやすくなります. このとき、ビルダーモードの使用を検討し、最初にビルダーの変数を設定してから、オブジェクトが常に有効な状態になるように、オブジェクトをタイムリーに作成します。
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
//...省略getter方法...
// 将Builder类设计成了ResourcePoolConfig的内部类。
// 也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
public static class Builder {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
public Builder setName(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
this.name = name;
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("...");
}
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("...");
}
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();
詳細が見られる:デザインパターンの美しさまとめ(創作記事)
1.3.1 ファクトリーパターンとの違いは?
ファクトリ パターンは、異なるが関連するタイプのオブジェクト (同じ親クラスまたはインターフェイスを継承するサブクラスのグループ) を作成するために使用され、指定されたパラメーターによって、作成するオブジェクトのタイプが決定されます。ビルダーモードは、さまざまなオプションのパラメーターを設定することにより、複雑なオブジェクトのタイプを作成するために使用され、さまざまなオブジェクトを作成するために「カスタマイズ」されます
たとえば、顧客が食べ物を注文するためにレストランに足を踏み入れた場合、工場モデルを使用して、ユーザーのさまざまな選択に応じて、ピザ、ハンバーガー、サラダなどのさまざまな食べ物を作ります。ピザの場合、ユーザーはチーズ、トマト、チーズなどのさまざまな材料をカスタマイズできます.ビルダーモードを使用して、ユーザーが選択したさまざまな材料に応じてピザを作成します.
1.4 プロトタイプの設計パターン
オブジェクトの作成コストが比較的高いが、同じクラスの異なるオブジェクト間で違いがほとんどない場合 (ほとんどのフィールドは同じ)、この場合、既存のオブジェクトのコピー (またはコピー) を使用できます。 (プロトタイプ)作成時間を節約するという目的を達成するために、新しいオブジェクトを作成する方法。プロトタイプに基づいてオブジェクトを作成するこの方法は、プロトタイプ デザイン パターン、または略してプロトタイプ パターンと呼ばれます。
コンセプトは難解なものではなく、コンセプトを通してデザインパターンの原理と使い方を大まかに理解する必要があります 詳細は「デザインパターンの美しさまとめ(クリエイティブ系)_ファン223のブログ プロトタイプモード」を参照
2. 構造
構造設計パターンは、主に「クラスまたはオブジェクトの組み合わせまたはアセンブリ」の問題を解決します。
2.1 プロキシ設計パターン
元のクラス (またはプロキシ クラスと呼ばれる) のコードを変更せずに、プロキシ クラスを導入して元のクラスに関数を追加します。次の例では、インターフェイス IUserController のlogin()
メソッド、ログイン ロジックが処理されます。
public interface IUserController {
UserVo login(String telephone, String password);
}
public class UserController implements IUserController {
//...省略其他属性和方法...
public UserVo login(String telephone, String password) {
// ... 省略login逻辑...
//...返回UserVo数据...
}
}
アクセス時間、処理時間など、インターフェースが要求する生データを収集するなど、メソッドを変更せずに追加機能を拡張したい場合、プロキシモードが役立ちます。
次のように、プロキシ クラス UserControllerProxy と元のクラス UserController は、同じインターフェイス IUserController を実装します。UserController クラスは、ビジネス機能のみを担当します。プロキシ クラス UserControllerProxy は、ビジネス コードの実行の前後に他のロジック コードを付加する役割を担い、委任によって元のクラスを呼び出してビジネス コードを実行します。
public class UserControllerProxy implements IUserController {
private MetricsCollector metricsCollector;
private UserController userController;
public UserControllerProxy(UserController userController) {
this.userController = userController;
this.metricsCollector = new MetricsCollector();
}
@Override
public UserVo login(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
// 委托
UserVo userVo = userController.login(telephone, password);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
}
// 因为原始类和代理类实现相同的接口,是基于接口而非实现编程
// 将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userController = new UserControllerProxy(new UserController())
元のクラスがインターフェイスを定義しておらず、元のクラス コードが当社によって開発および保守されていない場合 (たとえば、サード パーティのクラス ライブラリから取得されたもの)、元のクラスを直接変更してそのインターフェイスを再定義することはできません。 . この種の外部クラスの拡張には、継承という方法が一般的に採用されています。次のように、プロキシ クラスに元のクラスを継承させてから、追加の関数を拡張します。
public class UserControllerProxy extends UserController {
private MetricsCollector metricsCollector;
public UserControllerProxy() {
this.metricsCollector = new MetricsCollector();
}
public UserVo login(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
UserVo userVo = super.login(telephone, password);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
}
// UserControllerProxy使用举例
UserController userController = new UserControllerProxy();
アプリケーション シナリオ:
- 業務システムの非機能要件の策定
- RPC で適用
- キャッシュに適用
詳細が見れます:デザインパターンの美しさまとめ(構造型)_ファン223さんのブログプロキシモード
2.2 ブリッジ/ブリッジモード(ブリッジデザインパターン)
GoF の「デザイン パターン」本では、ブリッジ パターンは次のように定義されています。
抽象化を実装から分離して、2 つが独立して変更できるようにする
抽象化と実装を分離して、それらが独立して変更できるようにする
定義における「抽象」「実装」「分離」の 3 つの概念を理解することが、ブリッジ モードを理解するための鍵となります。
- 抽象化は、複数のエンティティに存在する共通の概念接続として理解できます。これは、一部の情報を無視し、異なるエンティティを同じエンティティとして扱うことを意味します
- 実装、つまり抽象化によって与えられる特定の実装であり、さまざまな実装方法が存在する可能性があります
- デカップリング、いわゆるカップリングは、2 つのエンティティの動作間の強い関係です。そして、それらの強い関連性を取り除くことは、カップリングまたはデカップリングの解放です. ここで、デカップリングとは、抽象化と実装の間の結合を切り離すこと、またはそれらの間の強い関連性を弱い関連性に変えることを指します。2 つのロール間の継承関係を集約関係に変更することは、それらの間の強い関連付けを弱い関連付けに変更することです。つまり、抽象化と実装の間の継承関係の代わりに、組み合わせ/集約関係を使用します。
詳しくは「デザインパターンの美しさまとめ(構造型)_ファン223さんのブログブリッジモード編」を参照してください。
2.3 デコレーターのデザインパターン
デコレータモードはプロキシモードに似ていますが、プロキシモードではプロキシクラスが元のクラスとは関係のない機能を追加し、デコレータモードではデコレータクラスが元のクラスに関連する拡張機能を追加します
// 代理模式的代码结构(下面的接口也可以替换成抽象类
public interface IA {
void f();
}
public class A impelements IA {
public void f() {
//...
}
}
public class AProxy impements IA {
private IA a;
public AProxy(IA a) {
this.a = a;
}
public void f() {
// 新添加的代理逻辑
a.f();
// 新添加的代理逻辑
}
}
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A impelements IA {
public void f() {
//...
}
}
public class ADecorator impements IA {
private IA a;
public ADecorator(IA a) {
this.a = a;
}
public void f() {
// 功能增强代码
a.f();
// 功能增强代码
}
}
複数の装飾も可能です。
class Father {
public void run() {
System.out.println("Father run");
}
}
class Son extends Father{
public void run() {
System.out.println("Son run");
}
}
class ChildDecorator extends Father {
protected Father father;
public ChildDecorator(Father father) {
this.father = father;
}
public void run() {
father.run();
System.out.println("ChildDecorator run");
}
}
class Child1 extends ChildDecorator{
public Child1(Father father) {
super(father);
}
public void run() {
father.run();
System.out.println("Child1 run");
}
}
class Child2 extends ChildDecorator {
public Child2(Father father) {
super(father);
}
public void run() {
father.run();
System.out.println("Child2 run");
}
}
public static void main(String[] args) {
Father son = new Son();
Father child1 = new Child1(son);
Child2 child2 = new Child2(child1);
child2.run();
}
詳細が見れる:デザインパターンの美しさまとめ(構造記事)_ファン223さんのブログデコレーターモード編
2.4 アダプタの設計パターン
このモードはその名のとおり適応のために使用され、互換性のないインターフェイスを互換性のあるインターフェイスに変換し、互換性のないインターフェイスのために連携できなかったクラスが連携できるようにします。
アダプター パターンには、クラス アダプターとオブジェクト アダプターの 2 つの実装があります。このうち、クラス アダプタは継承関係を使用して実装され、オブジェクト アダプタは構成関係を使用して実装されます。
1.クラスアダプター
// 类适配器: 基于继承
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() {
//...
}
public void fb() {
//...
}
public void fc() {
//...
}
}
public class Adaptor extends Adaptee implements ITarget {
public void f1() {
super.fa();
}
public void f2() {
//...重新实现f2()...
}
// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
2.オブジェクトアダプター
// 对象适配器:基于组合
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() {
//...
}
public void fb() {
//...
}
public void fc() {
//...
}
}
public class Adaptor implements ITarget {
private Adaptee adaptee;
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void f1() {
adaptee.fa(); //委托给Adaptee
}
public void f2() {
//...重新实现f2()...
}
public void fc() {
adaptee.fc();
}
}
この 2 つの実装方法について、実際の開発では、どちらを使用するかをどのように選択するのでしょうか。判断基準は主に 2 つあります。1 つは Adaptee インターフェイスの数、もう 1 つは Adaptee と ITarget の適合度です。
Adaptee インターフェースがあまりない場合は、どちらの実装でも問題ありません。Adaptee インターフェースが多く、Adaptee と ITarget のインターフェース定義がほとんど同じ場合は、クラス アダプターを使用することをお勧めします。アダプターは親クラスの Adaptee のインターフェースを再利用し、アダプターのコード量はオブジェクト アダプタの実装よりも少ない。多くの Adaptee インターフェイスがあり、ほとんどの Adaptee インターフェイスと ITarget インターフェイスの定義が異なる場合は、オブジェクト アダプターを使用することをお勧めします。これは、構成構造が継承よりも柔軟であるためです。
アプリケーション シナリオ:
- 欠陥のあるインターフェース設計をカプセル化
- 複数のクラスのインターフェース設計を統一する
- 依存する外部システムを置き換える
- 旧バージョンのインターフェースに対応
- さまざまな形式のデータに適応する
詳細が見れます:デザインパターンの美しさまとめ(構造体型)_ファン223さんのブログアダプターパターン編
2.5 プロキシ、ブリッジ、デコレータ、アダプタの 4 つの設計パターンの違い
プロキシ、ブリッジ、デコレータ、アダプタ、これら 4 つのパターンは、より一般的に使用される構造設計パターンです。それらのコード構造は非常に似ています。一般的に言えば、それらはすべて Wrapper モードと呼ぶことができます。つまり、元のクラスは Wrapper クラスによって 2 回カプセル化されます。
コード構造は似ていますが、これら 4 つの設計パターンの意図はまったく異なります。つまり、解決する問題とアプリケーション シナリオが異なります。これが主な違いです。
- プロキシモード:プロキシモードは元のクラスのインターフェースを変更せずに元のクラスのプロキシクラスを定義します. 主な目的はアクセスを制御することであり, 機能を拡張することではありません. これがデコレータモードとの最大の違いです.
- ブリッジ モード:ブリッジ モードの目的は、インターフェース部分を実装部分から分離することです。これにより、それらをより簡単かつ比較的独立して変更できるようになります。
- デコレータ モード:デコレータ モードは、元のクラスのインターフェイスを変更せずに元のクラスの機能を拡張し、複数のデコレータのネストをサポートします
- アダプター パターン:アダプター パターンは、後付けの修正戦略です。アダプターは元のクラスとは異なるインターフェイスを提供しますが、プロキシ モードとデコレーター モードは元のクラスと同じインターフェイスを提供します。
2.6 ファサードデザインパターン
GoF の本「デザイン パターン」では、ファサード パターンは次のように定義されています。
サブシステム内の一連のインターフェースに統一されたインターフェースを提供します. Facade パターンは、サブシステムを使いやすくする高レベルのインターフェースを定義します
.
概念は非常に単純で、a、b、c、および d の 4 つのインターフェースを提供するシステム A があるとします。システム B が特定のビジネス機能を完了すると、システム A のインターフェース a、b、および d を呼び出す必要があります。ファサード モードを使用して、システム B が直接使用できるように a、b、および d インターフェースの呼び出しをラップするファサード インターフェース x を提供します。
アプリケーション シナリオ:
- 使いやすさの問題を解決する: ファサード モードを使用して、システムの基盤となる実装をカプセル化し、システムの複雑さを隠し、使いやすい高レベル インターフェイスのセットを提供できます。
- 複数のインターフェイス呼び出しを 1 つのファサード インターフェイス呼び出しに置き換えることで、パフォーマンスの問題を解決し、ネットワーク通信コストを削減し、アプリ クライアントの応答速度を向上させます。
- 分散トランザクションの問題を解決する
詳細が見れます:デザインパターンの美しさまとめ(構造型)_ファン223さんのブログファサードモード
2.7 複合設計パターン
合成モードは、オブジェクト指向設計における「合成関係(合成によって2つのクラスを組み立てる)」とはまったく異なります。ここでいう「コンビネーションモード」は、主に木構造のデータを処理するために使用されます。ここでの「データ」は、オブジェクト コレクションのセットとして簡単に理解できます。
GoF の本「Design Patterns」では、構成パターンは次のように定義されています。
オブジェクトをツリー構造に構成して、部分全体の階層を表現します. コンポジットにより、クライアントは個々のオブジェクトとオブジェクトの構成を均一に扱うことができます
. コンポジションにより、クライアント (多くのデザイン パターン ブックでは、「クライアント」はコードのユーザーを指します) は、個々のオブジェクトと複合オブジェクトの処理ロジックを統合できます。
たとえば、会社の組織構造には、部門と従業員という 2 つのデータ型が含まれています。その中で、部門には、サブ部門と従業員を含めることができます.これは、ツリーデータ構造として表すことができるネストされた構造です.この時点で、組み合わせモードを使用して設計および実装できます.
この例を複合パターンの定義と比較してください: 「一連のオブジェクト (従業員と部門) をツリー構造に編成して、「部分全体」階層 (部門とサブ部門の入れ子構造) を表します。合成モードでは、次のことが可能です。クライアントは、単一オブジェクト (従業員) と複合オブジェクト (部門) の処理ロジック (再帰トラバーサル) を統合します。」
詳細が見れます:デザインパターンの美しさまとめ(構造記事)_ファン223さんのブログ組み合わせパターン
2.8 フライ級デザインパターン
いわゆる「フライングユアン」は、その名の通り共用ユニットです。flyweight パターンの目的は、flyweight オブジェクトが不変オブジェクトである場合、オブジェクトを再利用してメモリを節約することです。
具体的には、システム内に多数の反復オブジェクトがある場合、これらの反復オブジェクトが不変オブジェクトである場合、flyweight パターンを使用してオブジェクトを flyweight として設計し、複数の場所に対して 1 つのインスタンスのみをメモリ内に保持できます。これにより、メモリ内のオブジェクトの数を減らし、メモリを節約できます。実際には、同じオブジェクトをフライウェイトとして設計できるだけでなく、類似オブジェクトについては、これらのオブジェクトの同じ部分 (フィールド) を抽出してフライウェイトとして設計することで、多数の類似オブジェクトがこれらのフライウェイトを参照できるようにすることができます。
定義の「不変オブジェクト」とは、コンストラクターによって初期化されると、その状態 (オブジェクトのメンバー変数またはプロパティ) が再度変更されないことを意味します。したがって、不変オブジェクトは、内部状態を変更するset()
メソッド。フライウェイトが不変オブジェクトである必要がある理由は、フライウェイトが複数のコードで共有および使用されるためです。これにより、1 つのコードがフライウェイトを変更して、それを使用する他のコードに影響を与えるのを防ぐことができます。
概念は実際には非常に単純です。つまり、繰り返されるオブジェクトまたは同様のオブジェクトの繰り返される部分を再利用します。
詳細が見れる:デザインパターンの美しさまとめ(構造型)_ファン223のブログ
2.8.1 フライウェイト モード vs シングルトン、キャッシュ、オブジェクト プール
1. フライウェイトモードとシングルトンの違い
シングルトン モードでは、クラスは 1 つのオブジェクトしか作成できませんが、Flyweight モードでは、クラスは複数のオブジェクトを作成でき、各オブジェクトは複数のコード参照によって共有されます。実際、Flyweight パターンは、前述のシングルトンのバリアントに多少似ています: 複数のインスタンス
しかし、2 つの設計パターンを区別するには、コードの実装だけを見るのではなく、設計の意図、つまり解決すべき問題に注目する必要があります。Flyweight モードと複数のインスタンスには、コード実装の観点からは多くの類似点がありますが、設計意図の観点からは完全に異なります。フライウェイト モードはオブジェクトを再利用してメモリを節約するために使用され、マルチインスタンス モードはオブジェクトの数を制限するために使用されます。
2.フライウェイトモードとキャッシュの違い
Flyweight パターンの実装では、作成されたオブジェクトはファクトリ クラスを通じて「キャッシュ」されます。ここでの「キャッシュ」とは、実際には「ストレージ」を意味し、通常の「データベース キャッシュ」、「CPU キャッシュ」、「MemCache キャッシュ」とは異なります。私たちが普段話題にしているキャッシュは、主にアクセス効率を改善するためのものであり、再利用するためのものではありません
3. Flyweight モードとオブジェクト プールの違い
オブジェクト プール、接続プール (データベース接続プールなど)、スレッド プールなども再利用されますが、Flyweight モードとの違いは何ですか?
多くの人は、接続プールとスレッド プールについてはよく知っているかもしれませんが、オブジェクト プールについてはよく知らないので、ここでオブジェクト プールについて簡単に説明します。C++ のようなプログラミング言語では、メモリ管理はプログラマの責任です。頻繁なオブジェクトの作成と解放によるメモリの断片化を避けるために、ここで説明したオブジェクト プールである連続メモリ空間を事前に申請できます。オブジェクトが作成されるたびに、アイドル状態のオブジェクトが使用のためにオブジェクト プールから直接取り出されます。オブジェクトが使用された後は、直接解放されるのではなく、その後の再利用のためにオブジェクト プールに戻されます。
オブジェクトプール、接続プール、スレッドプール、Flyweight モードはすべて再利用のためのものですが、「再利用」という言葉を注意深く選ぶと、オブジェクトプール、接続プール、スレッドプールなどのプーリング技術は「再利用」と「再利用」です。 Flyweight モードでは、実際には異なる概念です
プーリング テクノロジの「再利用」は「再利用」と理解できます。主な目的は時間を節約することです (データベース プールから接続を再作成せずに取得するなど)。各オブジェクト、接続、スレッドは常に複数の場所で使用されるのではなく、1 人のユーザーによって排他的に使用され、使用が完了するとプールに戻され、他のユーザーによって再利用されます。Flyweight モードでの「再利用」は、「共有使用」として理解できます。これは、ライフ サイクル全体ですべてのユーザーが共有するものであり、主な目的はスペースを節約することです。
3.行動
ビヘイビア デザイン パターンは、主に「クラスまたはオブジェクト間の相互作用」の問題を解決します。
3.1 オブザーバー/パブリッシュ-サブスクライブ パターン (オブザーバー デザイン パターン/パブリッシュ-サブスクライブ デザイン パターン)
GoF の「デザイン パターン」ブックでは、次のように定義されています。
オブジェクト間に 1 対多の依存関係を定義して、1 つのオブジェクトの状態が変化したときに、そのすべての依存関係が自動的に通知および更新されるようにします。
一般に、依存するオブジェクトはオブザーバブル (Observable) と呼ばれ、依存するオブジェクトはオブザーバー (Observer) と呼ばれます。ただし、実際のプロジェクト開発では、これら 2 つのオブジェクトの名前は比較的柔軟で、Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener など、さまざまな名前があります。どのように呼んでも、アプリケーション シナリオが上記の定義に準拠している限り、オブザーバー モードと見なすことができます。
実際には、オブザーバーモードは比較的抽象化されたモードです.さまざまなアプリケーションシナリオと要件に応じて、完全に異なる実装方法があります.これは最も古典的な実装方法です.これは、このモードについて話すときでもあります.与えられた最も一般的な実装方法多くの本や資料によって。具体的なコードは次のとおりです。
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(Message message);
}
public interface Observer {
void update(Message message);
}
public class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<Observer>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(Message message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
public class ConcreteObserverOne implements Observer {
@Override
public void update(Message message) {
//TODO: 获取消息通知,执行自己的逻辑...
System.out.println("ConcreteObserverOne is notified.");
}
}
public class ConcreteObserverTwo implements Observer {
@Override
public void update(Message message) {
//TODO: 获取消息通知,执行自己的逻辑...
System.out.println("ConcreteObserverTwo is notified.");
}
}
public class Demo {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
subject.registerObserver(new ConcreteObserverOne());
subject.registerObserver(new ConcreteObserverTwo());
subject.notifyObservers(new Message());
}
}
上記のコードは、オブザーバーモードの「テンプレートコード」と見なされ、一般的な設計アイデアのみを反映できます。実際のソフトウェア開発では、上記のテンプレート コードをコピーする必要はありません。オブザーバー モードの実装方法はさまざまです. 関数とクラスの名前は、さまざまなビジネス シナリオに応じて大きく異なります. たとえば、登録機能はアタッチと呼ばれることもあり、削除機能はデタッチと呼ばれることもあります. しかし、刻々と変化するものは変わらず、デザインのアイデアはほとんど同じです
Observer パターンの核となる概念は、実際にはその定義にあります。詳細が見れる:デザインパターンの美しさまとめ(行動記事)_ファン223のブログオブザーバーモード
3.2 テンプレートメソッドの設計パターン
GoF の「デザイン パターン」ブックでは、次のように定義されています。
操作でアルゴリズムのスケルトンを定義し、いくつかのステップをサブクラスに委ねます. テンプレート メソッドを使用すると、サブクラスは、アルゴリズムの構造を変更することなく、アルゴリズムの特定のステップを再定義できます. サブクラスで実装
. テンプレート メソッド パターンにより、サブクラスは、アルゴリズムの全体的な構造を変更することなく、アルゴリズムの特定のステップを再定義できます。
ここでの「アルゴリズム」とは、広義の「ビジネスロジック」と理解できるものであり、データ構造やアルゴリズムにおける「アルゴリズム」を具体的に指すものではありません。ここでのアルゴリズムの骨格が「テンプレート」であり、アルゴリズムの骨格を含むメソッドが「テンプレートメソッド」であり、テンプレートメソッドパターンの名前の由来でもあります。コード例は次のとおりです。
public abstract class AbstractClass {
public final void templateMethod() {
//...
method1();
//...
method2();
//...
}
protected abstract void method1();
protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
public class ConcreteClass2 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
AbstractClass demo = ConcreteClass1();
demo.templateMethod();
- 機能 1: 再利用
テンプレート パターンは、アルゴリズムの不変プロセスを親クラスのテンプレート メソッドtemplateMethod()
に、可変部分をサブクラス ContreteClass1 および ContreteClass2method1()
に実装を任せます。method2()
すべてのサブクラスは、親クラスのテンプレート メソッドによって定義されたプロセス コードを再利用できます。 - 機能 2: 拡張
ここでいう拡張とは、コードの拡張性ではなく、フレームワークの拡張性を指します。これは、前述の制御の反転に多少似ています。この役割に基づいて、テンプレート モードはフレームワークの開発によく使用され、フレームワークのユーザーはフレームワークのソース コードを変更することなく、フレームワークの機能をカスタマイズできます。
Java Servlet を使用したことがある場合は、HttpServlet クラスを継承してから書き換えるというのdoGet()
がdoPost()
典型的なテンプレート パターンであり、フレームワークの拡張は次のとおりです。サーブレットの実行プロセスをカプセル化し、具体的な実装のために変数doGet()
とdoPost()
パーツを継承されたサブクラスに任せます
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
resp.getWriter().write("Hello World.");
}
}
3.2.1 テンプレート パターン VS コールバック
アプリケーション シナリオの観点からは、同期コールバックはテンプレート モードとほぼ同じです。それらはすべて大規模なアルゴリズム スケルトンにあり、特定のステップを自由に置き換えて、コードの再利用と拡張の目的を達成します。非同期コールバックはテンプレート モードとはかなり異なり、オブザーバー モードに似ています。
コード実装の観点からは、コールバック パターンとテンプレート パターンはまったく異なります。コールバックは構成関係に基づいて実装されます.オブジェクトを別のオブジェクトに渡すことはオブジェクト間の関係です.テンプレートモードは継承関係に基づいて実装されます.サブクラスはクラス間の関係である親クラスの抽象メソッドをオーバーライドします. .
前述のように、構成は継承よりも優れています。実際、ここにも例外はありません。コードの実装に関しては、コールバックはテンプレート モードよりも柔軟であり、主に次の点に反映されています。
- 単一継承のみをサポートする Java のような言語では、テンプレート パターンに基づいて作成されたサブクラスは親クラスを継承し、継承する機能がなくなります。
- コールバックは匿名クラスを使用して、事前にクラスを定義しなくてもコールバック オブジェクトを作成できますが、テンプレート パターンは実装ごとに異なるサブクラスを定義します。
- 複数のテンプレート メソッドがクラスで定義され、各メソッドに対応する抽象メソッドがある場合、テンプレート メソッドの 1 つだけが使用されている場合でも、サブクラスはすべての抽象メソッドを実装する必要があります。コールバックはより柔軟です。使用するテンプレート メソッドにコールバック オブジェクトを挿入するだけで済みます。
コールバックなどの詳細な知識は、デザイン パターンの美しさのまとめ (動作タイプ)_Fan 223 のブログテンプレート モードで見つけることができます。
3.3 戦略設計パターン
GoF の本「デザイン パターン」では、次のように定義されています。
アルゴリズムのファミリーを定義し、それぞれをカプセル化し、それらを交換可能にします. 戦略により、アルゴリズムは、それを使用するクライアントとは独立して変更できます. アルゴリズムのファミリーを定義し、各アルゴリズムを個別にカプセル化して、相互に置き換えられるようにします
. 戦略パターンは、アルゴリズムの変更を、それらを使用するクライアントとは無関係に行うことができます (ここでのクライアントとは、アルゴリズムを使用するコードを指します)。
ファクトリ パターンはオブジェクトの作成と使用を分離することであり、オブザーバー パターンはオブザーバーと被監視対象を分離することです。戦略パターンは 2 つに似ており、分離する役割も果たしますが、戦略の定義、作成、使用の 3 つの部分を分離します。
3.3.1 ポリシー定義
戦略クラスの定義は比較的単純で、戦略インターフェースと、このインターフェースを実装する戦略クラスのグループが含まれます。すべての戦略クラスは同じインターフェイスを実装するため、クライアント コードは、実装プログラミングではなく、インターフェイスに基づいてさまざまな戦略を柔軟に置き換えることができます。サンプルコードは次のとおりです。
public interface Strategy {
void algorithmInterface();
}
public class ConcreteStrategyA implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
public class ConcreteStrategyB implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
一般的に言えば、ポリシー クラスがステートレスで、メンバー変数を含まず、純粋なアルゴリズムの実装である場合、そのようなポリシー オブジェクトを共有して使用することができ、getStrategy()
呼び出さ. このような状況に鑑み、上記のファクトリ クラスの実装を使用して、事前に各ポリシー オブジェクトを作成し、それをファクトリ クラスにキャッシュし、使用時に直接返すことができます。
逆に、ポリシー クラスがステートフルである場合、ビジネス シナリオのニーズに応じて、ファクトリ メソッドから毎回、キャッシュされた共有可能なポリシー オブジェクトの代わりに新しく作成されたポリシー オブジェクトが取得されることが望まれます。ポリシー ファクトリ クラスを実装するには
public class StrategyFactory {
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
if (type.equals("A")) {
return new ConcreteStrategyA();
} else if (type.equals("B")) {
return new ConcreteStrategyB();
}
return null;
}
}
3.3.2 ポリシーの作成
ストラテジー モードには一連のストラテジーが含まれるため、それらを使用する場合は、通常、タイプ (タイプ) を使用して作成するストラテジーを決定します。作成ロジックをカプセル化するには、作成の詳細をクライアント コードから隠す必要があります。タイプに基づいて戦略を作成するロジックを抽出して、ファクトリ クラスに配置できます。サンプルコードは次のとおりです。
public class StrategyFactory {
private static final Map<String, Strategy> strategies = new HashMap<>();
static {
strategies.put("A", new ConcreteStrategyA());
strategies.put("B", new ConcreteStrategyB());
}
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
return strategies.get(type);
}
}
3.3.3 ポリシーの使用
戦略パターンには一連のオプションの戦略が含まれていますが、通常、クライアント コードはどの戦略を使用するかをどのように決定するのでしょうか? 最も一般的なのは、実行時に使用する戦略を動的に決定することです。これは、戦略パターンの最も典型的なアプリケーション シナリオでもあります。ここで言う「ランタイム ダイナミクス」とは、事前にどの戦略が使用されるか分からず、プログラムの実行中に構成、ユーザー入力、計算結果などの不確実な要因に応じて、どの戦略を使用するかを動的に決定することを指します。
// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory
public class UserCache {
private Map<String, User> cacheData = new HashMap<>();
private EvictionStrategy eviction;
public UserCache(EvictionStrategy eviction) {
this.eviction = eviction;
}
//...
}
// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
public static void main(String[] args) throws Exception {
EvictionStrategy evictionStrategy = null;
Properties props = new Properties();
props.load(new FileInputStream("./config.properties"));
String type = props.getProperty("eviction_type");
evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
public static void main(String[] args) {
//...
EvictionStrategy evictionStrategy = new LruEvictionStrategy();
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
上記のコードから、「非ランタイムの動的決定」、つまり 2 つ目のアプリケーションでの使用方法では、ストラテジー モードを利用できないこともわかります。このアプリケーション シナリオでは、戦略パターンは実際には「オブジェクト指向のポリモーフィズム」または「プログラミングの原則を実装するのではなく、インターフェイスに基づく」に退化します。
詳細が見れる:デザインパターンの美しさまとめ(動作タイプ)_ファン223のブログ攻略パターン
3.4 責任の連鎖の設計パターン
GoF の「デザイン パターン」では、次のように定義されています。
リクエストを処理する機会を複数のオブジェクトに与えることで、リクエストの送信者をその受信者に結合することを避けます. 受信オブジェクトをチェーンし、オブジェクトが処理するまでチェーンに沿ってリクエストを渡します. これらの受信オブジェクトをチェーンにストリングし、チェーン内の受信オブジェクトのいずれかが処理できるようになるまで、チェーンに沿ってリクエストを渡します
Chain of Responsibility パターンでは、複数のプロセッサ (つまり、定義で言及した「受信オブジェクト」) が同じ要求を順番に処理します。要求は、最初にプロセッサ A によって処理され、次にプロセッサ B に渡され、プロセッサ B によって処理された後、プロセッサ C に渡され、チェーンを形成します。チェーン内の各プロセッサは独自の処理責任を負うため、責任チェーン モードと呼ばれます。
責任連鎖モードを実現する方法はたくさんありますが、ここでは、より一般的に使用される方法を 2 つ紹介します。
1.連結リスト
public abstract class Handler {
protected Handler successor = null;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
public final void handle() {
boolean handled = doHandle();
if (successor != null && !handled) {
successor.handle();
}
}
protected abstract boolean doHandle();
}
public class HandlerA extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerChain {
private Handler head = null;
private Handler tail = null;
public void addHandler(Handler handler) {
handler.setSuccessor(null);
if (head == null) {
head = handler;
tail = handler;
return;
}
tail.setSuccessor(handler);
tail = handler;
}
public void handle() {
if (head != null) {
head.handle();
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
2. HandlerChain クラスは、リンクされたリストの代わりに配列を使用してすべてのプロセッサを保存しhandle()
、 HandlerChain の関数で各プロセッサのhandle()
関数を順番に呼び出す必要があります。
public interface IHandler {
boolean handle();
}
public class HandlerA implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerChain {
private List<IHandler> handlers = new ArrayList<>();
public void addHandler(IHandler handler) {
this.handlers.add(handler);
}
public void handle() {
for (IHandler handler : handlers) {
boolean handled = handler.handle();
if (handled) {
break;
}
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
GoF によって与えられた定義では、プロセッサ チェーン内のプロセッサが要求を処理できる場合、要求を渡し続けることはありません。実際、責任連鎖モードには別のバリエーションがあります。つまり、要求はすべてのプロセッサによって処理され、途中で終了することはありません。このバリアントにも 2 つの実装があります。リンクされたリストを使用してプロセッサを格納し、配列を使用してプロセッサを格納します。上記の 2 つの実装と同様に、わずかに変更するだけで済みます。
実は、インターセプターとフィルターチェーンを比較すれば分かるのですが、詳しくは「デザインパターンの美しさまとめ(動作タイプ)_ファン223さんのブログ責任連鎖モード編」をご覧ください。
3.5 状態設計パターン
実際のソフトウェア開発では、状態パターンはあまり一般的に使用されませんが、使用できるシナリオで大きな役割を果たすことができます。この観点からは、前述のコンビネーションモードに少し似ています。ステート パターンは一般的にステート マシンの実装に使用され、ステート マシンはゲームやワークフロー エンジンなどのシステム開発でよく使用されます。ただし、ステート マシンを実現する方法は多数ありますが、ステート モード以外に、分岐論理方式とルックアップ テーブル方式がよく使用されます。
有限状態機械、英訳は状態機械と呼ばれる FSM と略される有限状態機械です。ステート マシンには、状態 (State)、イベント (Event)、アクション (Action) の 3 つのコンポーネントがあります。このうち、イベントは遷移条件(Transition Condition)とも呼ばれます。イベントは、状態の遷移とアクションの実行をトリガーします。ただし、アクションは必須ではなく、何もアクションを実行せずに状態を転送するだけでもかまいません。
例えば「スーパーマリオ」、ゲーム内では、マリオはスモールマリオ(Small Mario)、スーパーマリオ(Super Mario)、ファイヤーマリオ(Fire Mario)、ケープマリオ(Cape Mario)など、さまざまな形に変身できます。等々。異なるゲーム プロットの下で、各フォームは互いに変形し、それに応じてポイントが増減します。例えば、初期形態はリトルマリオで、きのこを食べるとスーパーマリオになって100点アップ
実際、マリオのフォームの変換はステート マシンです。このうち、マリオのさまざまな形態がステートマシンの「状態」、ゲームのプロット (きのこを食べるなど) がステートマシンの「イベント」、ポイントの加減がステートマシンの「アクション」です。ステートマシン。たとえば、きのこを食べるイベントは、リトル マリオからスーパー マリオへの状態遷移をトリガーし、アクションの実行をトリガーします (100 ポイントを追加)。
コンセプトは難しくなく、詳細が見える:デザインパターンの美しさまとめ(動作タイプ)_ファン223のブログ状態モード編
3.6 Iterator/Cursor パターン (Iterator Design Pattern/Cursor Design Pattern)
イテレータ パターンは、コレクション オブジェクトをトラバースするために使用されます。ここでいう「コレクション オブジェクト」は「コンテナー」や「集合オブジェクト」とも呼ばれ、実際には配列、リンク リスト、ツリー、グラフ、ジャンプ リストなどのオブジェクトのグループを含むオブジェクトです。イテレータ モードは、コレクション オブジェクトのトラバーサル操作をコレクション クラスから分離し、それをイテレータ クラスに入れ、2 つの責任をさらに単一にします。
反復子はコンテナーをトラバースするために使用されるため、通常、完全な反復子パターンには、コンテナーとコンテナー反復子の 2 つの部分が含まれます。実装ではなくインターフェイスに基づくプログラミングの目的を達成するために、コンテナにはコンテナ インターフェイスとコンテナ実装クラスが含まれ、イテレータにはイテレータ インターフェイスとイテレータ実装クラスが含まれます。簡単なクラス図は次のとおりです。
概念も非常にシンプルで, Javaの Iterator イテレータと比較して理解できます. 他のほとんどのプログラミング言語もコンテナをトラバースするためのイテレータクラスを提供しています. 通常の開発では, 直接使用できます. Scratch. イテレータ
詳細が見れます:デザインパターンの美しさまとめ(ビヘイビアタイプ)_ファン223さんのブログイテレータパターン
3.7 ビジターデザインパターン
GoF の「デザイン パターン」ブックでは、次のように定義されています。
実行時に 1 つまたは複数の操作をオブジェクトのセットに適用できるようにし、操作をオブジェクト構造から分離します。1 つまたは複数の操作を
オブジェクトのセットに適用し、操作とオブジェクト自体を分離できます。
ビジターモードはここが分かりにくいので、直接詳しい説明に行くことをお勧めします:デザインパターンの美しさまとめ(行動記事)_Fan223さんのブログビジターモード
これには、二重ディスパッチの問題、Double Dispatch が含まれます。ダブルディスパッチがあるので、対応するシングルディスパッチがあります。
- いわゆるシングル ディスパッチは、オブジェクトのランタイム タイプに応じて決定されるオブジェクトが実行されるメソッドを参照し、メソッド パラメータのコンパイル時のタイプに応じて決定されるオブジェクトのどのメソッドが実行されます。
- いわゆるダブル ディスパッチは、実行するオブジェクトのメソッドを指し、オブジェクトのランタイム タイプに従って決定されます。オブジェクトを実行するメソッドは、メソッド パラメータのランタイム タイプに従って決定されます。
ダブル ディスパッチをサポートする言語では、ビジター モードは必要ありません. ビジター モードは、主に、シングル ディスパッチがポリモーフィックである場合に、メソッド パラメータがオーバーロードされるポリモーフィズムの問題を解決するためのものです.
3.8 Memento/Snapshot(スナップショット)モード(Mementoデザインパターン)
GoF の本「Design Patterns」では、Memento パターンは次のように定義されています。
カプセル化に違反することなく、後で復元できるように、オブジェクトの内部状態をキャプチャして外部化します。
このモードの定義は主に 2 つの部分を表しています。1 つは後で復元するためにコピーを保存することで、もう 1 つはカプセル化の原則に違反することなくオブジェクトをバックアップおよび復元することです。
コンセプトも非常にわかりやすく、通常のバックアップと比較して理解できます。2 つのアプリケーション シナリオは非常に似ており、どちらも損失防止、回復、失効などのシナリオに適用されます。それらの違いは、メモ パターンはコードの設計と実装に重点を置いており、バックアップはアーキテクチャ設計または製品設計に重点を置いていることです。
詳細はこちら:デザインパターンの美しさまとめ(行動型)_ファン223のブログメモパターン
3.9 コマンド設計パターン
GoF の「デザイン パターン」ブックでは、次のように定義されています。
コマンド パターンは、リクエストをオブジェクトとしてカプセル化するため、別のリクエスト、キューまたはログ リクエストで
他のオブジェクトをパラメータ化し、取り消し可能な操作をサポートできます。コマンド)、ロギング、失効など(追加の制御)機能
実装のコーディングに関して言えば、コマンド モードで使用される主要な実装方法は、関数をオブジェクトにカプセル化することです。C 言語は関数ポインターをサポートしており、関数を変数として渡すことができます。ただし、ほとんどのプログラミング言語では、関数を他の関数に引数として渡すことも、変数に割り当てることもできません。コマンド パターンを使用すると、関数をオブジェクトにカプセル化できます。具体的には、この関数を含むクラスを設計し、オブジェクトをインスタンス化して渡し、関数をオブジェクトのように使用できるようにします。実装の観点からは、前述のコールバックに似ています
関数がオブジェクトにカプセル化された後、オブジェクトは簡単に制御および実行できるように格納できます。したがって、コマンド モードの主な機能とアプリケーション シナリオは、非同期、遅延、コマンドのキューイング、コマンドの取り消しとやり直し、コマンドの保存、コマンドのログの記録など、コマンドの実行を制御することです。 can do ユニークな役割を果たす場所
詳細が見れます:デザインパターンの美しさまとめ(ビヘイビア型)_ファン223さんのブログコマンドモード編
3.9.1 コマンドモード VS ストラテジーモード
上記の定義を見ると、コマンド モードはストラテジー モードやファクトリー モードと非常に似ていると感じるかもしれませんが、それらの違いは何ですか? それだけでなく、以前のモデルの多くは非常に似ているように感じます。
実際、各設計パターンは 2 つの部分で構成する必要があります: 最初の部分はアプリケーション シナリオ、つまりこのパターンがどのような問題を解決できるか、2 つ目の部分はソリューション、つまり設計のアイデアと具体的なコードの実装です。このパターンの。ただし、コードの実装をパターンに含める必要はありません。ソリューションの部分だけ、またはコードの実装にさえ注目すると、ほとんどのパターンが似ているような錯覚に陥ります。
実際、設計パターン間の主な違いは、アプリケーション シナリオである設計意図にあります。デザインのアイデアやコードの実装を単純に見ると、ストラテジー パターンやファクトリー パターンなど、いくつかのパターンは非常によく似ています。
ストラテジー パターンについて説明したとき、ストラテジー パターンにはストラテジーの定義、作成、使用の 3 つの部分があると述べましたが、コード構造から見ると、ファクトリ パターンに非常に似ています。それらの違いは、戦略パターンが「戦略」または「アルゴリズム」の特定のアプリケーションシナリオに焦点を当てていることです。これは、ランタイム状態に従って一連の戦略から異なる戦略を選択する問題を解決するために使用されますが、ファクトリパターンはカプセル化されたオブジェクトの作成プロセスに焦点を当てています. ここでのオブジェクトは、ビジネスシナリオに限定されず、戦略である可能性がありますが、他のものである可能性もあります. 設計意図から、これら 2 つのモードはまったく別のものです。
コマンドモードとストラテジーモードの違いを見てみましょう。コマンドの実行ロジックも作戦とみなせると思うかもしれませんが、作戦パターンですか?実はこの2つには微妙な違いがあります
戦略パターンでは、異なる戦略には同じ目的と異なる実装があり、互いに置き換えることができます。たとえば、BubbleSort と SelectionSort はどちらも並べ替えの実装に使用されますが、一方はバブル ソート アルゴリズムを使用して実装され、もう一方は選択ソート アルゴリズムを使用して実装されます。コマンドモードでは、異なるコマンドは異なる目的を持ち、異なる処理ロジックに対応し、相互に交換することはできません。
3.10 インタプリタの設計パターン
GoF の本「デザイン パターン」では、次のように定義されています。
インタープリター パターンは、言語の文法表現を定義するために使用され、この文法を処理するインタープリターを提供します
。
「言語」「文法」「通訳」など、通常の開発ではほとんど触れない概念がたくさんあります。実際、ここでの「言語」は、通常の中国語、英語、日本語、フランス語、およびその他の言語を指すだけではありません。広い意味で、情報を運ぶことのできる担い手であれば「言語」と呼ぶことができ、例えば古代の結び目、点字、ダム言語、モールス信号など。
「言語」が表現する情報を理解するためには、対応する文法規則を定義する必要があります。このように、書き手は文法規則に従って「文」を書き(専門用語は「表現」である必要があります)、読み手は文法規則に従って「文」を読むことができ、情報を正しく伝えることができます。通訳者モードは、実際には文法規則に従って「文」を解釈するために使用される通訳者です
足し算、引き算、掛け算、割り算の新しい計算「言語」を定義するとします。文法規則は次のとおりです。
- 演算子には足し算、引き算、掛け算、割り算のみが含まれ、優先順位の概念はありません
- 式(つまり、上記の「文」)では、最初に数字を書き、次にスペースで区切って演算子を書きます
- 順序に従って、2つの数字と演算子の計算結果を取り出し、結果を数字の先頭に戻し、残りの数字が1つになるまで上記のプロセスを繰り返し、この数字が式の最終的な計算結果です。
たとえば、「8 3 2 4 - + *」などの式は、上記の文法規則に従って処理され、数字の「8 3」と「-」演算子が取り出され、5 が計算されるため、式は次のようになります。 「5 2 4+*」。次に、「 5 2 」と「 + 」演算子を取り出して 7 を計算すると、式は「 7 4 * 」になります。最後に、「7 4」と「*」演算子を取り出すと、最終結果は 28 になります。この結果を処理するのはインタプリタです
詳細を見ると、概念を理解するのは難しくないはずです:デザイン パターンの美しさのまとめ (動作タイプ)_ファン 223 のブログインタープリター モードの部分
3.11 メディエータの設計パターン
GoF の本「Design Patterns」では、次のように定義されています。
メディエーター パターンは、一連のオブジェクト間の相互作用をカプセル化する個別の (メディエーター) オブジェクトを定義し、オブジェクトは相互に直接相互作用するのではなく、それらの相互作用をメディエーター オブジェクトにデリゲートします。オブジェクト間の直接的な相互作用を避けるために、一連のオブジェクト間の相互作用を中間オブジェクトに委譲する
「コードを分離する方法」について話すとき、方法の 1 つは、中間層を導入することです。実際、仲介パターンの設計思想は中間層の設計思想と非常によく似ており、仲介の中間層を導入することで、オブジェクトのグループ間の相互作用関係 (または依存関係) が多対から変換されます。 -多 (ネットワーク関係) から 1 対多 (スター関係)。本来、オブジェクトは n 個のオブジェクトと対話する必要がありますが、現在は中間オブジェクトと対話するだけでよいため、オブジェクト間の対話が最小限に抑えられ、コードの複雑さが軽減され、コードの可読性と保守性が向上します。
オブジェクトの相互作用関係の比較図は、次のように描画されます。このうち、右側の相互作用図は、左側の相互作用関係を中間モードで最適化した結果であり、右側の相互作用関係がより明確で簡潔であることが直感的にわかります。
中間モデルに関して言えば、もっと古典的な例があります。それは、航空管制です。
航空機が互いに干渉せずに飛行するためには、各航空機が常に他の航空機の位置を把握している必要があり、そのためには他の航空機と常に通信する必要があります。航空機通信によって形成される通信ネットワークは非常に複雑になります。このとき、「タワー」などの仲介者を導入することで、各航空機はタワーとのみ通信し、自身の位置をタワーに送信し、タワーは各航空機のルート スケジューリングを担当します。これにより、通信ネットワークが大幅に簡素化されます
詳細が見れます:デザインパターンの美しさまとめ(動作タイプ)_ファン223さんのブログ仲介モード編
3.11.1 中間パターン VS オブザーバー パターン
オブザーバー モードについて説明したとき、オブザーバー モードを実装するには多くの方法があると述べました。従来の実装方法では、オブザーバーと被監視対象を完全に切り離すことはできませんが、オブザーバーを被監視対象に登録する必要があり、被監視対象の状態を更新するには、オブザーバーのupdate()
メソッド しかし, クロスプロセスの実装では, メッセージキューを使用して完全な分離を実現できます. オブザーバーと被観察者の両方がメッセージキューと対話するだけで済みます. オブザーバーは被観察者の存在をまったく知りません.観察者は全く知らない 観察者の存在を意識する
仲介モードは、オブジェクト間の相互作用を分離することでもあり、すべての参加者は仲介者とのみ相互作用します。オブザーバー モードのメッセージ キューは、中間モードの「仲介者」に多少似ており、オブザーバー モードのオブザーバーと被監視者は、仲介モードの「参加者」に多少似ています。ここで質問があります: 中間モードとオブザーバー モードの違いは何ですか? メディエーション パターンの使用を選択するのはいつですか? オブザーバー モードの使用を選択するのはいつですか?
オブザーバー モードでは、参加者はオブザーバーと被観察者の両方になることができますが、ほとんどの場合、相互作用関係は一方通行であることが多く、参加者はオブザーバーまたはオブザーバーのいずれかになります。 . つまり、オブザーバー モードのアプリケーション シナリオでは、参加者間の相互作用関係がより組織化されます。
中間モデルは正反対です。参加者間のやり取りが複雑で、メンテナンス コストが高い場合にのみ、仲介モデルを検討する必要があります。結局のところ、中間パターンの適用は特定の副作用をもたらし、大規模で複雑な神のクラスを生成する可能性があります。さらに、参加者の状態が変化した場合、他の参加者によって実行される操作には特定のシーケンス要件があります. このとき、中間モデルは、中間クラスを使用して、異なる参加者のメソッドを呼び出すことにより、順次順序を実装できます. コントロール,オブザーバーモードはそのような注文要件を達成できません