小さいリファクタリング

復習:これまでに見たリファクタリングテクニック

小さいテスト駆動開発では、VS Codeの機能を使ってリファクタリングした

名前の変更

https://refactoring.com/catalog/renameVariable.html

関数の抽出

https://refactoring.com/catalog/extractFunction.html

文字の変換プログラム

アルファベット大文字小文字を 13字ずらすrot13.py

  • a は13字後ろの n に変換

  • o は13字手前の b に変換

与えた文字列の1つ1つの文字を13字ずらす

  • abc -> nop

  • XYZ -> KLM

テストがすでにある(test_rot13.py)。 rot13.py をリファクタリングしていく

$ # examples/tdd にいる前提です(TDDの例からの続き)
$ cd ../refactoring # examples/refactoring
.
├── rot13.py
└── test_rot13.py

現状の実装

sequenceDiagram プログラマ->>transform: 文字列 nikkie loop 各文字について transform->>transform_letter: 1文字の文字コード transform_letter->>is_between: 文字コードは"a"と"m"の範囲か is_between->>code_for: "a" code_for-->>is_between: "a"の文字コード is_between->>code_for: "m" code_for-->>is_between: "m"の文字コード is_between-->>transform_letter: True / False transform_letter->>is_between: 文字コードは ... の範囲か is_between->>code_for: ... code_for-->>is_between: ... is_between-->>transform_letter: True / False transform_letter-->>transform: 13字入れ替えた1文字 end transform-->>プログラマ: 13字入れ替えた文字列 avxxvr

ユーザが使う関数 transform

  • 与えられたinputと同じ長さで13字ずらした文字を並べて返す

def transform(input: str) -> str:
    result = ""
    for character in input:
        char_code = ord(character)
        result += transform_letter(char_code)
    return result

transform_letter

  • transform で1文字を文字コードに変換してから呼ばれる(ord

  • 文字コードを元に13字だけずらした文字を返す(変換)

def transform_letter(char_code: int) -> str:
    """
    >>> a_code = ord("a")
    >>> transform_letter(a_code)
    'n'
    """
    if is_between(char_code, "a", "m") or is_between(char_code, "A", "M"):
        char_code += 13
    elif is_between(char_code, "n", "z") or is_between(char_code, "N", "Z"):
        char_code -= 13
    return chr(char_code)

is_between

  • ある文字コードが a から m の範囲にあるかのような判定で transform_letter から呼ばれる

  • 文字コードが範囲内にあるかどうかを返す

def is_between(char_code: int, first_letter: str, last_letter: str) -> bool:
    """
    >>> b_code = ord("b")
    >>> is_between(b_code, "a", "c")
    True
    >>> is_between(b_code, "c", "e")
    False
    >>> # 境界は含む
    >>> is_between(b_code, "b", "d")
    True
    >>> is_between(b_code, "a", "b")
    True
    """
    return code_for(first_letter) <= char_code <= code_for(last_letter)

code_for

  • is_between から呼ばれる

  • 文字の文字コードを返す

def code_for(letter: str) -> int:
    """
    >>> code_for("a")
    97
    """
    return ord(letter)

Tip

文字コードまわりのPythonの組み込み関数

逆の関係

リファクタリングのアイデア

文字コードに変換する必要なくない?

  • transform_letter 関数の引数

  • is_between 関数の引数

今の実装は、ある文字が「a-mの範囲にあるか」「A-Mの範囲にあるか」などを文字コードに揃えて求めている。

💡文字コードにしなくても 文字の大小関係 で求められる!

>>> ord("a") <= ord("c") <= ord("m")
True
>>> "a" <= "c" <= "m"
True

リファクタリングしてこうしたい

sequenceDiagram プログラマ->>transform: 文字列 nikkie loop 各文字について transform->>transform_letter: 1文字 transform_letter-->>transform: 13字入れ替えた1文字 end transform-->>プログラマ: 13字入れ替えた文字列 avxxvr

リファクタリング:関数のシグネチャ変更

https://refactoring.com/catalog/changeFunctionDeclaration.html

  • 一部のIDEではサポート

  • VS CodeはPython拡張をインストールしても、シグネチャ変更をサポートしていないように思われる

要はこうしたい

- def transform_letter(char_code: int) -> str:
+ def transform_letter(letter: str) -> str:

Q: どのようにやりますか?

警告

かつてのnikkieの場合

  • char_code引数を消してletter引数に置き換え

  • 変数名でなく型も変わっているので、関係していそうなところを一度に直す

  • テストが通るまでこれを続ける(長いRed...)

$ # examples/refactoring にいる前提です
$ ptw

初手:引数を追加するだけ

transform_letter にまずは引数を追加

  • 文字列型の letter 引数を追加(第1引数)

  • 引数 char_code は最終的にはなくしたいが、いまはまだなくさず第2引数とする

rot13.py
def transform(input: str) -> str:
    result = ""
    for character in input:
        char_code = ord(character)
-        result += transform_letter(char_code)
+        result += transform_letter(character, char_code)
    return result

-def transform_letter(char_code: int) -> str:
+def transform_letter(letter: str, char_code: int) -> str:
    if is_between(char_code, "a", "m") or is_between(char_code, "A", "M"):
        char_code += 13
    elif is_between(char_code, "n", "z") or is_between(char_code, "N", "Z"):
        char_code -= 13
    return chr(char_code)

追加した引数は、この時点ではまだ使わない

テストは通っています!(リファクタリングを間違えていない🙌)

引数を追加 第2弾

  • アイデアは「文字コードの比較に代えて文字で比較」

  • 比較している is_between 関数にも引数を追加

rot13.py
def transform_letter(letter: str, char_code: int) -> str:
-    if is_between(char_code, "a", "m") or is_between(char_code, "A", "M"):
+    if is_between(letter, char_code, "a", "m") or is_between(letter, char_code, "A", "M"):
        char_code += 13
-    elif is_between(char_code, "n", "z") or is_between(char_code, "N", "Z"):
+    elif is_between(letter, char_code, "n", "z") or is_between(letter, char_code, "N", "Z"):
        char_code -= 13
    return chr(char_code)


-def is_between(char_code: int, first_letter: str, last_letter: str) -> bool:
+def is_between(letter: str, char_code: int, first_letter: str, last_letter: str) -> bool:

テストは通っています!

is_between 関数変更(アイデアを実装)

rot13.py
def is_between(letter: str, char_code: int, first_letter: str, last_letter: str) -> bool:
-    return code_for(first_letter) <= char_code <= code_for(last_letter)
+    return first_letter <= letter <= last_letter

テストは通っています!

使わなくなった code_for 関数を消す

rot13.py
-def code_for(letter: str) -> int:
-    return ord(letter)

テストは通っています!

関数のインライン化

is_between 関数を インライン化

  • 関数のインライン化は、関数の抽出の逆操作

  • 関数の本体が分かりやすければ、関数をやめて処理を直に書く

rot13.py
def transform_letter(letter: str, char_code: int) -> str:
-    if is_between(letter, char_code, "a", "m") or is_between(letter, char_code, "A", "M"):
+    if ("a" <= letter <= "m") or ("A" <= letter <= "M"):
        char_code += 13
-    elif is_between(letter, char_code, "n", "z") or is_between(letter, char_code, "N", "Z"):
+    elif ("n" <= letter <= "z") or ("N" <= letter <= "Z"):
        char_code -= 13
    return chr(char_code)


-def is_between(letter: str, char_code: int, first_letter: str, last_letter: str) -> bool:
-    return first_letter <= letter <= last_letter

1箇所ずつインライン化していくのがオススメ。

すべてインライン化した時点でも、テストは通っています! 2つ関数を消してスッキリしてきました

transform_letter 関数の char_code 引数を消す仕込み

引数 letter から char_code を求めてしまいます!

rot13.py
def transform_letter(letter: str, char_code: int) -> str:
+    char_code = ord(letter)
    if ("a" <= letter <= "m") or ("A" <= letter <= "M"):
        char_code += 13
    elif ("n" <= letter <= "z") or ("N" <= letter <= "Z"):
        char_code -= 13
    return chr(char_code)

テストは通っています! 引数として渡される char_code 引数は再代入されるので、 引数にあってもなくても同じ状態 となりました(削除の仕込み)

ついに char_code 引数を消す!

あってもなくても同じ char_code 引数を消します!

rot13.py
def transform(input: str) -> str:
    result = ""
    for character in input:
-        char_code = ord(character)
-        result += transform_letter(character, char_code)
+        result += transform_letter(character)
    return result


-def transform_letter(letter: str, char_code: int) -> str:
+def transform_letter(letter: str) -> str:
    char_code = ord(letter)
    if ("a" <= letter <= "m") or ("A" <= letter <= "M"):
        char_code += 13
    elif ("n" <= letter <= "z") or ("N" <= letter <= "Z"):
        char_code -= 13
    return chr(char_code)

テストは通っています!

考察

  • 小さい操作を続けて適用したことで、 Greenを保ったまま リファクタリングができた!

    • Redになっても1つ前のGreenに すぐに戻せる

  • リファクタリングは ルービックキューブ (『Clean Craftsmanship』)

  • 手順(や使い所)を押さえる & エディタを使った操作を覚える -> 常中(TDDのサイクルで自由に取り出せる)

参考文献

関連アウトプット