blog

デザインパターン・シリーズ - SOLID設計原則

S、O、L、I、Dは、オブジェクトの設計とコーディングの重要な原則と向き合うことを目的としています。これらの原則が使われると、プログラムの拡張や保守が容易になります。 上の単一責任原則の英語の説明から...

Dec 1, 2020 · 11 min. read
シェア

S、O、L、I、Dの設定は、オブジェクト設計とコーディングの重要な原則に直面します。これらの原則を使用すると、プログラムの拡張や保守が容易になります。

SRP 単一責任原則 単一責任原則
OCP オープン・クローズの原理 LSP
LSP リスコフ置換原理 リヒターの置換原理(数学)
インターネットせつぞくぎょうしゃ インターフェース分離の原則 インターフェース分離原理
DIP 依存関係の逆転 背理法

単一責任の原則

単一責任の原則とは何ですか?

A class or module should have a single reponsibility

上記の単一責任原則の英語の説明から、単一責任原則の原則は比較的単純であることがわかります。つまり、クラスやモジュールは1つの機能だけに責任を持つべきだということです。

つまり、クラスやモジュールを設計するときには、すべてを包含するような大きなクラスやモジュールの設計は避け、小さくて機能的に独立した、きめの細かいクラスやモジュールを設計することです。たとえば、あるクラスやモジュールが

2つ以上のバラバラの関数が含まれている場合は、可能な限り分割してください。

例を挙げましょう。eコマースシステムでは、注文とユーザーという2つのモジュールがあります。もし、注文とユーザーという2つのモジュールのうち、1つのモジュールに変更が加えられた場合、その変更とテストは、次のようになります。

この段階はすべて、修正されていない別のモジュールの調査とテストを含み、労力と時間を浪費します。もし、それらを2つの別々のモジュールに入れ、片方だけを修正すれば、もう片方のモジュールを修正する必要があるのは、それが関係している場合だけです。

修正とテストが行われたかどうかを検討するのは、それが行われたときだけです。

責任は重ければ重いほどいいのですか?

単一責任の原則を追求する場合、クラスやモジュールをできるだけ細かく分けた方がいいのでしょうか?答えはノーです。単一責任の原則では、同じ関数をクラスやモジュールに入れることで、異なる関数間の結合を避け、コードのまとまりを良くします。しかし、分割が細かすぎると逆効果になります。このような状況の例を示します。

@Slf4j
public class Token {
 private String secret="token-secret";
 private Long expiration=L;
 /**
 * トークンの有効期限を生成する
 */
 private Date generateExpirationDate() {
 return new Date(System.currentTimeMillis() + expiration * 1000);
 }
 /**
 * JWTの生成責任者によるとtoken
 */
 private String generateToken(Map<String, Object> claims) {
 return Jwts.builder()
 .setClaims(claims)
 .setExpiration(generateExpirationDate())
 .signWith(SignatureAlgorithm.HS512, secret)
 .compact();
 }
 /**
 * トークンからログインユーザー名を取得する
 */
 public String getUserNameFromToken(String token) {
 String username;
 try {
 Claims claims = getClaimsFromToken(token);
 username = claims.getSubject();
 } catch (Exception e) {
 username = null;
 }
 return username;
 }
 /**
 * トークンからJWTの読み込みを取得する
 */
 public Claims getClaimsFromToken(String token) {
 Claims claims = null;
 try {
 claims = Jwts.parser()
 .setSigningKey(secret)
 .parseClaimsJws(token)
 .getBody();
 } catch (Exception e) {
 log.info("JWTフォーマット検証の失敗:{}", token);
 }
 return claims;
 }

1つはトークンを生成する ***generateToken*** で、もう1つはトークンから情報を解析する ***getClaimsFromToken*** と ***getUserNameFromToken*** です。単一責任の原則に従う場合、この 2 つの関数を ***GenerateToken*** と ***AnalysisToken*** クラスに含める必要があります。これは異なる関数の責任を分離するものですが、変数 ***secret*** を変更するには、両方のクラスで変更する必要があります。これはコードのまとまりを悪くするだけです。

開閉原理

software entities should be open for extension , but closed for modification

簡単に言うと、機能を追加するときは、既存のコードを修正するのではなく、既存のコードの上にコードを拡張すべきだということです。

configMapはコンフィギュレーション・キャッシュ、annotationParseはアノテーション・パーサー、xmlParseはxmlパーサー、ConfigTypeはパーシングの列挙型です。

public class Configuration {
 private Map<String,Object> configMap;
 private AnnotationParse annotationParse;
 private XmlParse xmlParse;
 public Configuration(Map<String, Object> configMap, AnnotationParse annotationParse, XmlParse xmlParse) {
 this.configMap = configMap;
 this.annotationParse = annotationParse;
 this.xmlParse = xmlParse;
 }
 public void parse(String type){
 if (ConfigType.Annotation.toString().equals(type)){
 annotationParse.parse(configMap);
 }else {
 xmlParse.parse(configMap);
 }
 }
 public static void main(String[] args) {
 Configuration configuration=new Configuration(new HashMap<String, Object>(),new AnnotationParse(),new XmlParse());
 configuration.parse("Xml");
 }
}
public enum ConfigType {
 Annotation,Xml
}

上記のコードは比較的シンプルで、異なるタイプの設定情報を解析し、configMapキャッシュに入れることに基づいています。今、私たちは、元のベースにJSON形式の設定関数を追加したいと思います。主な変更点は、3つの場所を追加し、1:ConfigType列挙型でJsonの種類を増やすには、2:変更するConfigurationのパースメソッド、3:どのようにJsonParseは、データの形式をパースします!

public class Configuration {
 private Map<String,Object> configMap;
 private AnnotationParse annotationParse;
 private XmlParse xmlParse;
 private JsonParse jsonParse;// 
 public Configuration(Map<String, Object> configMap, AnnotationParse annotationParse, XmlParse xmlParse,JsonParse jsonParse) 	{
 this.configMap = configMap;
 this.annotationParse = annotationParse;
 this.xmlParse = xmlParse;
 this.jsonParse=jsonParse;
 }
 public void parse(String type){
 if (ConfigType.Annotation.toString().equals(type)){
 annotationParse.parse(configMap);
 }else if (ConfigType.Json.toString().equals(type)){// 
 jsonParse.parse(configMap);
 } else {
 xmlParse.parse(configMap);
 }
 }
 public static void main(String[] args) {
 Configuration configuration=new Configuration(new HashMap<String, Object>(),new AnnotationParse(),new XmlParse(),new JsonParse());
 configuration.parse("Json");
 }
}
public enum ConfigType {
 Annotation,Xml,Json
}

追加されたJSON形式のConfiguration関数Configuration関数のコードは非常にシンプルですが、まだ多くの問題があります。一方では、Configurationが修正され、Configurationを呼び出すメソッドを修正しなければならず、他方では、Configurationのparseが修正され、parseに関連する関数全体をテストしなければならず、テストの量が増えます。

上記のコードは、Json形式の新しい設定関数を修正したものです。では、この関数を開閉の原則に従って実装するにはどうすればよいでしょうか。

まず、コードをリファクタリングして、解析メソッドを定義するインターフェースを追加します。

public interface ParseHandler {
 void parse(Map<String,Object> configMap,String type);
}

そして、他の解析クラスがインターフェイス

public class AnnotationParse implements ParseHandler {
 
 public void parse(Map<String, Object> configMap,String type) {
 if (ConfigType.Annotation.toString().equals(type)){
 
 }
 }
}

最後にConfigurationをリファクタリングします。

public class Configuration {
 private Map<String,Object> configMap;
 private List<ParseHandler> parseHandlers=new ArrayList<ParseHandler>();
 public Configuration(Map<String, Object> configMap) {
 this.configMap = configMap;
 }
 public void addparseHandlers(ParseHandler parseHandler){//登録インターフェース
 this.parseHandlers.add(parseHandler);
 }
 public void parse(String type){
 for (ParseHandler parseHandler : parseHandlers) {
 parseHandler.parse(configMap,type);
 }
 }
 public static void main(String[] args) {
 Configuration configuration=new Configuration(new HashMap<String, Object>());
 configuration.addparseHandlers(new AnnotationParse());
 configuration.addparseHandlers(new XmlParse());//登録固有の分析
 ConfigType[] values = ConfigType.values();
 for (ConfigType value : values) {
 configuration.parse(value.toString());
 }
 }
}

上記は、コードをリファクタリングした後、アノテーションとXmlの設定パース関数であり、 parseHandlersは、リストのParseHandlerインターフェイスコレクションであり、 addparseHandlersメソッドを介して登録され、対応するパースメソッドの実行を解析します。以下は、Json形式の設定関数の完了です。

まず、コンフィギュレーションの列挙にJsonタイプを追加します。

public enum ConfigType {
 Annotation,Xml,Json
}

次に、JsonParseクラスを追加して、ParseHandlerインターフェイスを実装します。

public class JsonParse implements ParseHandler{
 public void parse(Map<String,Object> configMap,String type){
 if (ConfigType.Json.toString().equals(type)){
 
 }
 }
}

最後に、JsonParseをConfigurationに登録します。

public class Configuration {
	//...コードは変更していない。
 public static void main(String[] args) {
 Configuration configuration=new Configuration(new HashMap<String, Object>());
 configuration.addparseHandlers(new AnnotationParse());
 configuration.addparseHandlers(new XmlParse());//登録固有の分析
 ConfigType[] values = ConfigType.values();
 for (ConfigType value : values) {
 configuration.parse(value.toString());
 }
 }
}

上記のリファクタリングされたコードからわかるように、新しいJsonコンフィギュレーション関数を追加することは、Handlerインターフェイスを実装し、それを登録することだけを必要とし、他の2つのタイプに関するコードを変更しないので、まったく影響しません。

上記のコードも修正されているので、元のコードを修正したことになるのでしょうか?答えはノーです。インターフェースの実装と登録メソッドの実装は、どちらもコードを拡張するものであり、元の機能に影響を与えることはありません。あなたが構成の解析の他のタイプを追加したい場合は、私はちょうど上記の2つの手順を完了する必要があり、他の追加の影響はありません。

リヒターの置換原理(数学)

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it

リヒター置換原則の定義からは、それが親子オブジェクト間の設計上の制約であることは明らかではありません。リヒター置換原則の核心は、規約に従って設計することです。親クラスが関数やメソッドの規約を指定した場合、サブクラスはそれぞれ関数やメソッドのロジックを実装することはできますが、親クラスが指定した規約を変更することはできません。規約には、入力パラメータ、戻り値、例外処理などがあります。オブジェクトのポリモーフィズムに直面することは、リヒターの置換原理の一種です。

インターフェース分離原理

Clients should not be forced to depend upon interfaces that they do not use

インターフェイス分離の原則は、その名前からも分かるように、インターフェイス設計の原則であり、インターフェイスは単にオブジェクトのインターフェイスだけを指すのではなく、単一または複数の関数やメソッド、httpリクエストインターフェイスなども含みます。

インターフェイスを設計する場合、インターフェイスはそれ自身のイベントのみを行い、インターフェイスの呼び出し元には論理的なイベントを追加しないようにします。単一責任の原則はモジュール、クラス、インターフェースの設計原則を扱いますが、インターフェースの分離原則はインターフェースの設計により重点を置きます。インターフェースの利用者がインターフェースの機能の一部しか呼び出さない場合、単一責任の原則は満たされません。

まず、更新のホットロード規約を設定する ***HotUpdate*** インターフェイスを作成します。

public interface HotUpdate {
 void update();
}

次に、JsonParseとXmlParseにHotUpdateインターフェイスを実装させます。

public class XmlParse implements ParseHandler,HotUpdate{
 public void parse(Map<String,Object> configMap,String type){
 if (ConfigType.Xml.toString().equals(type)){
 System.out.println("xml");
 }
 }
 public void update() {
 // 
 }
}

最後に、時間制限のあるタスクを使って、ホットローディングを実行します。

public class Configuration {
 private Map<String,Object> configMap;
 private List<ParseHandler> parseHandlers=new ArrayList<ParseHandler>();
 private List<HotUpdate> hotUpdates=new ArrayList<HotUpdate>();
 public Configuration(Map<String, Object> configMap) {
 this.configMap = configMap;
 }
 public void addparseHandlers(ParseHandler parseHandler){//登録インターフェース
 this.parseHandlers.add(parseHandler);
 }
 public void addhotUpdates(HotUpdate hotUpdate){
 this.hotUpdates.add(hotUpdate);
 }
 //時限タスクのホットローディング
 public void ScheduledUpdater(){
 for (HotUpdate hotUpdate : hotUpdates) {
 hotUpdate.update();
 }
 }
 public void parse(String type){
 for (ParseHandler parseHandler : parseHandlers) {
 parseHandler.parse(configMap,type);
 }
 }
 public static void main(String[] args) {
 Configuration configuration=new Configuration(new HashMap<String, Object>());
 configuration.addparseHandlers(new AnnotationParse());
 configuration.addparseHandlers(new XmlParse());
 configuration.addparseHandlers(new JsonParse());// JsonParse
 ConfigType[] values = ConfigType.values();
 for (ConfigType value : values) {
 configuration.parse(value.toString());
 }
 }
}

ホットロード関数のxmlとJsonのパースメソッドの追加に基づいて、上記のパース設定ファイルの関数では、この関数は、それを達成するために行く場合?まず第一に、ホットロードインターフェイスUpdateを定義するので、xmlParseクラスとJsonParseクラスは、それぞれ、インターフェイスを実装するために、それぞれのホットロードロジックを実現します。第二に、時間指定されたタスクの使用は、更新ホットロードメソッドを実行します。

上記の例から、xmlとJson形式のパース設定のみがホットローディング・メソッドの実行に使用され、設定ファイルをパースするアノテーションの使用はホットローディング・メソッドの実行に使用されないことがわかります。

背理法

High-level modules shouldn t depend on low-level modules. Both modules should depend on abstractiojavans. In addition, abstractions shouldn t depend on details. Details depend on abstractions.

高位モジュールと低位モジュールは、単に呼び出し側と呼び出し側の関係です。この定義から、依存関係の逆転の原則がアーキテクチャレベルでの設計原則であることは明らかです。例えば、ウェブアプリケーションの tomcat コンテナと tomcat の関係は、ウェブアプリケーションが低レベルモジュールであるのに対して、tomcat は高レベルモジュールです。両方は、サーブレットの仕様に依存して、サーブレットは、tomcatやWebアプリケーション固有の実装の詳細には依存しませんが、彼らはサーブレットの仕様に従って詳細を実装することです。

結語

これらの設計原則の原則は複雑なものではないので、独断的にならずにできるだけ適用すべきです。コード開発と設計の初期段階では、機能変更のたびにコードをリファクタリングしなければならないリスクを減らすために、設計原則をできるだけ考慮することが重要です。

これはデザインパターンの学習記事です。もし間違っているところがあれば、コメントや訂正をいただければと思います。

Read next

JavaScriptのオブジェクト:オブジェクト指向かオブジェクトベースか?

他の言語と比べて、中国語の「オブジェクト」はいつも場違いな感じがします。 ある議論では、中国語は「オブジェクト指向言語」ではなく、「オブジェクトベース言語」であると強調されています。この声明は一時期広く流布されましたが、実際、私がこれまでに出会った人の中で、この声明を持っている人は誰も、「オブジェクト指向とオブジェクトベースをどう定義しますか?

Dec 1, 2020 · 8 min read