4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

「良いコード悪いコードで学ぶ設計入門」を読んだので不変な値オブジェクトをPythonで実装してみる

Posted at

はじめに

積読になりかけていた良いコード悪いコードで学ぶ設計入門を読了した。
私はこれまで設計に関する本としては自走プログラマーやリーダブルコードなどを読んできたが、この本では主にドメイン駆動開発の考え方に基づいて、保守性、特に変更容易性の向上を目的とした設計手法を取り扱っている。

変更容易性を高めることは、ソフトウェアの成長性を高めることなのです。ソフトウェアの成長性を高めることが、本書の意義です
(15章 設計の意義と設計への向き合い方)

大規模開発における数万行のコードを長期的に成長可能なものにするための設計手法の基礎がクラス設計、メソッド設計、命名、SOLID原則、エンジニア組織など広い領域にわたって記載されており、読み物として非常に面白かった。しかし、かといって自分の技術力が爆上がりするかというと、そんなことはない。技術というのはコードを書いて試しながら身につけていくものであり、本を読んだだけでは一朝一夕に身につかないものであると感じた。(本の中にもインプット2割アウトプット8割を意識せよとある)(8割はキツくない?)
というわけで少しでも覚えたての設計手法を手に馴染ませるため、Javaで書かれた本書のコードを私の業務上利用しているPythonで書くとどうなるのかやってみた。

値クラスの設計

今回は6章におけるinterfaceの実装をPythonで書き換える。

抽象基底クラスによるInterfaceの実装

JavaのinterfaceはPythonには存在しないため、抽象基底クラスabcを利用する。
pythonで基底クラスをつくるabcにはabstractmethodデコレーターがあり、これで修飾されたメソッドとプロパティのすべてがオーバーライドされていない限り、インスタンス化できなくなる。

magic.py
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の実装がない
test_magic.py
class TestMagic:
    def test_abstractmethod(self):
        with pytest.raises(TypeError):
            mera = Mera() #TypeErrorになる

以上のように、abc.abstractmethodデコレーターにより必要なメソッドが定義されていないクラスの実装を防げる。

dataclassによる不変

加えて、攻撃魔法を実装する際にはdataclassを利用し、frozen=Trueとすることで、メソッドやプロパティを不変にできる。

magic.py
@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は不変性を模倣しているだけであって、変更される可能性がなくなったわけではないことには注意が必要。

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?