45
27

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.

死んでも破壊的操作をしない

Last updated at Posted at 2023-08-20

エンジニアが学ぶべきスキルやノウハウは数多くありますが、その中でもとくに重要だと感じるものとして「破壊的操作」の回避が挙げられます。

破壊的操作とは?

この記事における「破壊的操作」とは、コーディングにおける次のようなものを指しています。

  • ある関数に渡した引数(参照渡しされた引数)の中身が変わってしまうこと
  • setterメソッドにより、そのオブジェクトが持つ変数の中身が変わってしまうこと
  • 配列の要素が変わってしまうこと

破壊的操作とは「想定していない変更」である

では、なぜ破壊的操作をしてはいけないのでしょうか。

私の言葉で説明するなら、破壊的操作は「想定していない変更」だからです。

たとえば次のようなコードを、作者以外が読んだとします1

コードを読んでいる人物の心の声を書いてみましょう。

実例1

以降の実装例は、読みやすさを考慮してコードを省略しています。
そのまま実行してもエラーになることをご容赦ください。

user = User(name="コボリ アキラ")
# 心の声「ここでUserインスタンスが作成されるのか。名前は半角スペースつきで入っている」

# 10行程度の処理...

user.remove_half_space() # ここで半角スペースを取り除く「破壊的操作」がある

# 10行程度の処理...

print(user.name) # -> コボリアキラ
# 心の声「あれ? なんで半角スペースがないんだ? コンストラクタで取り除いてる訳でもないぞ?」

コードを書いた人にとっては自明です。わざわざremoveHalfSpaceとまで書いているのですから。

しかし読んでいる人が気づくかは別です。

そしてこのような破壊的があることがわかると、コードを読む側は「インスタンスの中身は最後までわからない。つまりコードをすべて読まないかぎりは、このインスタンスの中身は確定しないのだ」と学習します。

「想定していない変更」があることで、プロダクトを理解するための労力は一気に跳ね上がります。

実例2

同じく「引数が変わった場合」も見てみましょう。

def print_user(user: User):
    """ 半角スペースを取り除いてから出力する """
    user.remove_half_space()
    print(user.name)

user = User(name="コボリ アキラ")
# 心の声「ここでUserインスタンスが作成されるのか。名前は半角スペースつきで入っている」

# 数10行の処理...

print_user(user) # print_userメソッド内で、半角スペースを取り除く「破壊的操作」があり、userインスタンスにも影響する

# 数10行の処理...

print(user.name) # -> コボリアキラ
# 心の声「あれ? なんで半角スペースがないんだ?  print_userでuserを渡しただけなのに......」

こちらの例は、コードを書いた人でもいずれ思い出せないような破壊的操作になりそうです。

もちろんコードを読めば、何が起きているかはわかります。そして今後の開発においても、まずすべてのコードを読まないとコードを書き始めるなんて怖くて仕方なくなるでしょう。

死んでも破壊的操作をしないために

もちろんパフォーマンスの観点や限定的な影響だと許容して破壊的変更を使うことはあります。

しかし原則としては破壊的操作をするべきではないでしょう。名著『良いコード/悪いコードで学ぶ設計入門』でも「デフォルトは不変に」と述べらています。2

それでは破壊的操作をしないためにどうすればよいでしょう。

破壊的操作ではなく「あたらしく宣言」もしくは「再代入」する

覚えておきたい解決方法は、次のような工夫です。

class User:
    name: str

    def remove_half_space(self):
        """ 半角スペースを取り除いたあとのuserインスタンスを返却する """
        name = self.name.replace(" ", "")
        return User(name=name)

user = User(name="コボリ アキラ")

# 数10行の処理...

user_with_removed_half_space = user.remove_half_space()

# 数10行の処理...

print(user.name) # 当然userインスタンスの中身は変わらない

上記のようにすることで、破壊的操作はいかようにでも防ぐことができます。

user_with_removed_half_spaceが気に入らなければ、状況に応じてはuserとして再代入することも許されるでしょうか。

破壊的操作ができるようなメソッドをつくらない

自身が「破壊的操作をしないようになった」としても、すべてのエンジニアがそうとは限りません。そのため破壊的操作ができるようなメソッドの用意も避けましょう。

よくあるのは「容易にgetter/setterをつくらない」ことでしょうか。

自身はsetterを使わないと決めているが、組織の暗黙的な文化で「すべてのクラスにgetter/setterを設ける」みたいなルールがあったとすれば、これは根深い問題になりえます。

まとめ

ちょっと挑発的なタイトルで3、個人的に重要だと感じるコーディングのルールを書いてみました。

最近では言語仕様の変化もあって「当たり前」になってきたようにも見えますが、あらためて自身の理解のために記事として残してみました。

  1. 現実的な例がパッと思いつかず、とりあえずこのような例にしてみました。よろしければ「こういう例があった」みたいなことを教えていただけたらとても幸いです。

  2. 仙場大也『良いコード/悪いコードで学ぶ設計入門―保守しやすい 成長し続けるコードの書き方』、p52。

  3. 『死んでも床にモノを置かない』がネタ元。

45
27
7

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
45
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?