はじめに
積読になりかけていた良いコード悪いコードで学ぶ設計入門を読了した。
私はこれまで設計に関する本としては自走プログラマーやリーダブルコードなどを読んできたが、この本では主にドメイン駆動開発の考え方に基づいて、保守性、特に変更容易性の向上を目的とした設計手法を取り扱っている。
変更容易性を高めることは、ソフトウェアの成長性を高めることなのです。ソフトウェアの成長性を高めることが、本書の意義です
(15章 設計の意義と設計への向き合い方)
大規模開発における数万行のコードを長期的に成長可能なものにするための設計手法の基礎がクラス設計、メソッド設計、命名、SOLID原則、エンジニア組織など広い領域にわたって記載されており、読み物として非常に面白かった。しかし、かといって自分の技術力が爆上がりするかというと、そんなことはない。技術というのはコードを書いて試しながら身につけていくものであり、本を読んだだけでは一朝一夕に身につかないものであると感じた。(本の中にもインプット2割アウトプット8割を意識せよとある)(8割はキツくない?)
というわけで少しでも覚えたての設計手法を手に馴染ませるため、Javaで書かれた本書のコードを私の業務上利用しているPythonで書くとどうなるのかやってみた。
値クラスの設計
今回は6章におけるinterfaceの実装をPythonで書き換える。
抽象基底クラスによるInterfaceの実装
JavaのinterfaceはPythonには存在しないため、抽象基底クラスabcを利用する。
pythonで基底クラスをつくるabcにはabstractmethodデコレーターがあり、これで修飾されたメソッドとプロパティのすべてがオーバーライドされていない限り、インスタンス化できなくなる。
class Magic(ABC):
@abstractmethod
def cost_magic_cost(self) -> MagicCost:
pass
@abstractmethod
def attack_power(self) -> AttackPower:
pass
@abstractmethod
def technical_power(self) -> TechnicalPoint:
pass
abstractmethod指定されたメソッドを1つでもオーバーライドしていない継承クラスの場合、インスタンス化がTypeErrorとなる。
class Mera(Magic):
def cost_magic_cost(self) -> MagicCost:
return super().cost_magic_cost()
def attack_power(self) -> AttackPower:
return super().attack_power()
#technical_powerの実装がない
class TestMagic:
def test_abstractmethod(self):
with pytest.raises(TypeError):
mera = Mera() #TypeErrorになる
以上のように、abc.abstractmethodデコレーターにより必要なメソッドが定義されていないクラスの実装を防げる。
dataclassによる不変
加えて、攻撃魔法を実装する際にはdataclassを利用し、frozen=Trueとすることで、メソッドやプロパティを不変にできる。
@dataclass(frozen=True)
class Fire(Magic):
member: Member
def cost_magic_cost(self):
return MagicCost(2)
def attack_power(self):
value = 20 + self.member.level
return AttackPower(value)
def technical_power(self):
return TechnicalPower(0)
データクラスにして、frozen=Trueとすることで不変にできる。
例えば、attack_powerに代入を実行しようとすると以下のエラーが出る。
E dataclasses.FrozenInstanceError: cannot assign to field 'attack_power
しかし本当の本当に不変なわけではなく、以下のようにすれば変更は可能。
object.__setattr__(fire, "attack_power", 0)
さらにイミュータブル型の場合には、削除や追加、変更が可能になってしまう。
というわけで、frozenは不変性を模倣しているだけであって、変更される可能性がなくなったわけではないことには注意が必要。
参考