kanachi-blog

notionでの公開記事をastro-notion-blogを使って公開するよ

【Clean Architecture 第Ⅱ部】プログラミングのパラダイムシフト

目次

3つのパラダイム

  1. 構造化プログラミング
  2. オブジェクト指向プログラミング
  3. 関数型プログラミング

この3つに共通していること

全てプログラミングに制限をかけるネガティブな意図を持ったものであるということ

詳しくは以下で見ていく。

構造化プログラミング

トップダウン設計

プログラムは大きなタスクからスタートし、そのタスクを小さなサブタスクに分割することで設計を進める方法。

制御構造の3つの原則

あらゆるプログラムは「順次」「選択」「反復」の3つの構造で構築できる

  • 順次(連続実行): 命令は上から下へと順番に実行される
  • 選択(条件判断): if-else文などの条件分岐を使って、条件に応じて異なる命令を実行する
  • 反復(ループ): forwhileループを使用して、一連の命令を繰り返し実行する

結果

プログラムからgoto文は、モジュールを再帰的に小さな単位に分割できなくなること、それにより合理的な証明に欠かせない「分割統治」が使えなくなることを発見した。現在ではほとんどの言語でgoto文は使用されなくなっている。

オブジェクト指向プログラミング

オブジェクト指向とは何か?この言葉はよく間違って使われている

カプセル化

データと関数がその周囲から見えないように隠されていること

C言語ではヘッダーファイルと実装ファイルで完璧なカプセル化が行えていた。

C++ではコンパイラの技術的な理由からクラスのメンバー変数をヘッダーファイルに宣言する必要があった。それゆえ、完璧なカプセル化ではなくなっていった。

現在はほとんどの言語でヘッダーファイルは廃止されている。

それゆえに実際多くの言語では強制的なカプセル化はほとんど行われておらず、アクセス修飾子のprivateprotectedpublic などでカプセル化を部分的に修復されている。

したがってOOはカプセル化に依存しているというわけではない。

継承

新しいクラス(通常「サブクラス」や「派生クラス」と呼ばれる)が既存のクラス(通常「スーパークラス」や「基底クラス」と呼ばれる)の属性やメソッドを受け継ぐ仕組みを指します。

これはOO登場以前から、プログラマが使用する一般的なプラクティスだった。

実際OO言語の登場のおかげで、継承クラスのキャスト変換は暗黙的に行われるようになった。

OO言語はまったく新しいものを我々に与えてくれたわけではない。だが、データ構造のなりすましが格段に便利になったことは間違いない。

したがってOOは既存であった継承をより使いやすくした。

ポリモーフィズム

異なるオブジェクトが同じインターフェースやメソッド呼び出しに対して異なる実装を持つことができる特性を指します。

ポリモーフィズムは関数へのポインタの応用である。1940年代後半にノイマン型アーキテクチャが実装されて以来、プログラマはポリモーフィズムの振る舞いを実現するために、関数へのポインタを使用してきた。つまり、OOは新しいものを提供しているわけではないのである。

関数へのポインタを明示的に使用して、ポリモーフィズムの振る舞いを生み出すときの問題は、関数へのポインタが危険であることだ。ポインタを初期化するときは「ポインタを経由して関数を呼び出す」という規約を覚えておく必要がある。プログラマが規約を覚えておかないと、バグの追跡と排除が相当難しくなるだろう。

OO言語はこうした規約を排除することで、関数へのポインタの危険性を回避している。OO言語を使えば、ポリモーフィズムも簡単に使えるようになる。

ポリモーフィズムのもたらしたパワー

従来のソースコードの依存関係の流れ

ポリモーフィズムを活用する時の依存関係の流れ

ML1とインターフェースIのソースコードの依存関係(継承関係)が制御の流れと逆転している。

これを依存関係逆転と呼ぶ。

OO言語が安全で便利なポリモーフィズムを提供してくれることで、ソースコードの依存関係は(たとえどこであっても)逆転できる。

これを使って何ができるか?

データベースとUIをビジネスルールに依存させることができる。

結果として、ビジネスルール、UI、データベースを異なるコンポーネントやデプロイメントユニットにまとめることができる。

ビジネスルールは独立してデプロイすることができる。なぜならUIやデータベースに依存していないから。

また一つのコンポーネントのソースコードを変更しても、そのコンポーネントだけデプロイすればいい。これが独立デプロイ可能性。

システムにあるモジュールを個別にデプロイできるので、別々のチームが個別に開発できる。これが、独立開発可能性

結論

OOとは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。

関数型プログラミング

関数型言語の変数は変化しない

可変変数がもたらすもの

  • 競合状態
  • デッドロック
  • 並行更新の問題

だからと言って全てを不変変数にできるのか

  • ストレージ
  • プロセッサ速度

    上記を考慮しなければそれは可能である。

結論

できるだけアーキテクチャ的には全てを不変性のあるものとして扱いたい

以下に不変性へのいくつかのアイデアを書いていく。

可変性の分離

アプリケーションのサービスを以下に分離する

  • 可変コンポーネント
    • 変数の状態の変更を許可している
    • 並行処理や競合状態から保護するためトランザクショナルメモリを使用する
  • 不変コンポーネント
    • 可変変数を使わない
    • 1つ以上の可変コンポーネントと通信する

アーキテクトでの視点でいえば、不変コンポーネントにできるだけ多くの処理を記述して、可変コンポーネントからできるだけ多くのコードを省いていくべきです。

💡
トランザクショナルメモリとは

並行プログラミングとその中のデータの同時アクセス問題を扱うための抽象化手法の一つ

この方法を使用すると、複数のスレッドがメモリに同時にアクセスする際の問題を、データベースのトランザクションのように扱えるようになります。

  1. トランザクション: トランザクショナルメモリでは、複数のメモリ操作をまとめて「トランザクション」という単位で実行する。トランザクションの中での操作は、すべて成功するか、あるいはすべて失敗(中断)するか、どちらか。
  2. アトミック性: トランザクション内の操作は「アトミック」と見なされます。これは、操作が途中で中断されることなく、完全に実行されることを意味します。
  3. 分離性: 各トランザクションは他のトランザクションから分離されて実行されます。これにより、複数のスレッドが同時にメモリにアクセスしても、互いの影響を受けずに安全に操作を行うことができる。
  4. 競合の検出と解決: もし2つのスレッドが同時に同じメモリ領域を変更しようとすると、トランザクショナルメモリはこれを「競合」として検出します。競合が発生した場合、一つのトランザクションは成功し、もう一つは中断され再試行されるか、他の方法で解決される。
  5. 簡単なプログラミング: トランザクショナルメモリを使用することで、従来のロックベースの同期方法よりも、並行プログラミングが簡単になります。ロックやセマフォなどの低レベルな同期手段を直接扱う必要がなくなる。
イベントソーシング

状態ではなく取引(トランザクション)を保存するという戦略、状態が必要になった時は全ての取引を収集するという考え方

言い換えると、CRUDにおけるU(update 更新)とD(delete 削除)を使わないでアプリケーションのステートを管理する手法

具体例

口座残高を保持する銀行アプリケーション、入出金の取引を実行すると、口座の残高が変更される。口座の残高を保存する代わりに、取引のみを保存して、残高を知りたい時には、これまでのすべての取引を合計する。ただ、現状ストレージと処理能力には限界があるので、一定期間だけこの仕組みを働かせる。

まとめ

  • パラダイムの概要
    • 大きく3つのパラダイムがあり、それらは与えるのではなくすべて制限をかけている
  • 構造化プログラミング
    • 直接的な制御の移行に規律を課すものである(goto文)
  • オブジェクト指向プログラミング
    • 間接的な制御の移行に規律を課すものである(関数ポインタ)
  • 関数型プログラミング
    • 代入に規律を課すものである(変数の不変性)