Open14

アーキテクチャ

104104

なぜ設計思想が生まれるのか・取り入れるのか

  • クリーンアーキテクチャのような設計思想が対峙するには、変更容易性という部分になると思われる。
    • 「テスト容易性を高める => 変更容易性が高まる => テストファーストで設計」しようという考えに基づいたのがテスト駆動開発?
  • 変更容易性がなぜ重要かという話についてはソフトウェア設計原則は変更容易性に通ずの冒頭の解説が分かりやすい。
104104
  • 上記の記事によると変更というのは2パターンある。
    • 1.)ビジネス要求に起因する変更
    • 2.)言語やライブラリなど技術的問題に起因する変更
  • 本来は前者のビジネス要求に起因する変更を優先すべきで、技術的要因の変更に手間をかけたくない。そこで、ビジネス要求に起因する変更と技術的問題に起因する変更を受ける箇所をそれぞれ分離しておく設計手法が生まれる。
    • 技術的なレイヤーに関して実装の詳細は隠蔽しておき、ビジネス側はインターフェース等の仕組みによって公開された振る舞いにのみ依存する。「実装ではなくインターフェースに依存せよ」というのはこのことを指す。

さらに上記の記事では変更容易性を、外部変更容易性と内部変更容易性に分類している。

ただ、内部変更容易性を上げる具体的な方法としてインターフェースを使用することが挙げられているが、外部変更容易性の例にDIやオープン・クローズド原則が挙げられており、インターフェースという概念の有無では区別がつけられないように感じる。

104104

オープンクローズド原則を適用した例についての例

【SOLID】オープン・クローズドの原則を完全に理解したいC#
=> 会員種別によって付与するポイントの計算ロジックを変更したい際に、

  1. 機能追加の際に既存の機能への影響を最小限に抑えるためにロジックを分離する。
  2. タブった処理を簡略化するためにInterfaceを使用して、振る舞いを統一する。
  3. ポイント判定とクラスの呼び出しをFactoryメソッドに移譲する。このとき、Factoryメソッドの戻り値には2のinterfaceを使用する

ということをやっている。

オープン・クローズドの原則とは
=> こちらも似たような例である。

104104

https://b.hatena.ne.jp/entry/s/blog.shin1x1.com/entry/all-principals-of-software-design-lead-to-etc

上記ブコメを見ると「捨てやすさ」というワードが出てくる。

捨てやすさとは?

https://qiita.com/putan/items/d6c10a77ecc5b6649b73

ロジックを関心によってグループ化することで下記のメリットがある
修正の目的や頻度もグループ化され、良くいじるところとそんなにいじらないところが分かれる
(e.g.) UI層は更新頻度が高く、刷新機会も多い
良くいじられる層を外側にし、依存方向を内側向きの一方向にすることで、外側を捨てやすくなる
(e.g.) ReactをVueにする
(e.g.) 自前DBを外部APIにごっそり差し替える

捨てやすさというのは技術面におけるリプレースというニュアンスで使われている?
更新頻度が高そうな箇所とそうでない箇所に分けるのも良さそう。

104104

変更が求められる場面

https://www.wantedly.com/companies/kurashicom/post_articles/388178

ソフトウェアは動かし続けるだけでも劣化する
まず、たとえサービス自体に何の変化が無かったとしても、システムがクラウドをはじめとしたプラットフォームやOSSの上に成り立っている以上、それらの変化への追従から免れることはできません。バージョンアップに対応できなければセキュリティリスクを抱えることになりますし、提供が終了されるものがあれば乗り換えない限りサービスを存続させることもできなくなります。

それだけでなく、ウェブやアプリであればブラウザ・OSも変化していきます。プライバシーに関連する領域のようにプラットフォームの方針や法規制の変化に影響を受けることもあります。我々は自分たちだけで事業を行っているわけではないので、たとえば決済や物流にまつわる対応も発生します。そして(有り難いことに)お客様が増えていけば、求められるパフォーマンスだって変わっていくことでしょう。

https://qiita.com/TairaNozawa/items/276ef777f260f77fe6d8

要求
顧客との話し合いで要求が変化する
運用が進んで使っている人の考え方が変わって要求が変化
働いている人のレベルが上がって要求が変化
チームのメンバーの構成が変わって要求が変化
業務領域、ビジネスの変化によって要求が変化
実際に開発した機能を使ってみたら実際はもっとこうした方が良かったっていう変化
顧客やユーザーが周りの人たち(コミュニティ、家族、友人)などの影響を受けて要求が変化
テクノロジーの変化による要求の変化
業務フローの変化による要求の変化
ライバル企業のプロダクトの進化による要求の変化
フレームワークやライブラリ、言語、開発環境、ツールの変化
法律の変化

104104

クリーンアーキテクチャ

クリーンアーキテクチャというと例の図が先行してしまって、「クリーンアーキテクチャとは結局何か?」という部分が見えづらいので整理する。

  • アウトプットだけ見るとアプリケーションをその責務によって分けてレイヤー化している。
  • 何のために分類するかというと「保守性や拡張性を高めるため」に分類を行なっている。
104104

単体テスト(モック化まわりなど)

https://blog.8-p.info/ja/2021/10/12/mock/

モックの一番の問題は、本番とテストで違うコードが走ることで、これは自動テストの価値をだいぶ下げてしまう。テストが通っているのは、コードが正しいのか、コードがモックと揃っているからなのかわからなくなる。

もう一つの問題は、モックと実装が密結合してしまうことで、後々コードを変更するのが大変になる。実装が変えやすいようにテストを書くのに、実装を変えづらくなっては本末転倒だ。

これは分かる。

Martin Fowler のこの記事は、モックするかどうかを、やりとり (インタラクション) をテストしているのか状態をテストしているのかの違いだと説明し、モックのほうがうまくいくパターンに触れつつも、最後に自分は古典派だと断っていて、両論併記で終わらないところも含めて良かった。

単体テストは状態を、結合テストはインタラクションをテストしており、モック化を積極的に行いたくない層はインタラクションを重視しているということ?
=> 追記

  • 古典派としては単体テストは状態のテストを行うことに焦点を当てたい。
    • 状態のテストというのは「あるメソッドを実行した後、オブジェクトの状態がどう変化したかを検証するテストのこと」。具体的には戻り値の結果を見ることで「あるインプットを与えたらあるreturnが得られる」ことをテストすることができる。
  • モック化したクラスでテストすると本番環境と異なるコードが実行されるのと、モッククラスというのが本来の処理を無視して決まった結果を返すので、上記の状態のテストを行うことができない。なので本番環境での動作も問題ないことが保証されていない。
    • その代わりモックは実行されるときの引数や呼び出し回数はアサーションすることができるので、「あるメソッドを実行したときに他のオブジェクトとどのようなやり取りがあったか」をテストできる。
  • モックを多用するテストの問題点
    • 結果ではなく振る舞いをテストしている
      • モックは、呼び出されるメソッドの返り値を事前に設定する。そのため、テストは「Aを呼び出したらBが呼び出される」という相互作用を検証しているだけで、Bが実際に返す値が正しいか(本番環境で予期せぬエラーが起きないか)までは保証できていない。
    • 本番環境の再現性の欠如
      • 複雑なロジックや外部システムとのやり取り(例えば、データベースのトランザクション処理や外部APIの失敗ケースなど)は、モックでは完全に再現できない。モックはあくまで「想定された振る舞い」を模倣するだけであり、本番でしか起こらない予期せぬバグを見逃すリスクがある。
    • 過剰なカプセル化につながりやすい
      • モック化を前提とすると、必要以上に小さなクラスやインターフェースを定義することになり、コード全体の複雑性を増大させる可能性がある。
104104

https://zenn.dev/nanagi/articles/0e899711611630

複雑なアルゴリズムのテストなどでは確かにユニットテストが有効です。境界条件などの詳細な条件に対してすべて問題なく動作することを保証するために、適切な作業量で適切な効果を得ることができます。しかしながら、Clean Architecture における UseCase など、DBなどの外部システムとの結合度が高いコードに対するユニットテストでは大量のモックオブジェクトを必要とすることがあります。このようなテストはモックを順番に呼び出すだけのテストになりがちで、一体何をテストしたかったのかがわからなくなることもしばしばあります。モックオブジェクトの多いテストは実装コストも保守コストも比較的高いことから、テストの意義がぼやけると余計に費用対効果が悪くなっていきます。

世の中のシステムでは複雑なアルゴリズムを実装するケースの方が少ないのでは?
そうなると単体テストで果たしたい目的はという状況になりそう。

外部システムとの結合が多かったり、内部状態によって実行結果が変わったりと様々なケースがあります。ユニットテストを記述しづらいと感じた時はリファクタリングを検討すると良いでしょう。ただし、テストが簡単になるからと言って、ただ処理をまとめるために過剰にカプセル化を行うようなことはしないように注意を払う必要があります。カプセル化にも当然のようにトレードオフが存在します。テストをしやすくするためではなく、本質的に必要な抽象化を行うべきです。

Todo:カプセル化のトレードオフについて理解する

104104

変更容易性の高いコード

https://zenn.dev/tak_dcxi/articles/65629b78ee3bf7f82c4a

僕がWebデザイナーとして働き始めた頃は「一行でも一文字でもコードを短く書く」「重複のない簡潔でスッキリとしたコードを書く」ことが良いコードだと思っていた。記述を極限まで減らしたシンプルでスッキリしたコードは確かに「綺麗なコード」かもしれない。
ただ、そういった「綺麗なコード」を書くことは実装者のエゴでしかなく、その「綺麗なコード」を実現するためには保守性や拡張性、再利用性、そしてCSSの捨てやすさを犠牲にしかねない。

こちらはCSSの話になるが、極限まで最適化しようとすると変更容易性を害いかねないとは思う。

104104

整理

変更が求められる場面

  • ビジネス的要求
  • 技術面の要求

ビジネス的要求に対応する

  • SOLID原則に従ったコードを書く。
    • 特に単一責任の原則とオープン・クローズドの原則、リスコフの置き換え法則を意識
  • 入力パターンによって出力パターンが異なる場合はファイルやクラスを分ける。
  • 無理な共通化は行わずにファイルやクラスが増えることを恐れない。多少の冗長性は受け入れる。

技術的要求

  • SOLID原則に従った設計にする。
    • 特に単一責任の法則とインターフェース分離の原則、依存性逆転の法則を意識
  • 関心ごとの分離を行なって技術的変更がビジネスロジックの実装に影響しないようにする。
    • 他の技術にすり替えてもある程度同じ記述で置き換えられるようにする。
104104

マイクロサービスアーキテクチャ

コードなどの静的共有データ

  1. マイクロサービス化した後に、それを使用するサービスで重複して持つ。
  2. コード内のプロパティファイルや列挙型として管理する
  3. 独立したサービスとして立てる。

1.と2.は「一方を更新した場合に他方を更新し忘れた」という一貫性の問題が存在するが、後者の方が対応としては楽だと考えられる。ただ、大規模なサービスであれば各サービス内で定義した方が、テストパターンの用意の時に楽なのでないかと思われる。

データベースの分割

モノリシックなサービスを分割する際には「DBの分割→アプリケーションの分割」と進めた方が良い。
例えば元々は1つのアプリケーションで2つのテーブルを更新していた場合に、1つのトランザクション内でそれらを行えていたことになるが、アプリケーションの分割まで踏み込むとトランザクションが分割されて安全性が失われてしまう。

分割されてトランザクションがAとBの2つに分かれているとし、ここでAは成功したがBは失敗したような場合を考える。まず、Bが失敗したときにAをロールバックするようなことはせずに非同期でBを修正する方法があるが、これはBが修正されることが保証されていなければならない。
次に、トランザクションAで作成されたデータを修正・削除戻すという方法があるが、同じく失敗する可能性があるのと、トランザクションが3つ、4つと多数になった場合の処理が困難である。
トランザクションマネージャのようなものを使って各トランザクションのステータスを中央管理し、特定のステータスになったら一斉更新させるという方法もあるが、これはコミットが走るまでトランザクションを扱う各プロセスが中断しているという前提が存在する。

104104

クリーン・レイヤードの設計順

  1. Application層から作成。ユースケース定義を起こす。
    a. InputとOutputのDTOは作成しておく
    b. このとき永続化を伴う場合はインフラ層のインターフェースを作る
    c. ドメイン層に関しては各種Interfaceだけ置いて、ValueObjetやインフラ層に渡すDTOは後から作成する。(インフラ層に渡す引数多くなるが、一旦プリミティブ型で事足りる気がする)

クリーン・レイヤードアーキの構成で迷ったポイント

  1. ユースケース層をどれくらいの粒度で作るか
    例えば「ユーザーを認証する」というケースでemail + パスワード以外の認証方法を考慮したときに、Presentation層で分岐させて、ユースケースも認証方法ごとにクラスを作成するか。