ソフトウェアアーキテクチャの大統一論

2020/11/05 06:54

Rodrigo Flores    
バックエンドにフォーカスしたPython開発者
この記事は、著者の許可を得て配信しています。
https://danuker.go.ro/the-grand-unified-theory-of-software-architecture.html

Bobおじさんのクリーン・アーキテクチャや、コア関数型シェルを中心としたGary Bernhardtの薄い命令型シェルとの対応関係をマッピングしてみると、ソフトウェアを安価に維持し、スケールアップするやり方がよく理解できるはずです!

Brandon Rhodes氏がこの方法を用いていました。このような明確な考え方にはなかなか出会えません。

2011年、2012年、2013年に開催された、BobおじさんのクリーンアーキテクチャやGary BernhardtのPyConの講演を説明する彼のプレゼンテーションやスライドを聞く機会が得られ、私はとても光栄に思っています。

Rhodes氏は、これらの重要な概念を3枚のスライドで説明してくれます。今回は、彼の話を要約して、私の見識を少しだけ加えた説明をしたいと思います。

このページのPythonコードの著作権はBrandon Rhodes氏に帰属し、図の著作権はRobert C. Martin (Uncle Bob)氏に帰属しています。私はこれらを(うまくいけば)フェアユース(著作物を公正に利用する場合、著作権者の許諾がなくても、 著作権の侵害にあたらないとする考え方、非営利で教育的な目的)の下で使用しています。

用語説明

まず、お互いに共通認識を持ち、理解し合えるようにするためにここで使う言葉をご紹介します。

  • 関数:Python の「関数」のことを「関数」 または 「純粋関数」 と呼びます。パラメータを入力にのみ使用し、結果を出力として返します。またいかなる副次的効果も(I/Oのような)起こしません。

・ 純粋関数は、同じ入力値を渡すたび、決まって同じ出力値が得られます。
・ 純粋関数は、システムの状態を変えることなく何度でも呼び出すことができ、DB、UI、他の関数やクラスに影響を与えることはありません。
・ 数学の関数と非常によく似ています。xからyへの移動に関して、それ以外は何も起こりません。
・ 悲しいことに、純粋関数だけを持つことはできません。ソフトウェアには副次的効果を引き起こす目的があります。 

  • プロシージャ、ルーチン、またはサブルーチン:実行されるコードの一部で、副次的効果がある場合とない場合があります。これはPythonでは「関数」ですが、「純粋関数」ではないかもしれません。
  • テスト: 自動化されたユニットテスト。「ユニット」とは、必ずしも単なるクラスではなく、ビヘイビアを意味します。これに関しては、以前の記事のカップリングの章で詳しく説明しています

リスト #1: 複雑なコード

import requests                      # Listing 1
from urllib import urlencode

def find_definition(word):
    q = 'define ' + word
    url = 'http://api.duckduckgo.com/?'
    url += urlencode({'q': q, 'format': 'json'})
    response = requests.get(url)     # I/O
    data = response.json()           # I/O
    definition = data[u'Definition']
    if definition == u'':
        raise ValueError('that is not a word')
    return definition

ここにURLのためのコードがあります。ネットワーク上(I/O)のデータを取得し、結果(単語の定義)をバリデートして、それを返します。

これは少しやりすぎですね。理想的にはプロシージャは一つのことだけを行うべきです。この小さなプロシージャはまだ非常に読みやすいですが、これはより発展したシステムのメタファーであり、適当な長さになる可能性があります。

今のワンパターンな反応は、I/O操作をどこか遠くに隠そうとするためのものです。以下は、I/O 行を抽出した後の同じコードです。

リスト#2: I/Oを下に隠す

def find_definition(word):           # Listing 2
    q = 'define ' + word
    url = 'http://api.duckduckgo.com/?'
    url += urlencode({'q': q, 'format': 'json'})
    data = call_json_api(url)
    definition = data[u'Definition']
    if definition == u'':
        raise ValueError('that is not a word')
    return definition

def call_json_api(url):
    response = requests.get(url)     # I/O
    data = response.json()           # I/O
    return data

リスト2では、I/Oはトップレベルのプロシージャから抽出されます。

ただコードがまだカップリングされていることが問題です。何かをテストしようとする場合、call_json_apiがいつでも呼び出されます。 URLの構築や結果の解析の時にも呼び出されます。

カップリングで、ソフトウェアをキルされます。

カップリングを見極めるための使える経験則は以下の通りです。 フランケンシュタインのようにモックや依存オブジェクト注入をすることなく、コードの一部をテストできますか?

ここでは、HTTPリクエストを避けるために、call_json_apiを内部から何らかの方法で置き換えることなく、find_definitionをテストすることはできません。

それでは、より良い解決策を具体的に見てみましょう。

リスト#3: トップにあるI/O

def find_definition(word):           # Listing 3
    url = build_url(word)
    data = requests.get(url).json()  # I/O
    return pluck_definition(data)

def build_url(word):
    q = 'define ' + word
    url = 'http://api.duckduckgo.com/?'
    url += urlencode({'q': q, 'format': 'json'})
    return url

def pluck_definition(data):
    definition = data[u'Definition']
    if definition == u'':
        raise ValueError('that is not a word')
    return definition

ここでは、トップのプロシージャ(プログラムの命令型シェルとも言う)がI/Oを処理し、他のすべては純粋関数(build_url, pluck_definition)に移されています。純粋関数は、作成されたデータ構造上でそれらを呼び出すだけで簡単にテストできます。

このように命令型シェルとコア関数型に分離するというやり方は、関数型プログラミングでは推奨されている方法です。

しかし、理想的には、実際のシステムでは、これらのルーチンのような小さな要素をテストするのではなく、より多くのシステムを統合することになるでしょう。そういったトレードオフを理解するために、以前に書いた記事のカップリングの章を参照してください

コア関数型シェル/命令型シェル対クリーン・アーキテクチャ

こちらのBobおじさんのクリーンアーキテクチャの図を見てください。(この図の著作権はRobert C. Martin氏、別名Bobおじさんに帰属しています)

Bobおじさんのユースケースとエンティティ (チャートの赤い円と黄色い円) は、先ほど見た純粋関数 - リスト 3 の build_url と pluck_definition と、それらがパラメータとして受け取り出力として送信するプレーンオブジェクトにマッピングされます。(2020年10月28日更新)

Bobおじさんの インターフェイスアダプター (緑の円) は先ほどのトップレベルの命令型シェルにマップします。リスト3の find_definition は外部 (Web、DB、UI、他のフレームワーク) に対する I/O だけを処理します。

2020年10月28日更新:今日のMVCフレームワークにおける 「Model」オブジェクトは毒リンゴのようなものです。「純粋な 」オブジェクトや 「humble 」オブジェクト(テストしにくいロジックとテストしやすいロジックを分離し、テストにくいロジックが控えめになるよう分割すべきというもの)ではなく、データベースからの保存や読み込みのような副次的効果が起こる可能性のあるものです。そういった「保存」や 「読み込み」のメソッドが原因で、コードがテスト不可能になる副次的効果が起きています。これらのオブジェクトを避けるか、システムの周辺部に限定し、 DB とのやりとりによる影響を減らすようにしましょう (実際には隠れたインターフェイスアダプタです)。

円の左側にある矢印は、より抽象的な部分を内側に向かっていることに注目してください。これらはプロシージャや関数の呼び出しです。私たちのコードは外部から呼び出されます。これにはいくつかの例外があります。何をするにしても、データベースがアプリを呼び出すことはありません。しかし、ウェブはそれができますし、ユーザーはUIを通じて、OSはSTDINを通じて、タイマーは一定の間隔で(ゲームのように)呼び出しできます。(2020年10月28日更新)

トップレベルのプロシージャ

  1. インプットを取得する
  2. システムに受け入れられるシンプルなオブジェクトに適応させる
  3. コア関数を介してそれをプッシュする
  4. コア関数から戻り値を取得する
  5. 出力デバイスに合わせて適応させる
  6. 出力デバイスにプッシュする

これにより、コア関数を簡単にテストすることができます。理想的には、生産システムのほとんどは純粋関数であるべきです。

メリット

命令型シェルを減らし、コードをコア関数に移動させれば、各テストはほぼ全体の (現在の関数の) スタックをバリデーションすることができますが、実際に外部アクションを実行することはできません。

これにより、統合テストの回数を減らしても十分に命令型シェルをテストすることができます。ただし、コア関数に正しく接続されているかどうかは必ず確認してください。

システムに実際のユーザーと統合テスト用の 2人のユーザーがいて、両方のユーザーの声を聞くことで、アーキテクチャをガイドして、カップリングを最小限に抑え、より柔軟なシステムを構築することができます。

柔軟なシステムを持つことで、ビジネスとしての競争力を維持するために、新しい機能を実装したり、既存の機能を迅速に安価なものに変更したりすることができます。

みなさんからのコメントを楽しみにしています。私はまだこの考え方が定着していないので、大切なことを書き忘れていることもあります!

2020年10月28日編集:私は小さなTDD Kataでこの方法論を試したことがありますが、とてもうまくいっています。しかし、私は今フリーで働いているので、実際に試したとは言えませんね。

appstore
googleplay
会員登録
URLからPICKする

会員登録して、もっと便利に利用しよう

  • 1.

    記事をストックできる
    気になる記事をPickして、いつでも読み返すことができます。
  • 2.

    新着ニュースをカスタマイズできます
    好きなニュースフィードをフォローすると、新着ニュースが受け取れます。