Connascence:コードの結合度を測るもうひとつの指標

構造は、凝集度が高く、結合度が低い場合に安定する - Larry Constantine

私たちプログラマーは、その仕事において、できる限り良いコードを書きたいと考えます。しかし、「良いコードとは何か」という問いに対して答えるのは、そう簡単ではありません。「良さ」を測るには、「何に対して」という軸が必要であり、その軸は一つではなく、さらには、コードを書いている状況に応じて、大事にすべき軸が変わるということも往々にしてあるからです。そうしたとき、私たちは何らかの尺度でもってコードを測って、そのときのコンテキストにおいて良い落とし所を定めます。

そのようなときに、コードの品質を測る軸としては、有名なものには「凝集性(Cohesion)」と「結合度(Coupling)」があります。ここでは、そのうちの「結合度」を測る指標の一つとして「コナーセンス(Connascence)」を紹介します。

コードの結合度を測る指標

コードの結合度を測る指標としては、『高信頼性ソフトウェア : 複合設計』や『ソフトウェアの複合/構造化設計』などでGlenford Myersによって提唱されたモジュール結合度が有名です。

https://ja.wikipedia.org/wiki/結合度

しかし、モジュール結合度は、現在のプログラミング言語ではすでに問題にしずらかったり、読み替えたりしないと理解が難しい尺度なども混ざっていたりして、今の私たちが尺度にするには少し難しくなってしまっています。

また、他に代表的な結合度の指標としては、『ソフトウェアの構造化設計法』においてEdward Yourdon と Larry Constantineが定義した「主系列からの距離(Distance from the main sequence)」があります。

ソフトウェアのメトリクスの基本的なところ

「主系列からの距離」は、コンポーネントの「不安定性」(「求心性結合(依存入力数、ファンイン)」と「遠心性結合(依存出力数、ファンアウト)」から求められる)と「抽象度」の理想的なバランスからの距離を求めるもので、それによってコンポーネントの依存度や抽象度が適切かが測れます。これは良い指標の一つではありますが、これはこれで、私たちがコードを見ただけで導き出すことは難しいため、コードを書いているときなどに意識する尺度としては少々扱いづらいところもあります。

これらと比べて、「コナーセンス(Connascence)」は、私たちプログラマーがコードを評価したり、リファクタリングする際に比較的意識しやすい尺度が定義されているものであり、その割に知名度が低いと感じるもののため、ここで紹介しようと考えました。

コナーセンス

コナーセンス(Connascence)とは、1992年にMeilir Page-Jones によって考案されたソフトウェア品質指標で、それまでの結合メトリクスを改良して、オブジェクト指向言語用に再構築したものとなります。

https://en.wikipedia.org/wiki/Connascence

コナーセンスという用語は

two components are connascent if a change in one would require the other to be modified in order to maintain the overall correctness of the system (システムの全体的な正しさを維持するために、一方の変更が他方の変更を必要とする場合、2つのコンポーネントはコナーセントである)

という文章からきており、コナーセント(connascent)とは「con- + nascent (「共に」「出現する」)という意味になります。

コナーセンスの特徴

コナーセンスでは、以下の3つの尺度(次元)を用いて、コードの品質を評価します。

https://programhappy.net/wp-content/uploads/2020/06/connascence-properties-diagram.png

引用: https://programhappy.net/2020/06/22/connascence-why-is-it-so-important/

度合い(Degree)

どれだけ結合されているか。結合の度合いが強い(高い)ほど、変更の難易度およびコストが高くなる

強度(Strength)

コナーセンスには種類(後述)に応じて強度があり、強いコナーセンスほど変更の難易度とコストが高くなる

距離(Locality)

関係する要素がどれだけ近いか。同じ強度と度合いのコナーセンスでも、関係する要素が離れているほど、変更の難易度とコストが高くなる

結合といって一般に想像される度合い(degree)に加えて、強度(strength)と距離(locality)という次元を持たせているところが、コナーセンスの大きな特徴となります。

コナーセンスを数値として計算する場合には、以下のような式で数値を検出します。

Strength x Degree / Locality

コナーセンスの種類

強度のところで触れたコナーセンスの種類には、次のようなものがあります。これらは、コンポーネントが何によって結びついているかに従った分類となっています。

  • 静的なコナーセンス: コードレベルで生じる結びつき
    • 名前のコナーセンス Connascence of Name (CoN)
      • 他方のコンポーネントが持つ要素の名前(メソッド名など)を参照している状態
    • 型のコナーセンス Connascence of Type (CoT)
    • 意味のコナーセンス Connascence of Meaning (CoM)
    • 位置のコナーセンス Connascence of Position (CoP)
      • メソッドのパラメータ順序などを知っている必要がある状態
    • アルゴリズムのコナーセンス Connascence of Algorithm (CoA)
  • 動的なコナーセンス: 実行時に生じる結びつき
    • 実行順序のコナーセンス Connascence of Execution (CoE)
      • 他方のコンポーネントを利用するにあたって、メソッドなどの実行順を知っている必要がある状態
    • タイミングのコナーセンス Connascence of Time (CoT)
      • 他方のコンポーネントを利用するにあたって、実行タイミングなどを知っている必要がある状態
    • 値のコナーセンス Connascence of Value (CoV)
      • コンポーネントが持つ値同士が関連していて、同期をとって管理する必要がある状態
    • アイデンティティのコナーセンス Connascence of Identity (CoI)

これらは、上のコナーセンスほど結びつきの強度が弱く、下にいくほど強度が強い、という関係になっており、できるだけ弱いコナーセンスに向けてリファクタリングするのが良いとされています。

https://res.cloudinary.com/practicaldev/image/fetch/s--i1piwW4k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/olhdijilxlv9b5txzkj6.png

参考 https://dev.to/edisonnpebojot/architecting-modules-for-software-modularity-part-2-3bpc

コナーセンスをどのように意識するか

ここからは、具体的なコードを見ながら、コードを書くときにどのようにコナーセンスを意識するかの例を見ていきましょう。

例1: 意味のコナーセンスを名前のコナーセンスに変換して、コナーセンス強度を下げる

たとえば、以下のようなコードがあったとします。

class Itemdef price(tax_included=false)
    if tax_included
       price * TAX
    else
       price
    end
  endend

この Item#price を利用するには、引数に渡す truefalse の意味を利用者側のコンポーネントが知っている必要があります。この状態は、意味のコナーセンスが生じている状態といえます。

これを、名前を知っていればメソッドを利用できる状態にしましょう。

class Itemdef price_including_tax
    price * TAX
  endend

これで、メソッド名さえ知っていればコンポーネントを利用できるようになりました。

例2: 実行順序のコナーセンス→位置のコナーセンス→名前のコナーセンス

たとえば、以下のようなコードがあったとします。

email = Mail.new
email.recipient "foo@example.com"
email.sender "me@example.com"
email.subject "Hi"
email.body "Hello"
email.send

このとき、たとえば Mail#recipientMail#subject といったメソッドは Mail#send の手前で呼び出す必要があるとします。この場合には、 Mail と利用者側のコンポーネントには実行順序のコナーセンスが生じている状態といえます。

これは、手続きが個々のメソッドに分かれていることが原因ですので、まずは手続きをひとまとめにしましょう。

class Mail
  ...
  def send(from, to, subject, body)
     ...
  end
end

これで、実行順序を気をつけなくてもメソッドを利用できるようにはなりました。

email.send("foo@example.com", "me@example.com", "Hi", "Hello")

しかし、今度は引数の順序を知らなければコンポーネントを正しく利用できません。この状態は、 Mailと利用者側のコンポーネントには位置のコナーセンスが生じている状態といえます。

次は、このメソッドにパラメータ引数を導入して、位置を知らなくてもメソッドを利用できるようにしましょう。

class Mail
  ...
  def send(from:, to:, subject:, body:)
    ...
  end
end

これで、位置を知らなくてもメソッドを利用できるようになりました。

email.send(
  from:    "foo@example.com",
  to:      "me@example.com",
  subject: "Hi",
  body:    "Hello"
)

この状態は、メソッド名とパラメータ名を知っていればメソッドを利用できるので、名前のコナーセンスがある状態といえます。

コナーセンスを設計のものさしに使う

どうだったでしょうか。もちろん、コナーセンスを知らなくても、自然と上記のような設計判断をするよという方もいるとは思います。ですが、コナーセンスの概念を用いることで、上記のような判断をある程度しっかりとした軸をもって進められそうな感じがしないでしょうか。少なくともモジュール結合度よりは今のコンテキストに素直にのせて評価しやすい指標ではないかなと考えます。

また、上記の例では、コナーセンスの種類(強度)だけにフォーカスしていましたが、ここに距離や度合いの観点も踏まえて考えると、さらにより良い判断のヒントが得られるだろうと考えています。

たとえば、位置のコナーセンスが生じていたとしても、2個の引数の場合と7個の引数の場合とでは、その度合いが異ります。あるいは、メソッドが同じファイル内からしか利用できない場合と、自分たちがメンテナンスしていないコードから利用される場合とでは、その距離が異なります。こうしたことを踏まえて、じゃあどのコナーセンスで十分としようか、といった具合です。

コナーセンスは他の尺度と比べるとだいぶマイナーなコンセプトで、日本語書籍で扱っているのも今のところは『Adaptive Code ~ C#実践開発手法 第2版』 (日経BP)くらい、英語圏でもそこまで流通はしていなさそうです。僕が調べた中では、RubyistにはおなじみのJim Weirichが2012〜2013年あたりに講演している内容がもっとも充実していると感じました。よければ時間のあるときにでも眺めてみてもらえたら嬉しいです。

www.youtube.com

以上で、簡単なコナーセンスの紹介を終わります。この記事が、より良い設計のヒントになれば幸いです。

参考文献

What Every Programmer Should Know About Object-Oriented Design

What Every Programmer Should Know About Object-Oriented Design