『C#実践開発手法』を読んだ

台風が過ぎて快晴。暑い。一歩も外に出たくない。一歩たりとも。
でも、これを書いたら出ようと思う。なぜなら、快晴だから。

C#実践開発手法 (マイクロソフト公式解説書)

C#実践開発手法 (マイクロソフト公式解説書)

どんな本か

本書は、『Adaptive Code via C#: Agile Coding with design patterns and SOLID principals』(Microsoft Press、2014年)の日本語版です。著者である Gray McLean Hall は、「理論と実践の橋渡し」を本書として記しました。

監訳者あとがきより。
原著タイトルをそのままみると「C#による適応力のあるコード」的に読める。邦題は若干C#の機能やコーディングに寄った本と誤認されるかも。Microsoft公式解説書。書中のサンプルコードはすべて公開されていて実際に動かしながら理解できる。

監訳者の長澤智治氏はAtlassianの現Senior Evangelist。アジャイルやソフトウェア品質の分野で著名な方らしい。

なぜ読んだか

3連休ということで、まとまった時間をとって何か1冊を読み切ろうと思い立った。
最近の興味といえばセキュリティというか認証周りと、あいかわらずデータベース周りなのだが、プログラミングがあまりにできない状況を省みて、C#の本でも読むか、ってなった。
たぶん過去のAmazonのセールで買ったまま、Kindleの中で眠っていた本書を引っ張り出してきた。

感想など

全体

タイトルでC#に言及しているにも関わらず、プロジェクト運営に言及しており、しかもそれが序盤と終盤の大部分を占めている。アジャイルやGitについても丁寧な記述がある。C#er,.NETerでかつこれらを組み合わせるのは著者のトガりを感じられる。プロジェクト運営とコーディングはカテゴリとして別という認識があったが、よくよく考えるとアジャイルプロセスとアジャイルプラクティスは相互作用的に効果をもつはずなので、同じ書籍で言及されるほうがむしろ自然なのかもしれない。

自分はやらなかったけど、サンプルコードが充実しているので、実際に動かしながら進めると理解しやすいかもしれない。「理論と実践の橋渡し」とあるが、個人的感覚としては、かなり実践に寄っているように思えた。理論の部分がもう少し厚ければ理解できるかもという箇所が多々あった。

訳書としてどれくらいの水準が求めら得るものなのかわからないが、訳の品質がけっこうしんどかった。自分は頭が悪いので、日本語としての係り受けや文法がおかしい箇所があると混乱して全体的に理解できなくなる。また、文書の構造ももう少し整理されていれば、と思った。定義を説明している箇所や説明していない箇所があるなど。適宜ググりながら読み進めた。

第1部: アジャイルの基礎

前半は、プロジェクト管理のお話。

弊社の開発部では、基本的にアジャイル開発をやっている。自分が所属するチームは、機能横断的な基盤部分を担当するチームのため、例外的にアジャイル開発をやっていない。そのため、他のチームがしょっちゅう集まって付箋をペタペタはったり、「スプリントガ〜」とか言ったりしてて、謎だったが、本書によって少し理解が進んだ。

ウォーターフォールとアジャイルって適材適所なのかと思っていたが、著者は「ウォーターフォールはダメ」と断言していて意外だった。「変更に強くすること」と「変更を惰性的に受け入れること」は違うと思うので、そのへんのバランスが難しそう。

後半は、設計・コーディングのお話。

リファクタリングの過程が丁寧に説明されていて、どういう過程を経てベストプラクティスと呼ばれるコードに至るのかがわかりやすかった(※わかったとは言っていない)。お仕事で書いているコードは既存サービスの機能追加・改修がほとんどなため、先人たちによるいいかんじのコードが既にある。その設計に乗って書くため、「なんでそうなっている必要があるのか」に対する理解が足りないままでもできてしまったりする。

全体的に、デザインパターンというものに対する知識・理解が圧倒的に不足していると感じた。デザインパターンは学ぶものではないという言説もあるが、自分は自力でそんなもの思いつけるような有能ではないとしっかり自覚している。「技術力をつけるにはコードを書くのが一番」はそうだと思う一方で、体系的に学ぶことでそれを加速させることもできるはず。

第2部: SOLIDコードの記述

それぞれの言葉となんとなくのイメージは持っていたが、理解しているとは良い難い状態だった。

実際読んでみると、普段からほとんど無意識に意識している(?)内容だった。プログラミングをはじめたころはコードレビューで指摘してもらったりしていたことなんだろうと思う。少しは身についているみたいでよかった。

ここでもかなりデザインパターンの理解不足によって読み進めるのがきつかった。共変性と反変性とか未だに理解できていない。ググっていろいろ読み漁ってみたが、それでもまだ理解できない。頭が悪すぎてつらい。  

第3部 アダプティブサンプル

読んでない。
アジャイルをやったことがないので、自身の体験に紐付けることができない。こういう場合、右から左へ抜けていくだけなのを経験上知っているのと、そもそも読み続けていて疲れと飽きでまったく集中力が続かなくなった。
第2部まで呼んだだけでも褒めて欲しい。めっちゃ読み飛ばしてるけど。

今後

.NETのお作法とか、デザインパターンをもっと体系的に着実に理解すべきと思った。願わくば、お仕事で設計するときに自然につかえる状態にまで。 あと、頭が良くなればいいと思った。技術書をサクサク読みたい。読んだことを理解したいし、記憶したい。

読みながら書いたメモ

長い上にまったく推敲していないクオリティ底辺のメモなので、気になる人は本を読んでください。

第1部: アジャイルの基礎

  • アジャイルプロセス: 小回りのきく開発方法を提案する
  • アジャイルプラクティス: 小回りのきくコードの書き方を提案する

第1章: スクラムの紹介

  • 用語
    • スクラム: ソフトウェアプロダクトに対して価値を反復的に積み上げていくという発想に基づく、プロジェクトマネジメントの方法論の1つ
    • スプリント: スクラムプロセスの繰り返しの単位
    • ストーリー: スクラムにおける作業の単位
    • プロダクトバックログ: 未完成のストーリーをまとめ優先順位付きのキュー
    • スプリントバックログ: プロダクトバックログのうち、対象のスプリント内で実行されるストーリーのキュー
  • ウォーターフォールとアジャイル

    変化に対する姿勢 ドキュメントに対する姿勢
    ウォーターフォール コストがかかるし、望ましくないし、放っておく 成果物の中心
    アジャイル 変更は歓迎され、変更に適応することが誰にでも認められる 「動くソフトウェア」が最も重要なドキュメント
  • 役割と責任

    • スクラムはプロセスであり、人々がプロセスに従わなければ効果がない
    • プロダクトオーナー: 構築するフィーチャーと優先順位の決定、完成したプロダクトの承認・却下
    • スクラムマスター: プロセスの決定、運用
    • 開発チーム: プロセスに従って開発
  • その他
    • スクラムボード・カード・スイムレーン
  • 技術的負債

    • コードの品質 vs 締め切り
    • 選択肢を慎重に調べ、その負債を抱える価値があるのかを判断することが重要
    • 技術的負債の4象限 by Martin Fowler
    無鉄砲 用心深い
    意図的 設計のための時間はない とりあえずリリースし、結果に対処しなければならない
    無意識 階層化って何? 今度こそどうすればいいかわかった
  • スプリント

    • リリース計画・スプリント計画・デイリースクラム・スプリントデモ・スプリント振り返り
  • アジャイルとスクラムの問題点: 見積もりの乖離による不適応なコード
    • 硬直性: 抽象化の欠如、責務の混在
    • テスタビリティの欠如
    • メトリクス: ユニットテストのカバレッジ、循環的複雑度

第2章: 依存関係の階層化

  • 依存関係
    • すべてのソフトウェアには依存関係がある
      • 同じコードベース内のファーストパーティの依存関係
      • 外部アセンブリへのサードパーティの依存関係
      • .NET Frameworkへの普遍的な依存関係
    • 依存関係は正しく管理されるべき
      • 最も正しいコードとは、コードが書かれないこと
      • もっとうまく管理される依存関係とは、依存関係が存在しないこと
  • 依存関係の定義
    • 依存関係: 2つのエンティティ間の関係であり、相互に機能の必要条件となる
    • 依存元をサービス、依存先をクライアントと呼ぶ
    • 有向グラフによるモデル化が有効である
  • 依存関係の管理

    • 以下のコードを依存関係の観点でリファクタしていく

      public class AccountConroller
      {
          private readonly SecurityService securityService;
      
          public AccountController()
          {
              // クライアントはインターフェイスの実装に関知するべきではない
              // コンストラクタは実装上の詳細であるため、newの使用は依存関係を密にしてしまう
              this.securityService = new SecurityService();
          }
      
          [HttpPost]
          public void ChangePassword(Guid userId, string newPassword)
          {
              var userRepository = new UserRepository();
              var user = userRepository.GetById(userId);
              this.securityService.ChangeUserPassword(user, newPassword);
          }
      }
      
    • 問題

      • 実装の拡張が困難: SecurityServiceとUserRepositoryに永遠に依存する
      • テスタビリティの欠如: SecurityServiceとUserRepositoryがモックアップできない
      • 不適切な関係: ChangeUserPasswordがUserを要求する
    • 対策

      • SecurityServiceからインターフェースを抽出し、その実装にUserRepositoryも含める

          public class AccountConroller
          {
              private readonly ISecurityService securityService;
        
              public AccountController()
              {
                  this.securityService = new SecurityService();
              }
        
              [HttpPost]
              public void ChangePassword(Guid userId, string newPassword)
              {
                  securityService.ChangUsersPassword(userId, newPassWord)
              }
          }
        
    • 問題

      • 依存の連鎖: SecurityServiceの依存先に暗黙的に依存する
    • 対策

      • 依存性を注入する

          public class AccountConroller
          {
              private readonly ISecurityService securityService;
        
              // ISecurityServiceの実装を提供する別のクラスを要求
              public AccountController(ISecurityService securityService)
              {
                  if(securityService == null)
                      throw new ArgumentNullException("securityService");
        
                  this.securityService = securityService;
              }
        
              [HttpPost]
              public void ChangePassword(Guid userId, string newPassword)
              {
                  securityService.ChangUsersPassword(userId, newPassWord)
              }
          }
        
    • Entourage(取り巻き)アンチパターン

      • 単純なものを要求したはずなのに、仲間をすべて連れてくる
    • Stairway(階段)パターン
      • インターフェイスとそれらの実装を異なるアセンブリに配置することで、変更が独立する
      • 理想的には、クライアントに必要な参照は1つ(インターフェイスアセンブリへの参照)だけ
    • 依存関係の解決
      • CLRはJITモデルを使い、アセンブリIDを特定し、GACあるいはフォルダを検索する
    • NuGetを使った依存関係の管理
      • 依存関係の連鎖を辿り、すべて利用可能であることを確認してくれる
      • 依存関係のバージョンを管理し、特定バージョンのみへの依存も指定できる
  • 階層化

    • コンポーネント: 相互関係のある2つ以上のアセンブリのグループ
    • 階層化:コンポーネントを水平レイヤーに積み重ね、依存関係は常に下を向く
    • 共通パターン
      • 2層アーキテクチャ: ユーザインターフェイス層/データアクセス層
        • アプリケーションにあまりロジックがない場合
      • 3層アーキテクチャ: ユーザインターフェイス層/ロジック層/データアクセス層
    • 非対称な階層化
      • ユーザリクエストのパラメータに寄ってその下の層の処理が分岐することもある
      • コマンド/クエリ分離(CQS)
        • コマンド: アクションに対する命令的な呼び出しであり、コードに何かを実行させる
        • クエリ: データに対するリクエストであり、コードに何かを取得させる
      • コマンド/クエリ責務分離(CQRS)
        • CQSとほぼ同じルールをアーキテクチャレベルで適用する
        • 「コマンドをクエリを最も効果的に実行するためには、階層化を通じて異なるパスをたどったほうがいかも」という認識に基づいている
        • CQRS適用の最も簡単なシナリオ: ドメインモデルを持つ3層アーキテクチャ
          • ドメインモデルはコマンドでのみ使い、クエリは2層アーキテクチャ(非対称な階層化)
  • まとめ
    • プロジェクトの安定性、適応性、生存能力は、依存関係の管理にかかっている

第3章: インターフェイスとデザインパターン

  • インターフェイスとは
    • クラスの振る舞いを定義するが、実装方法は定義しない
    • 常にpublicであり、1つのクラスが複数のインターフェイスを実装できる
      • cf.クラスは多重継承できない(菱形継承問題)
    • 明示的な実装
      • インターフェイスのインスタンスへの参照が必要になる
      • メソッドのシグネチャがクラスにすでに存在していて、シグネチャの競合を回避したい場合
    • 多態性
      • ある1つの型のオブジェクトを使用し、別の型のオブジェクトであるかのように動作させる
  • アダプティブデザインパターン

    • https://www.amazon.co.jp/dp/B000SEIBB8
    • よいデザインパターンは、インターフェイスとクラスの再利用可能な組み合わせである
    • Null Objectパターン
      • 目的: NullReferenceReceptionを無意識にスローし、nullチェックだらけになるのを防ぐ
      • 解決: nullの場合のクラスを定義し、サービス側でnullチェックしてそのクラスのオブジェクトを返す

          public class NullUser : IUser
          {
              public void IncrementSessionTicket() { /* 何もしない */ };
          }
        
      • IsNullプロパティアンチパターン

        • NullObjectのときだけtrueを返すIsNullプロパティをIUserに追加し、nullチェックに使う
        • カプセル化を目的としているオブジェクトからロジックがこぼれ出てしまう
    • Adapterパターン
      • 目的: 未実装のインターフェイスに依存していても、そのオブジェクトインスタンスを提供する
      • 解決: 目的のインターフェイスを実装するAdapterクラスを作成し、別のオブジェクトに委譲する
      • 利用シーン: 目的のインターフェイスに合わせたクラスの変更が不可能な場合(sealed, 外部アセンブリ)
      • Class Adapterパターン

          public class Adaptee
          {
              public void MethodB() { }
          }
        
          public class Adapter : Adaptee
          {
              public void MethodA() { MethodB(); }
          }
        
          public Program
          {
              static Adapter dependency = new Adapter();
              static void Main(string[] args)
              [
                  dependency.MethodA();
              ]
          }
        
        • アダプターとしての継承を利用する
      • Object Adapterパターン

           public interface IExpectedInterface
          {
              void MethodA();
          }
        
          public class Adapter : IExpectedInterface
          {
              private TargetClass target;
        
              public Adapter(TargetClass target) { this.target = target; }
              public void MethodA() { target.MethodB(); }
          }
        
          public class TargetClass
          {
              public void MethodB() { }
          }
        
        • 委譲を利用する
    • Strategyパターン
      • 目的: 再コンパイルを行わずにクラスの振る舞いを変更できるようにする
      • 利用シーン: オブジェクトの状態に応じてクラスの振る舞いを切り替える必要がある場合

          public Interface IStrategy
          {
              void Execute();
          }
        
          public class ConcreteStrategyA : IStrategy
          {
              public void Execute { /* Aの処理 */ };
          }
        
          public class ConcreteStrategyB : IStrategy
          {
              public void Execute { /* Bの処理 */ };
          }
        
          public class User
          {
              private IStrategy currentStrategy;
        
              public User() { currentStrategy = new ConcreteStrategyA(); }
        
              public void DoSomething()
              {
                  currentStrategy.Execute();
        
                  // 呼び出しごとにストラテジを切り替え
                  currentStrategy = currentStrategy == strategyA ? new ConcreteStrategyA() : new ConcreteStrategyB();
              }
          }
        
  • さらなる汎用性

    • ダックタイピング
      • 「アヒルのように歩き、アヒルのように泳ぎ、アヒルのように鳴くものはアヒルである。」
      • オブジェクトに何ができるかはオブジェクトそのものが決定する
      • ダックテスト: オブジェクトが特定のインターフェイスで宣言されたメソッドをもつ場合、そのインターフェイスを実装しているとみなす
    • ミックスイン
      • 実装の継承を使用せずに、他の複数のクラスの実装を含んでいるクラス
      • C#では多重継承がサポートされていないため、拡張メソッドもしくはサードパーティライブラリを利用する
  • まとめ
    • インターフェイスは、クラスの多様性をカプセル化する多態性を促進し、デザインパターンを成長させる原動力になる
    • インターフェイス自身は、何もしない

第4章: ユニットテストとリファクタリング

  • ユニットテスト
    • 他のコードをテストするためのコードを記述する手法
    • AAAパターン
      • Arange: テストの事前条件のセットアップ
      • Act: テストの対象となるアクションの実行
      • Assert: 振る舞いが期待どおりであることの検証
    • テスト駆動開発(TDD): Red→Green→Refactor
    • より複雑なテスト: Fake,Mockの利用
  • リファクタリング

    • 既存のコードを漸進的に改善するプロセス
    • 既存のコードの変更

        public class Account
        {
            public Account(AccountType type) { this.type = type; }
            public decimal Balance {get; private set;}
            public int RewordPoints {get; private set;}
      
            public void AddTransaction(decimal amount)
            {
                RewordPoints += CalculateRewordPoints(amount);
                Balance += amount;
            }
      
            public int CalculateRewordPoints(decimal amount)
            {
                int points;
                switch(type)
                {
                    case AccountType.Silver:
                        points = (int)decimal.Floor(amount / 10);
                        break;
                    case AccountType.Gold:
                        points = (int)decimal.Fllor((Balance / 10000 * 5) + (amount / 5));
                        break;
                    default:
                        points = 0;
                        break;
                }
                return Math.Max(points, 0)
            }
      
            private readonly AccountType type;
        }
      
      • コンストラクタをファクトリメソッドに置き換える

          public abstract class AccountBase
          {   
              public static AccountBase CreateAccount(AccountType type)
              {
                  AccountBase account = null;
                  switch(type)
                  {
                      case AccountType.Silver:
                          account = new SilverAcount();
                          break;
                      case AccountType.Gold:
                          accunt = new GoldAcount();
                          break;
                  }
        
                  return account;
              }
        
              public decimal Balance {get; private set;}
              public int RewordPoints {get; private set;}
        
              public void AddTransaction(decimal amount)
              {
                  RewordPoints += CalculateRewordPoints(amount);
                  Balance += amount;
              }
        
              public abstract int CalculateRewordPoints(decimal amount) { }
          }
        
          // クライアント
          public void CreateAccount(AccountType accountType)
          {
              var newAccount = AccountBase.CreateAccount(accountType);
              accountRepository.NewAccount(newAccount);
          }
        
        • staticであるため、クライアントがインスタンスではなく型でメソッドを呼び出す
        • 戻り値の型が基底クラスであるため、サブクラスを隠蔽できる
      • コンストラクタをファクトリクラスに置き換える

          public interface IAccountFactory
          {
              AccountBase CreateAccount(AccountType type);
          }
        
          public class AccountService
          {
              private readonly IAccountRepository accountRepository;
              private readonly IAccountFactory accountFactory;
        
              public AccountService(IAccountFactory accountFactory, IAccountRepository accountRepository)
              {
                  this.accountFactory = accountFactory;
                  this.accountRepository = accountRepository;
              }
        
              public void CreateAccount(AccountType accountType)
              {
                  var newAccount = accountFactory.CreateAccount(accountType);
                  accountRepository.NewAccount(newAccount);
              }
          }
        
          // クライアント
          public void CreateAccount(AccountType accountType)
          {
              AccountService.CreateAccount(accountType);
          }
        
        • スタンドアロンファクトリの実装に結び付けず、インターフェイスを提供する
    • 新しいアカウントタイプの作成
      • AccountBase.CreateAccountのswitch分岐を追加する方法
      • accountTypeの名前からAccountBase型のインスタンスを生成する方法

          public AccountBase CreateAccount(string accountType)
          {
              var objectHandle = Activator.CreateInstance(null, string.Format("{{accountType.ToString}}Account"));
              return (AccountBase)objectHandle.Unwrap();
          }
        
  • まとめ

    • ユニットテストとリファクタリングは同時に実行すべきである
    • ユニットテストはオブジェクトの振る舞いに関知せず結果のみをみる

第2部: SOLIDコードの記述

第5章: 単一責務の原則

  • 単一責務の原則
    • 変更する理由が1つしかないコードを記述する
    • 変更する理由が複数あるクラスは、複数の責務をもち、それは分割されるべきである
  • 責務が多すぎる例
    • ファイル処理に関する、ストリーム読み取り、文字列の解析、フィールドの検証、ログの出力、データベースへの挿入などの責務をもつクラスあるいはメソッド
    • 以下の手順でリファクタリングする
        1. コードの明確化のため、各処理をメソッドに切り出す
        1. 抽象化のため、インターフェイスに切り出す
  • 単一責務の原則とDecoratorパターン
    • Decoratorパターン
      • 目的: 各クラスの責務を確実に1つにする
      • 前提

        • 各デコレータクラスが型の契約を満たす
        • それらの型をコンストラクタのパラメータとして受け取る

            public interface IComponent
            {
                void Something();
            }
          
            public class ConcreteComponent : IComponent
            {
                public void Something() { }
            }
          
            public class DecoratorComponent : IComponent
            {
                private readonly IComponent decoratedComponent;
          
                public DecoratorComponent(IComponent decoratedComponent)
                {
                    this.decoratedComponent = decoratedComponent;
                }
          
                public void Something()
                {
                    SomethingElse();
                    decoratedComponent.Something();
                }
          
                private void SomethingElse() {}
            }
          
            class Problem
            {
                static IComponent component;
          
                static void Main(string[] args)
                {
                    component = new DecoratorComponent(new ConcreteComponent());
                    component.Something();
                }
            }
          
      • Compositeパターン

        • Decoratorのより一般的なパターン
        • 目的: 1つのインターフェイスのさまざまなインスタンスを1つのインスタンスであるかのように扱う

            public interface IComponent
            {
                void Something();
            }
          
            public class Leaf : IComponent
            {
                public void Something() {}
            }
          
            public class CompositeComponent : IComponent
            {
                private ICollection<IComponent> children;
                public CompositeComponent() { children = new List<IComponent>(); }
                public void AddComponent(IComponent component) { children.Add(component); }
                public void RemoveComponent(IComponent component) { children.Remove(component); }
                public void Something()
                {
                    foreach(var child in children)
                    {
                        child.Something();
                    }
                }
            }
          

           

      • 述語デコレータ: コードの条件付きの実行をクライアントから隠蔽する
      • 分岐デコレータ: 述語デコレータをさらに拡張し、条件評価の分岐で何かを実行する
      • 遅延デコレータ: 最初に使用されるときまでインスタンス化されないインターフェイスへの参照をクライアントに提供できる
  • switchの代わりにStrategyパターンを利用する

      public class OnlineCart
      {
          public void CheckOut(PaymentType paymentType)
          {
              switch(paymentType)
              {
                  case PaymentType.CreditCard:
                      /* クレジットカードでの支払い処理 */
                      break;
                  case PaymentType.Paypal:
                      /* Paypalでの支払い処理 */
                      break;
              }
          }
      }
    
    • 各処理をStrategyクラスに切り出す

        public class OnlineCart
        {
            private IDictionary<PaymentType, IPaymentStrategy> paymentStrategy;
      
            public OnlineCart()
            {
                paymentStrategies = new Dictionary<PaymentType, IPaymentStrategy>();
                paymentStrategies.Add(PaymentType.CreditCard, new CreditCardPaymentStrategy);
                paymentStrategies.Add(PaymentType.Paypal, new PaypalPaymentStrategy);
            }
      
            public void Checkout(PaymentType paymentType)
            {
                // 支払い方法に応じた処理
                paymentStrategies[paymentType].ProcessPayment();
            }
        }
      
  • まとめ

    • 単一責務の原則は、コードの適応力を大きく上げる
    • コードをインターフェイスとして抽出し、それを実装するオブジェクトに委譲する

第6章: 開放/閉鎖の原則

  • 開放/閉鎖の原則とは
    • Mayerの定義: ソフトウェアエンティティは、拡張に対して開いていなければならず、変更に対して閉じていなければならない
    • Martinの定義: 「拡張に対して開いている」とは、モジュールの振る舞いを変更できること、「変更に対して閉じている」とは、その結果として、ソースやバイナリコードで変更が生じないこと -「変更に対して閉じている」の例外
      • バグの修正
      • クライアントに影響を与えない箇所
  • 拡張ポイント
    • 以下3点がある
      • 仮想メソッド: 継承によって拡張できる
      • 抽象メソッド: 委譲によって拡張できる
      • インターフェイスの継承
    • 継承を意図して設計する、もしくは継承を禁止する
      • sealedがついていないければ継承をサポートしていることになる
  • 「予想されるバリエーションのポイントを特定し、それらの周りに安定したインターフェイスを作成する」by Addson-Wesley
    • 予想されるバリエーション: ビジネスクライアントが追加変更を要求する可能性のある箇所
    • 安定したインターフェイス: 変化しない(インターフェイスのメリットは変化しにくいこと)
    • ゴルディロックスゾーン(適度な料の拡張ポイントが含まれているコード)を目指す
  • まとめ
    • 開放/閉鎖の原則は、クラスとインターフェイスの全体的な設計と、将来にわたって変更可能なコードを開発者が構築するためのガイドラインである

第7章: リスコフの置換原則

  • リスコフの置換原則とは
    • クライアントが期待される振る舞いに関して妥協することなく、すべてのクラスまたはサブクラスを確実に使用できるような継承階層を作成するためのガイドラインの集まり
    • これに従っていれば、クラス階層を拡張するときに、基底クラスまたはインターフェイスの変更、ひいてはクライアントの変更が必要ない
    • そのため、開放/閉鎖の原則と単一責務の原則の両方を適用するのに役立つ
    • 正式な定義: 「SがTの派生系であるとすれば、T型のオブジェクトをS型のオブジェクトと置き換えたとしても、プログラムは動作し続ける」by Barbara Liskov
    • LSPに準拠するためのルール
      • コントラクトのルール
      • 変性のルール
  • コントラクト
    • インターフェイスやそのシグネチャをみただけでは、実装上の要件や保証がわからない
    • 事前条件: メソッドの先頭でオブジェクトの状態をチェックし、エラーを投げる
      • ルール1. 事前条件を派生型で強化することはできない
    • 事後条件: メソッドの最後で戻り値をチェックし、エラーを投げる
      • ルール2. 事後条件を派生型で緩和することはできない
    • データ不変条件:オブジェクトのライフタイムにわたって変化しない
      • ルール3. 基底型の不変条件は派生型でも維持されなければならない
    • 既存クラスのサブクラス作成時にこれらを満たせば、置換性が保たれる
    • コードコントラクト: 静的な検証により、アプリケーションを実行せずにコントラクト違反をチェックする
  • 共変性と反変性
    • 定義
      • 変性: 複雑な型を含んでいるクラス階層において、派生型に期待される振る舞い
      • 共変性

        • Enables you to use a more derived type than originally specified.
        • You can assign an instance of IEnumerable to a variable of type IEnumerable.

            public class Entity
            {
                public Guid Id { get; private set; }
                public string Name { get; private set; }
            }
          
            public class User : Entity
            {
                public string Email { get; private set; }
            }
          
            public class EntityRepository
            {
                public virtual Entity GetById(Guid id) { return new Entity(); }
            }
          
            public class UserRepository : EntityRepository
            {
                // C#において、ジェネリック型がない場合、メソッドの戻り値の型は共変ではないため、コンパイルエラー
                public override User GetById(Guid id) { return new User(); }
            }
          
        • EntityRepositoryをジェネリッククラスとして再定義

            // where句により、Entityクラス階層の一部でない型がサブクラスから提供されることはなくなる
            public interface IEntityRepository<TEntity> where TEntity : Entity
            {
                TEntity GetById(Guid id);
            }
          
            // EntityRepositoryとUserRepositoryの継承関係が維持される
            public class UserRepository : IEntityRepository<User>
            {
                // UserRepositoryのクライアントがEntityではなくUserオブジェクトを受け取ることを保証できる
                public User GetById(Guid id) { return new User(); }
            }
          
      • 共変性

        • Enables you to use a more generic (less derived) type than originally specified.
        • You can assign an instance of IEnumerable to a variable of type IEnumerable.
      • 不変性
        • Means that you can use only the type originally specified; so an invariant generic type parameter is neither covariant nor contravariant.
        • You cannot assign an instance of IEnumerable to a variable of type IEnumerable or vice versa.
        • 型をどのように準備してもクラス階層は生成されない
        • 共変でも半変でもないジェネリック型は不変
    • LSPの型システムのルール
      • ルール1. 派生型のメソッドには反変性がなければならない
      • ルール2. 派生型の戻り値の型には共変性がなければならない
      • ルール3. 新しい例外は許可されない
  • まとめ
    • サブクラスが事前条件を強化したり、サブクラスを緩和できないようなガイドラインを提供する
    • サブクラスの変性に関するルールを提案する
    • ルールに違反する場合、クライアントが基底型とインターフェイス以外を参照し、実装によって振る舞いを変更されてしまう

第8章: インターフェイス分離の原則

  • インターフェイスをより小さくすべきであることを示す原則
  • インターフェイス分離の例

      public interface ICrud<TEntity>
      {
          void Create(TEntity entity);
          TEntity ReadOne(Guid id);
          IEnuemerable<TEntity ReadAll();
          void Update(TEntity entity);
         void Delete(TEntity entity);
      }
      
    
    • ロギングやトランザクションの管理は横断的関心事なので、AOPを使ってすべての実装をデコレーターで拡張できる

        public clas CrudLogging<TEntity> : ICrud<TEntity>
        {
            public CrutLogging(ICrud<TEntity decoratedCrud, ILog log)
            {
                this.decoratedCrud = decoratedCrud;
                this.log = log;
            }
      
            public void Create(TEntity)
            {
                log.InfoFormat($"Creating entity of type {{typeof(TEntity).Name}}");
                decoratedCrud.Create(entity);
            }
      
            /* 残りのメソッドも同様 */
        }
      
        public clas CrudTransactional<TEntity> : ICrud<TEntity>
        {
            public CrudLogging(ICrud<TEntity decoratedCrud)
            {
                this.decoratedCrud = decoratedCrud;
            }
      
            public void Create(TEntity)
            {
                using(var transaction = new TransactionScope())
                {
                    decoratedCrud.Create(entity);
                    transaction.Complete();
                }
            }
      
            /* 残りのメソッドも同様 */
        }
      
    • 一方で、一部のメソッドだけをデコレートしたい場合(Deleteメソッドの中にConfirmの処理を追加するなど)は、それ以外をパススルーするため、冗長になる

    • そのため、削除だけをinterfaceに切り出す

        public interface ICrud<TEntity>
        {
            void Create(TEntity entity);
            TEntity ReadOne(Guid id);
            IEnuemerable<TEntity ReadAll();
            void Update(TEntity entity);
        }
      
        public interface IDelete<TEntity>
        {
            void Delete(TEntity entity);
        }
      
        public class DeleteConfirmation<TEntity> : IDelete<TEntity>
        {
            /* Deleteの実装など */
        }
      

第9章: 依存性反転の原則

  • 最初が肝心
    • DIを使用しない

        public ExampleController()
        {
            this.ExampleService = new ExampleService();
        }
      
      • 実装に依存するため、Controllerのユニットテストが難しい
      • 外から見てなにが必要かわからない
      • ExampleServiceの依存先に暗黙的に依存している
    • DIを使用する

        public ExampleController(IExampleService exampleService)
        {
            this.ExampleService = exampleService;
        }
      
      • プロキシ化可能(別の実装をクライアントに提供できる)
      • オブジェクトグラフの作成において、インターフェイスをコンストラクタに注入するが、それでも実装に依存してしまう
        • 解決法1. Poor Man's Dependency Injectionパターン
          • 事前(コンストラクタや初期化イベント)に、必要なオブジェクトグラフを作成する
          • 利点: オブジェクトグラフを作成する方法が一つしかないため、柔軟である
          • 欠点: 新しい機能が増えるたび(必要な依存関係が増えるたび)に事前処理が拡張するため、冗長である
        • 解決法2. メソッド注入
          • メソッドの引数にインターフェイスを渡す
          • 利点: 依存関係がメソッドに限定される
          • 欠点: メソッドの呼び元がインスタンスを確保する必要がある
        • 解決法3. プロパティ注入
          • クラスのコンストラクタにインターフェイスを渡す
          • 利点: インスタンスプロパティを実行時に変更できる
          • 欠点: 実装だけでなくインターフェイスを変更する必要がある
      • Inversion of Controlパターン
        • 上記の問題: オブジェクトグラフが静的に(コンパイル時に)生成される
        • IoCコンテナ: アプリケーションのインターフェイスをそれらの実装と結びつけ、依存関係を解決し、クラスのインスタンスを取得できる

          // Unityコンテナを作成
          var container = new UnityContainer(); 
          
          // 実行時に解決されるべきインターフェイスとその実装の関係をコンテナに登録
          container.RegisterType<IExampleService, ExampleService>();
          
          // クラスのコンストラクタを調べ、依存関係にあるクラスを連鎖的にインスタンス化
          // Poor Man's Dependency Injectionと違ってコードに明示する必要がない
          var test = container.Resolve<ExampleService>();
          
          • Register,Resolve,Releaseパターン: IoCコンテナはすべてメソッドが3つのシンプルなインターフェイスとして単純化できる
  • 高度な注入
    • Service Locatorアンチパターン

      var service = ServiceLocator.Current.GetInstance<IExampleService>();
      var test = service.GetXxxx();
      
      • 依存先のオブジェクトを解決するための役割もったオブジェクト

      • 問題1. 本来不要であるServiceLocatorへの依存が発生してしまう

      • 問題2. 依存関係がわかりにくくなる
      • 依存関係をまったく注入しないよりはマシ
      • コンテナを直接クラスに注入すると、依存関係の階層が深くなり外から見えない振る舞いが大きくなるため、小さいクラスに切り出すべき
    • Illegitimate Injectionパターン ?
    • 合成ルート:
      • DIにおいて依存性を知っている箇所(Poor Man's〜におけるクラス生成箇所、IoCコンテナにおけるRegister群)は一箇所にまとめるべき
      • アプリケーションのエントリポイント近くにあるべき
      • 解決ルート: 解決の対象になるオブジェクトグラフのルートを形成するオブジェクト型
      • ASP.NET MVCでは、解決ルートはコントローラ、合成ルートはHttpApplication.Application_Start()と明確に定義されている
    • 設定より規約
      • 設定: インターフェイスを実装に1つずつマッピングする
      • 規約: コンテナに対する命令であり、インターフェイスを実装に自動的にマッピングする
      • DIに関する3つの選択肢(Poor Man's〜/設定より規約/明示的な登録)の間にはトレードオフがある(https://www.amazon.co.jp/dp/1935182501)
  • まとめ
    • 依存性反転の原則とそれに基づくDIは、他の原則をつなぎ合わせる役割を持つ
    • DIを実装するにはさまざまな選択肢がある(Poor Man's〜/設定より規約/明示的な登録)
    • Service LocatorとIllegitimate Injectionは正しく実装されたDIを台無しにする
    • アプリケーションの種類ごとに合成ルートと解決ルートがある

第3部: アダプティブサンプル

  • 割愛。

参考