コナーセンス(Connascence)について調べてみた

Connascence強度について

こんちは、最近spotifyでLofi-Hiphopばっかり聴いてます

GWに入る前ぐらいから、ソフトウェアアーキテクチャの基礎 ―エンジニアリングに基づく体系的アプローチ良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方をじっくり読んでましたが、前者の書籍の中で コナーセンス(Connascence) という言葉を初めて聞いて少し調べたので自分なりにまとめます

コナーセンス(Connascence)とは?

1996年に Meilir Page-Jones が書籍の中で紹介したのが最古とされてるようですが、1992年に同氏がACMの記事の中で既に紹介していたようです。

該当のACMの記事は検索すると見つけることができました。

Comparing techniques by means of encapsulation and connascence | Communications of the ACM

この記事では構造化プログラミングとオブジェクト指向プログラミングをカプセル化とConnascenceの両軸から比較した考察が記載されており、その中で結合度と凝集度という2つの指標を統合させてConnascenceという言葉を生み出しています(今の時代だと構造化プログラミングがについてワイワイ論じられるのをみたことがあまりないので記事自体がとても面白いですね)

Connascenceは、とあるコンポーネントAへの変更時に全体的な正確さをもたらすためにコンポーネントBに対しても変更が必要となればそれらのコンポーネントはconnascentであり、これらの結びつきが強いほどConnascenceが強いと定義されています。

例えば以下のAとBの間にもある種のConnascenceが存在しています。

let i = 0;  // A
i = 1;      // B

また、1996年のWhat every programmer should know about object-oriented designでは、Connascenceを表す3つのメトリクスが導入されています。

  • Strength
  • Degree
  • Locality

ここでは、DegreeとLocalityは難しい概念ではなかったので置いておき、Strengthについて整理していきたいと思います。

Strengthについて

StrengthはConnascenceの強度(わかりにくいので以後、Connascence強度とします)を表します。Connascence強度は9段階に分類されています。

1.Connascence of Name (CoN)

最も弱いConnascenceの強度です。省略してCoNとも呼ばれるそうです。

これは名前をもとにして結合している状態を表しています。

# reference: https://connascence.io/name.html
class Request:

    def __init__(self, url, data=None, headers={},
                 origin_req_host=None, unverifiable=False,
                 method=None):
        pass

    def set_proxy(self, host, type):
        pass

例えば上記のようなクラスを別のクラスから呼び出すときには Request__init__set_proxyという名前をもとに呼び出す必要がありますが、これがCoNに当てはまります。 しかし、特定のコンポーネントAが別のコンポーネントBに依存する場合はこれ以上弱い強度で結合することはできません。逆にいうと他のConnascence強度からこのCoNにすることで強度が弱くなるということになります。

例えばset_proxyという関数名を変更するとそれを利用する箇所も同様に変更する必要があるためConnascenceとして列挙されているのだと思いますが、このような変更はたいがいIDEによって効率的にできるので、最も弱い強度であるというのは納得です。

また、個人的にはプログラミングでのクラス名やメソッド名はラベルとしての意味を持っていると考えていて、それを適切に使っているだけだということだと思うので全く何も問題はないように感じました

2.Connascence of Type (CoT)

CoNの次に強いConnascenceの強度で、これは型をもとに結合している状態を表します。これも省略してCoTとも呼ばれるそうです。

// reference: https://connascence.io/type.html
std::string cost;  

cost = 10.95; // OOPS!

具体的には上記のように定義された型通りに代入する必要がある場合にCoTが発生しているとみなされるようです。型が変わるとその型を利用している箇所も変える必要があるので当然ですね。

ただ、個人的にはこのConnascence強度はCoNと同等レベルだと思っていて、この状態になっていても特に気にしなくても良いのではないかと思っています。(TypeScriptやりすぎてそう思うだけかもしれない…)

もうちょっとBadな状態になっている例が欲しいですね🤔

3.Connascence of Meaning (CoM)

CoTの次に強いConnascence強度でこれは意味をもとに結合している状態を指します。他と同様にCoMと呼ばれます

# 実装コード
# reference: https://connascence.io/meaning.html

def is_credit_card_number_valid(card_number):
    # Check for 'test' credit card numbers:
    if card_number == "9999-9999-9999-9999":
        return True
    # Do normal validation:
    # ...

上記の実装コードのように、クレジットカードのカード番号のバリデーションを実装する際にテスト用にpassするカード番号として9999-9999-9999-9999を定義した場合、この9999-9999-9999-9999というカード番号の意味(テスト用にpassするカード番号であること)をシステム全体で知っている状態になっています。そうなると以下のテストコードのように突然9999-9999-9999-9999という番号が出てくることになり、仮にこの番号を変えようとすると複数箇所の修正が必要になります。

# テストコード
# reference: https://connascence.io/meaning.html

def test():
    # ...
   result = is_credit_card_number_valid("9999-9999-9999-9999")
    # ... 

例えばこれは以下のように変数に定義しそれぞれから参照することで、CoMからCoNに変化させ、Connascence強度を弱くすることができます。

# どこか
TEST_PASS_CARD_NUMBER = "9999-9999-9999-9999"

# 実装コード
def is_credit_card_number_valid(card_number):
    # Check for 'test' credit card numbers:
    if card_number == TEST_PASS_CARD_NUMBER:
        return True
    # Do normal validation:
    # ...

# テストコード
def test():
    # ...
   result = is_credit_card_number_valid(TEST_PASS_CARD_NUMBER)
    # ... 

4.Connascence of Position (CoP)

CoMの次に強いConnascenceの強度で、CoPとも呼ばれます。これは位置に対して結合している状況を表します。

# reference: https://connascence.io/position.html
def get_user_details():
    # Returns a user's details as a list:
    # first_name, last_name, year_of_birth, is_admin
    return ["Thomas", "Richards", 1984, True]       # A

def launch_nukes(user):
    if user[3]:                                     # B
        # actually launch the nukes
    else:
        raise PermissionDeniedError("User is not an administrator!")

上記のようなコードでは、ユーザーが管理者かどうかという情報が配列の4つめに格納されており、その情報が AとBで利用されています。これが位置に対して結合しているということであり、このような状態では例えばユーザーの生まれ年の情報(配列の3つ目に格納されている)が不要になった際に、生まれ年の情報とは直接関係のないAとBの両方を修正する必要があります。

これは、さらに弱いConnascensce強度であるCoNに変化させることができます。具体的には以下のようにis_adminというプロパティ(Name)をもとにアクセスするようにします、こうするとユーザーの生まれ年の情報が不要になった場合でもAとBの両方は修正する必要がありません

def get_user_details():
    # Returns a user's details as a list:
    # first_name, last_name, year_of_birth, is_admin
    return {
        "last_name": "Thomas",
        "first_name": "Richards",
        "birth_year": 1984,
        "is_admin": True          # A
    }

def launch_nukes(user):
    if user.is_admin:             # B
        # actually launch the nukes
    else:
        raise PermissionDeniedError("User is not an administrator!")

5.Connascence of Algorithm (CoA)

CoPの次に強いConnascenceの強度で、CoAと呼ばれます。アルゴリズムが結合している状況を表します。

アルゴリズムというのがパッと想像できませんね。具体的には、以下のようにキャッシュからデータを読み書きする処理AとBで結合が発生しているとみなされます。なぜならそれらにはキャッシュファイルをutf-8でエンコード/デコードするという暗黙の制約があり、utf8からsjisに変更する際にはAとBの2ヶ所に対して修正が必要なためです。

# reference: https://connascence.io/algorithm.html
def write_data_to_cache(data_string):
    with open('/path/to/cache', 'wb') as cache_file:
        cache_file.write(data_string.encode('utf8')) # A

def read_data_from_cache():
    with open('/path/to/cache', 'rb') as cache_file:
        return cache_file.read().decode('utf8')      # B

少なくとも以下のように変更すればよりConnascenceの弱いCoNにすることができます。

cache_file_encoding = 'utf8'

def write_data_to_cache(data_string):
    with open('/path/to/cache', 'wb') as cache_file:
        cache_file.write(data_string.encode(cache_file_encoding)) # A

def read_data_from_cache():
    with open('/path/to/cache', 'rb') as cache_file:
        return cache_file.read().decode(cache_file_encoding)      # B

6.Connascence of Execution (CoE)

CoMの次に強いConnascenceの強度で、CoEとよばれます。実行順序の結合を表していて、例えば以下のような、Repositoryを使ってエンティティを保存する場合、A/B/Cの間の実行順序はA→B→Cとなっていなければなりません。例えば**他の処理の制約上でBの処理が移動した際、そのBの位置に従ってAやCを移動させる必要があります。**これはA/B/Cの間で実行順序が結合しているとみなされます。

const saveNewUser = (user) => {
    user.dateOfBirth = new Date(1980,0,1,1,1)                  // A
    user.age = differenceInYears(new Date(), user.dateOfBirth) // B
    userRepository.save(user)                 // C
}

しかし、個人的には上記で挙げたコードのような順序に依存する処理というのは世の中にいくらでも存在すると思っていますし、上記のようなsave処理とそのための事前準備処理は分離することで得られるメリットも大きいと感じます。また、これが世間的に特段と問題になっていないのは、これら一連の処理が同一スコープ内に存在するためじゃないかと感じます。

例えば上記コードのような結合状態自体は、同一のメソッド内であれば距離(Locality)が近く実行順序を自然と意識することができますが、逆に以下のように別々のメソッド間で結合状態が発生していると、その順序性をメソッド名から推論できず意図せずして不具合を生むの可能性が高いのではないかと思います。なので、このConnascence強度はLocalityと強く関連性があるのではないかと思います。

const setDateOfBirth = (user) => {
    user.dateOfBirth = new Date(1980,0,1,1,1)                  // A
}
const setAge = (user) => {
    user.age = differenceInYears(new Date(), user.dateOfBirth) // B
}
const saveNewUser = (user) => {
    setDateOfBirth(user)
    setAge(user)
    userRepository.save(user)                                  // C
}

7.Connascence of Timing (CoT)

CoEの次に強いConnascenceの強度で、CoTと呼ばれます。実行タイミングによって結合している状況を表します。

ネットでは例があまりなさそうだったんですが、これはフロントエンドでいうとsetTimeoutを利用している時によく発生している結合状態ではないかと思いました

例えばフロントエンドのVue.jsで「アカウントリストを取得した後に画面のローディングを非表示にする」という要件を実現するために、以下のように3秒立ったらデータの取得が終わっているだろうという想定で書かれているコードをみかけます。しかしこれは例えばネットワークの回線が弱くて3秒経ってもアカウントリストを取得できなかった場合にはローディングが非表示になってしまいます。このときのAとBの間にはCoTが存在しているのではないかと感じました。(そもそもこんなコードコードをプルリクエストで挙げてきた場合は普通はレビューで弾くと思いますが

export default {
    data() {
        return {
            accountList: [],
            isLoading: true
        }
    },
    async created() {
        setTimeout(() => {
            this.isLoading = false                  // A
        }, 3000)
        this.accountList = await fetchAccountList() // B
    }
}

8.Connascence of Value (CoV)

CoTの次に強いConnascenceの強度で、CoVと呼ばれ、値が結合している状態を指します。

# reference: https://connascence.io/value.html
class ArticleState(Enum):
    Draft = 1
    Published = 2


class Article(object):

    def __init__(self, contents):
        self.contents = contents
        self.state = ArticleState.Draft # A

    def publish(self):
        self.state = ArticleState.Published

上記のコードに対するテストコードは以下のようになりますが、このコードではAとBで値が結合している状態になっています。なぜかというと、ArticleStateの初期状態がDraftであるということをAとBの両方が暗黙的に知っているためです。

# reference: https://connascence.io/value.html
article = Article("Test Contents")
assert article.state == ArticleState.Draft # B
article.publish()
assert article.state == ArticleState.Published

このような結合状態は、初期状態がDraftではなくPublishedやあるいは他のStateになった場合にAとBの両方の変更を必要とします。

これは以下のように初期状態を表すStateを用意しそれをAおよびBで利用することで、よりConnascence強度の弱いCoNに変化することができます。

class ArticleState(Enum):
    Draft = 1
    Published = 2
    InitialState = Draft


class Article(object):

    def __init__(self, contents):
        self.contents = contents
        self.state = ArticleState.InitialState

個人的にはこのConnascence強度を持つ実装はよくやってしまっているなと感じました。例えば上記の例だと実装しているときはDraftが初期状態なのは自明だと感じてしまいますが、本当にいつでもそれが成り立つのか、都度注意する必要がありますね

9.Connascence of Identity (CoI)

CoVの次に強いConnascenceの強度で、CoIと呼ばれてます、これに関してはソフトウェアアーキテクチャの基礎 ―エンジニアリングに基づく体系的アプローチにも以下のようにしか記載がなく、調べてみましたがいまいち理解できませんでした。おそらくですが特定のクラスから生み出されるインスタンス全てに依存しているのではなく、その中の一部のインスタンスに依存している状況CoIに該当すると思われますが詳しくは不明です。

このコナーセンスは、複数のコンポーネントが、あるエンティティの特定のインスタンスに依存しているときに発生する。

Mark Richards, Neal Ford 『ソフトウェアアーキテクチャの基礎 ―エンジニアリングに基づく体系的アプローチ』(オライリージャパン、2022)

CoIをコード付きで解説している書籍があるようなので興味のある人は購入すればわかるかもしれません

https://subscription.packtpub.com/book/business-and-other/9781838980849/19/ch19lvl1sec130/connascence-of-identity

所感

整理してみた所感

  • おおまかには、Connascence強度が強くなっていくにつれて、リファクタリングの難易度やコストが上がっていくだろうと思った
  • 個人的にConnascenceの強度弱い順を考えると以下のような順番かなと思った
    • 1.Connascence of Name / Connascence of Type
    • 2.Connascence of Meaning
    • 3.Connascence of Execution
    • 4.Connascence of Position
    • 5.Connascence of Algorithm
    • 6.Connascence of Value
    • 7.Connascence of Timing
    • (不明)Connascence of Identity
  • コードレビューの時にガンガン使っていこうと思う
  • コードの品質をConnascence強度だけで判断してはだめで、LocalityやDegreeも十分加味する必要があると感じた
    • 例えば Connascence of Execution はLocality次第で大きくリファクタリングの難易度やコストも変わると感じたので
tech  設計 

See also