見出し画像

蛇の悪魔を寄せ付けないクラス設計


概要

 今回は退魔の本の3章の勉強をする。各論に入っていきなり最初がクラス設計である。やはり筆者はクラスに良い思い出が無いのだろうか。意外にも、リーダブルコードにはクラス設計に関する記述はない。そのため、主に退魔の本のクラス設計指針を満たすためには、ロブストPythonのどの技術を使えばいいかの対応となる。Pythonには頑丈なクラスを作るための機能がいくつもある。それでも悪魔を呼ぼうと思えば呼ぶことができるが、少なくとも悪魔を呼びにくくすることは大切である。
 今回、退魔の書の3.1節、3.2節の一部について考察する。他の部分は自明である部分や他との被りが多いため、割愛する。章・説を網羅することではなく概念を網羅することが重要なためだ。

3.1 クラス単体で正常に動作するようにする

 この章で特筆すべきことは二つ、データクラスに対する考えの違いと「自己防衛責務」の概念である。

データクラス

 まずデータクラスについて、退魔の本での記述とPythonでの実装の違いについてまとめる。退魔の本で、データクラスはメンバ変数とメンバメソッドを別々で実装することで低凝集を招くとしている。しかし、ロブストPythonの9章によると、Pythonのデータクラスは「データの集まり」という概念ではなく、datalcassデコレータを付けて一部の機能を追加実装したクラスである。それゆえ、Pythonのデータクラスにはメンバメソッドを実装できるため、低凝集を回避できる

自己防衛責務

 次にクラスの自己防衛責務についてである。自身のことは自身で管理する。これは単純にクラスメソッドを実装することにとどまらず、不正値の検出などを自前でやることを含む。
 これはリーダブルコードの11章「一度に一つのことを」にも共通する部分があるように思える。すなわち、あるクラスに関連することはすべてクラス内で完結させることにより、実際に使用する際にはクラスメンバ変数の処理が結果的にまとめられる
 また、これらはロブストPythonの9章「データクラス」と10章「クラス」の各論は言わずもがな、16章2.1節「物理的依存関係」、同2.2節「論理的依存関係」にも重要な記述がある。9章、10章の内容は以降詳細に取り扱う。16章で説明される依存関係の一部は、ちょうど自己防衛責務に反するクラス設計指針として、複数クラスの依存関係を煩雑にしてしまった時に起こるであろう弊害を記述している。あるクラスがなすべき仕事を他のクラスに任せてしまうことでメンテナンス性が落ち、悪魔を呼ぶ構造になる

3.2 成熟したクラスへ成長させる設計術

コンストラクタによる正常値設定

 退魔の本では不正値の検査をコンストラクタで行うべきとしている。Pythonではクラスのコンストラクタの役割は__init__特殊メソッドが、データクラスでは変数定義そのものが担う。しかし、ここで留意すべきなのは、データクラスでは不正値の確認などが行えないことである。ロブストPython10章2節には不変式に関する解説がある。すなわち、そのクラスインスタンスが生きている間は必ず守り通さねばならない決まり事である。ロブストPythonではピザオーダーシステムに100インチのピザを注文するような不正値を渡す例を示しているが、データクラスではこれを排除することができない。それゆえ、不正値の検査を初期化段階で要するならデータクラスで実装すべきではないことを示している。

不変で思わぬ動作を防ぐ

 退魔の本ではインスタンス変数の上書きを避けるべきとしている。その理由として、値がいつ書き変わったのかの追跡が困難になり、それが結果的に悪魔を呼ぶ。退魔の本ではその代替手法として次の節で新しいインスタンス生成を推奨している。これはリーダブルコード9章3節「変数は一度だけ書き込む」でも似た概念が述べており、「生きている変数」が少なければいいとしている。
 ロブストPythonではこの手段として、4章6節で述べられるFinal型、9章2.4節でのデータクラスのイミュータブル化、10章3.2節で述べられるアクセス制御が用意されている。Final型はそのスコープでの書き換えをしようとするとエラーになるが、関数スコープ内での書き換えは検出できない。データクラスはfrozen=trueを指定するとdataclassのフィールドが変更不可能になる(インスタンスの再代入は受け付ける)。クラスのアクセス制御はプライベート変数を(一見すると)書き換え不可能にする。厳密にはできなくはないが苦労する。それゆえ、
①-1 データクラスは極力frozenにする。
①-2 クラスのメンバはプライベートにし、__init__特殊メソッドによる初期化とpropertyメンバによる値取得のみを許容する。
②インスタンスはFinal型にしておく

を徹底することで、悪魔を寄せ付けにくいクラスになるだろう。
 留意すべき点として、リーダブルコード9章全体では気にすべき変数が多いと混乱を招くとしていて、これは新しいインスタンス生成のトレードオフとなりうる。解決手段としてスコープを上手く使う方法が述べられているが、Pythonのスコープは関数内のスコープだけであり、C++やRustのように{}のスコープを抜けたら変数が寿命を迎えることがない。つまりある関数スコープ内では変数は宣言後、ずっと生きたままになる。その点において、9.2節で述べられているように定義の位置を極力下げつつ、2章(特に2節)で述べられているように中間変数名に情報を詰め込むことで、名前だけでどのような処理を行っているか分かり易い中間インスタンス変数を生成することで、混乱の少ないコーディングをすべきである。

値の渡し間違いを型で防止する

 型による値の検査は非常に良いものだ。Pythonは動的型付け言語なので実行時まではエラーを出してくれないが、それでも助かる場面はある。この項は「型によるエラーで、本来渡されるべきでない値が渡されることを防ごう」というものである。例えば、チケットの個数と値段を両方ともint型にしていると、それを取り違えて代入してもインタープリタはそれ自体に対してエラーを出さない。退魔の書は値に対して独自の型を定義することを推奨している
 この一部は8章の列挙型Enumで解決できる。Pythonの一部のライブラリは、関数の動作を変えるために文字列を渡すことがある(matplotlibのax.plot()関数のline_styleなど)が、これは本来悪魔を呼ぶ構造だろう。渡されるべき内容は選択肢が限られているため、人間に自由にさせるべきではない。選択肢が限られているのであれば、そのことをEnumを介して知らせるべきだ。
 しかしEnumの誤用は別な悪魔を招く。ロブストPythonの8章3.3節には特殊な列挙型であるIntEnumを継承したばかりに、本来比較されるはずのない値が比較されたがゆえに呼び出された悪魔の例がある。しかもこれは例外やエラーを出さないため、ひっそりとプログラムをむしばむ。IntEnumは極力使われるべきではない。
 厳密にはこの節ではないが、3章4節で言葉だけ出ているファーストクラスコレクションについてはロブストPythonの5章4節で新しいコレクション型の作り方が参考になる。値の渡し間違いの対策として値の独自型(3章4節では値オブジェクトと呼んでいる)があるように、コレクション型の渡し間違い対策としてファーストクラスコレクションを渡すように述べられている。しかしPythonはdict型などの継承が推奨されていない。そのため、ユーザー定義のコレクションの作り方が本節で述べられている。

現実の営みにないメソッドを追加しない

 退魔の書では不必要なメソッドをむやみやたらに追加すると、予想外な使われ方をすると述べている。これはリーダブルコードの13章1節、13章3節に通ずるところがある。これらの節では「実装に必要な時間を過小評価しないこと」、「コードは短いほうが読みやすい」という、当たり前だがクラス設計をするときに調子乗って見落としがちなことが書かれてある。クラスを短時間で、短く仕上げようと努めていれば不必要なメソッドは追加されないはずだ

まとめ

  • クラスは__init__メソッドでのみ値の設定をすべき。

  • インスタンスはFinal型にする。

  • 値の書き換えは新しいインスタンス生成で行う。

  • 値オブジェクトやファーストクラスオブジェクトを使って値の入れ間違いを防ぐ。

余談

 ロブストPythonにpropertyデコレータのことが記述されていないのは意外である。アクセサとしてはかなり優秀だと思うのだが、なんで???

いいなと思ったら応援しよう!