小さいリファクタリング¶
復習:これまでに見たリファクタリングテクニック¶
小さいテスト駆動開発では、VS Codeの機能を使ってリファクタリングした
名前の変更¶
関数の抽出¶
文字の変換プログラム¶
アルファベット大文字小文字を 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
現状の実装¶
ユーザが使う関数 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の組み込み関数
chr
文字コード -> 文字ord
文字 -> 文字コード
逆の関係
リファクタリングのアイデア¶
文字コードに変換する必要なくない?
transform_letter
関数の引数is_between
関数の引数
今の実装は、ある文字が「a-mの範囲にあるか」「A-Mの範囲にあるか」などを文字コードに揃えて求めている。
💡文字コードにしなくても 文字の大小関係 で求められる!
>>> ord("a") <= ord("c") <= ord("m")
True
>>> "a" <= "c" <= "m"
True
リファクタリングしてこうしたい
リファクタリング:関数のシグネチャ変更¶
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引数とする
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
関数にも引数を追加
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
関数変更(アイデアを実装)¶
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
関数を消す¶
-def code_for(letter: str) -> int:
- return ord(letter)
テストは通っています!
関数のインライン化¶
is_between
関数を インライン化
関数のインライン化は、関数の抽出の逆操作
関数の本体が分かりやすければ、関数をやめて処理を直に書く
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
を求めてしまいます!
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
引数を消します!
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のサイクルで自由に取り出せる)