nikkie-ftnextの日記

イベントレポートや読書メモを発信

『Clean Craftsmanship』のリファクタリングのJavaのコード例をPythonで書き換える(検索してやりきった後にChatGPTも試してみました)

はじめに

上原歩夢さん、お誕生日おめでとうございます!1🎀🎉🖤 nikkieです

Uncle Bobの『Clean Craftsmanship』にリファクタリングを扱った章があったことを思い出し、素振りとして写経することにしました。
写経のスタートラインに立つ前に、Javaのコード(リファクタリング前)をPythonに変換したのですが、その中での学びをアウトプットします。

※肝心のリファクタリングの内容には立ち入りません(またの機会に)

目次

『Clean Craftsmanship』 第5章 リファクタリング

https://www.tumblr.com/asciidwango/693992928727760896/clean-craftsmanship

リファクタリングの例2として、COVIDの感染者数のレポートプログラム(Java)が登場します。

  • アメリカは州(state) > 郡(county)という行政単位
  • COVIDの新規感染者数(CSVファイル)を集計する
  • 以下を集計
    • countyごとの新規感染者数の移動平均
    • stateごとの合計
    • 総計

実装は巨大なメソッド1つとなっており、この後のリファクタリングがめっちゃ楽しみです!(テストコードがあるのが最高!)

結論:Pythonで書き換えた例

今回の書き換えで多くを学んだので、もう一度書き換えるとしたら(細かく改善して)別の形になると思います。
ただ今回の目的は(この後やる)リファクタリングの写経なので、Pythonの実装としては突き詰めずにこのまま進もうと思います。

動作環境は、Python 3.10.9です。

どうやって書き換えた?

Kindleでコードを確認し、Javaのコードで分からない部分を検索して調べながらPythonに書き換えました。
Javaの言語固有の事項がけっこう登場する例になっているのは誤算でした(想定よりかなり時間がかかっています)。

なんとか書き換え終えた後、1日1エントリのネタにしようと浮かびました。
その際に「ChatGPTに聞いていれば楽できたのでは?」と閃き、ChatGPTさんの回答も込みでまとめることにしました。

0.ソースコードKindleを見るべからず

InformITで配布されているWeb版を見ましょう(「はじめに」で案内されます)。
理由は、Kindleではスペースの数が区別できないからです。

Kindleではコードが以下のように見えました。

"====== ===== =============\n"

Web版ではこうです。

"======     =====     =============\n"

半角スペースの数がぜんぜん違うじゃん! やだも〜
まさか半角スペースの数に違いがあると思っていなくて、テストが通らずハマっていました。

1.String.trimの書き換え

trimメソッド | Javaコード入門

文字列から前後の空白を除去するには、trimメソッドを利用します。

Pythonではstr.stripメソッドになりそうですね。

2.String.formatの書き換え

以下のコード、Pythonでどう書き換えます?

String.format("%-11s%-10s%.2f\n", county, state, rollingAverage)

Javaの文字列のformatメソッドを確認します。
formatメソッド | Javaコード入門
-11-10が何か気になりますが、例が見つかりました。
%-10s左詰めの10文字です(右側に半角スペースが入ります)

Pythonのf-stringを使うとして、左詰めってどう実現します?
今回初めて知ったのですが、<を使う3そうです

# <の前の半角スペースはfill(空白を埋める文字)です
f"{county: <11}{state: <10}{rolling_average:.2f}\n"

ChatGPT先生でショートカットできたと思われる

nikkie:

以下のコードをPythonに変換するとどうなりますか?
String.format("%-11s%-10s%.2f\n", "c1", "s1", 1.23)

ChatGPT先生:

このJavaのコードは、String.formatメソッドを使用して、フォーマットされた文字列を生成しています。Pythonには、文字列をフォーマットするための方法がいくつかありますが、f文字列を使用するのが最も簡単で直感的な方法です。

print(f"{ 'c1':<11}{'s1':<10}{1.23:.2f}")

このコードをPython処理系に入れるとたしかに変換できています!

>>> print(f"{ 'c1':<11}{'s1':<10}{1.23:.2f}")
c1         s1        1.23

ChatGPTの回答からfillを省略できるという気付きがありました!4

この文字(※[引用者注] fillのこと)には任意の文字を指定でき、省略された場合はデフォルトの空白文字となります。

3.StringBuilder.appendの書き換え

appendメソッド | Javaコード入門

「+」演算子で文字列を連結することもできますが、Stringクラスの性質上、効率的ではありません。
というのも、Stringクラスが固定長の文字列を表すことから、連結のたびに新しいインスタンスが生成されるためです。
一方、StringBuilder/StringBufferクラスは可変長文字列を表すので、文字列が連結されてもインスタンスが新たに生成されることはありません。

なるほど〜。
ただPythonだとStringBuilderクラスはないため、(新しいインスタンスを返すけれど)文字列の+でいいんじゃないかなと思いました。

おしえて!ChatGPT先生

nikkie:

次のコードをPythonに変換するとどうなりますか?

StringBuilder builder = new StringBuilder("" +
"County     State\n" +
"======     =====\n"
)
builder.append("c1         s1\n")

ChatGPT先生:

StringBuilderは、Javaの文字列を効率的に扱うためのクラスであり、可変の文字列を表します。Pythonでは、文字列を直接編集できるため、StringBuilderに相当するクラスはありません。代わりに、Pythonの文字列はイミュータブル(不変)なので、文字列を連結するためには、+演算子を使用することができます。

builder = (
    "County     State\n" +
    "======     =====\n" +
    "c1         s1\n"
)

StringBuilder初期化で、空の文字列に連結した文字列を渡す?

ChatGPT先生への質問でnew StringBuilder("" +としている部分、なんで空文字列に連結しているかがよく分かりませんでした。
この空文字列、要らないんじゃないかと思うんですよ(必要な理由が分からず…)

このリファクタリングのコードは他にもスキが見つかった5ので、Uncle Bobが書いたコードじゃないんですかね?
ここは、私にJavaの知識がなさすぎて、間違ったことを言っている可能性があります。

ChatGPT先生も以下のように書き換えられるとのことなので、やはり必須ではないのかな?
空文字列への連結で始めるに込めた意図を知りたい!

StringBuilder builder = new StringBuilder();
builder.append("County     State\n");
builder.append("======     =====\n");
builder.append("c1         s1\n");

4.TreeSetの書き換え

Javaってコレクションに各種取り揃えらてるんですね。

TreeSetクラス - Javaちょこっとリファレンス

TreeSetは基本的にHashSetと同じですが、自動的に要素がソートされる、nullが使用できないという違いがあります。

今回のコードではTreeSetインスタンスの要素を1つずつ取り出して処理をしています。
自動的に要素がソートされる」性質を使っていそうでした。
そこでPythonのsetをソートして1つずつ取り出すとして実装しました。

states = set(state_counts.keys())
for state in sorted(states):
    report += f"{state} cases: {state_counts[state]}\n"

ChatGPT先生、JavaのTreeSetってPythonでは何になりますか?

ChatGPT先生:

JavaのTreeSetは、要素を自動的にソートし、一意の値のセットを保持するデータ構造です。

Pythonにおいて、要素の自動ソートを実現するためには、sorted関数を使用することができます。また、一意の値を保持するために、set型を使用することができます。

# TreeSetの代わりに、Pythonではset型を使用する
tree_set = set()

# 要素を追加する場合は、addメソッドの代わりに、直接setに要素を追加する
tree_set.add(3)
tree_set.add(1)
tree_set.add(2)

# 要素をソートする場合は、sorted関数を使用する
sorted_set = sorted(tree_set)

# ソートされた要素を出力する
print(sorted_set)

このように、Pythonでは、JavaのTreeSetに相当するデータ構造を標準ライブラリで提供されているものではなく、set型とsorted関数を組み合わせることで代替することができます。

終わりに

言語固有の事項が多いJavaのコードをPythonで書き換えての学びをアウトプットしました。
StringBuilderTreeSetがあるのが、興味深いです〜。
そして、f-stringのalignやfillは初めて知りました。

P.S. ChatGPTとコーディングして

ChatGPTの利用はブログにまとめるにあたって浮かんだアイデアなのですが、これはヤバいですね!
説明を100%信じると矛盾があって混乱しますが、コード例をもとに理解していくと、一人ではそんなに早く到達できないような地点にAIに高速で連れて行かれます

このツールはプログラミングとの向き合い方を大きく変えそうと予感しました。
1つの言語に習熟したら他の言語はChatGPTと一緒に習熟していけそうです。
また、ChatGPTにエラーメッセージを入れると、丁寧に解説してくれるとも聞いています。
書籍で躓いてググっても解決できないというのはもしかすると過去のものになるかもしれませんね。

そして、コーディング中検索が不要というのも大きそうです。
何もわからない中で繰り返し検索するより、ChatGPTにコード例を吐かせて、それをもとにわからないところを解消していくという方法が魅力的に映ります。
理解しないまま使いたくない私は一次情報に当たりたいので、今度は情報源も聞いてみようと思います。


  1. フィールドの抽出の例です
  2. 書式指定ミニ言語仕様alignに説明があります
  3. https://docs.python.org/ja/3/library/string.html#format-specification-mini-language
  4. テストコードに、書籍の中では、使っていないクラス変数(DELTA)がありました