「プログラミングをするとき、何に気をつけるべきか?」という質問に対して、自分が思っているのは「命名を大切にする」こと。
プログラミングというのは、モヤモヤしている概念(操作なども含む)を抽出して明確にし、そこに名前をつけていくことだと思ってる。
なので、いい命名を行えるかどうかで、概念の切り出しがキレイかどうかーーすなわち、プログラムがキレイになるかどうかが決まってくる。
これを、「コードの臭い」というリファクタリングの一視点と絡めて、実際に見ていきたい。
「初心者がリファクタリングで目をつけるべき場所」
とらラボ主催のLT会で、きり丸さん(@nainaistar)による次のような発表があった:
「コードの臭い(Code Smells)」に注目してリファクタリングするといいよ、という話なんだけど、この話の多くは「いい命名をしましょう」の一言で片付いたりする。
ここで挙げられてるのは、以下:
- Large Class(大きすぎるクラス)
- Long Method(長すぎるメソッド)
- Long Parameter(多すぎる引数)
- Duplicate Code(重複したコード)
- Magic Number(意味が読み取れない謎の値)
- Comments(大量のコメント)
- Arrow(深すぎるネスト)
- Primitive Obsession(基本型に執着する)
- Feature Envy(外部モジュールの内部データを使う)
- Data Clumps(一緒に扱われるべきデータが個別に扱われてる)
それぞれを「命名」の観点で見てみる。
Large Class
大きすぎるクラスというのは、いくつもの責務を持っていることになる。
そうなると、あれもやってこれもやってとなるから、その名前は一言で言い表しづらいものになる。
なので、以下のような特徴が出てくる:
- 「〜マネージャー」「〜コントローラ」etc.
- 名前にそぐわないメソッドがある
- 1モジュール=1クラスのstaticおじさんクラス
こういった場合、概念を切り出して名前をつけ、いくつかのクラスに分けてあげるといい。
「この概念はまとめて名前をつけられるな」とか「このデータだけ抜き出して名前をつけられるな」と考えるといい。
Long Method
長すぎるメソッドというのは、処理をダラダラと書いてしまってる。
そういう場合、大抵は
// ここでxxxする ... // 次にxxxする ...
みたいに、空行を挟んでいくつかの処理の塊が続いてることが多いと思う。
そういったときは、それぞれの処理の塊をメソッドに切り出してやるといい。
「ここの処理の塊、何やってるの?」という質問に対して、「〜をやってるんだよ」という答えが出せるなら、その操作には名前がつけられるということになる。
なので、その名前でメソッドを用意する。
そうすると、メソッドは短くなるし、メソッド名で処理が説明されるのでコメントも不要になる。
ここで一つ気をつけたいのが、処理の重複を防ぐためにメソッドを用意するのではなく、処理の説明をするためにメソッドを用意するということ。
実装上の都合(=同じ処理を何度も書くのは嫌)から設計を考えるのではなく、概念を整理する観点(=この処理には名前をつけられる)から設計を考える。
あとでも言及するけど、これは結構重要。
Long Parameter
パラメータが多すぎるメソッドは、そもそもそのパラメータをまとめて名前をつけられることが多い。
あるいは、処理に必要な情報があまりに多いということは、責務が大きくなりすぎてる可能性もある。
前者であれば、そのまとめたものに名前をつけてクラスとして切り出すといい。
そうすればパラメータの数はグッと減る。
そして、後者の場合、部分的な処理を適切な名前をつけて別のクラスに移せば、その処理で必要になるデータは移した先のクラスが持ってればいいので、パラメータとしてデータを渡す必要がなくなる。
「パラメータをまとめたものに名前をつけられないか」「やってる処理に名前をつけて、その処理で使ってるデータに名前をつけられないか」と考えるといい。
Duplicate Code
重複したコードは、その処理に名前をつけられるはずなので、その名前でメソッドにすればいい。
ここで重要なのが、「処理が同じだから抜き出してメソッドにする」のではなく、「処理に名前がつけられるからメソッドにする」ということ。
前述の通り、実装ではなく概念が同じかに気をつけないといけない。
たとえば、スライドだと次のような変更をしてる:
// 変更前 private static int userId = 0; private static int taskId = 0; public static int takeUserId() { return userId++; } public static int takeTaskId() { return taskId++; } // 変更後 private static int id = 0; public static int takeId() { return id++; }
これはダメな変更の例。
なぜかというと、「処理が同じだから」という理由で共通化してしまっているから。
でも、元のメソッド名が示す通り、「ユーザIDを取得する」という操作と「タスクIDを取得する」という操作は、実装は同じかもしれないけど、操作の意味が違う。
意味が違う(けど実装は同じ)処理をひとまとめにしてはダメ。
これをやってしまうと、概念ではなく実装に依存することになるので、途端に変更が難しくなる。
これに類似したミスというのはよくあって、代表的なのは処理が同じだから親クラスを作って、親クラスに実装を持っていくというもの。
結果、親クラスでした変更が思わぬ問題を引き起こしたり、あるいは変更が困難になったり。
特定の処理を使いまわしたいなら、その処理を行うクラスを用意して、そのクラスに処理を委譲するようにした方がいい。
「〜の処理をするクラス」と、ここでも概念を抜き出して命名を行っている。
(Rubyの場合、Mix-inが強力なので、これはModuleとしてincludeすれば実現できる)
Magic Number
これはもうそのまんまで、数字をそのまま使うんじゃなくて、名前をつけて意味が分かるようにしましょうね、ということ。
コメントではなくメソッド名で処理を説明するというのもこれと同じ。
名前をつければ、コメントを書かなくてもその名前で説明できる。
Comments
コメントが多すぎるコードというのは、つまり概念としてまとめられるコードがあるのに、それをメソッドとしてまとめてないということ。
まとめてなくてパッと見だと何をしてるか分からないから、コメントが必要になってる。
もう再三言ってるけど、コメントを書くくらいなら、適切な名前をつければいい。
ただし、「こういう方法もあるはずなのに、なんであえてこんな処理をしてるのか」という説明が必要な場合もある。
そういった場合はメソッド名だけでは説明できないので、コメントをつけた方がいい。
(大抵、何らかの問題と結びついてるはずなので、issueへのリンクを貼っておくといい)
Arrow
入れ子が深くなってる場合、いろいろ原因はあるけど、まずは処理に名前をつけて切り出せる部分がないかを探してみるといい。
切り出せればそれだけで入れ子が浅くなる。
また、スライドのようにif文がズンズン深くなってるときは、その条件判断に名前をつけて切り出すといい。
たとえば、if (args_is_valid(...)) { ... }
とするとか。
ちなみに、早期returnは個人的にはOKだと思ってるけど、コーディング規約で許されない場合もあるので注意。
Primitive Obsession
基本型に固執しているというのも、やっぱり適切に命名してないということ。
概念として切り出せるのだから、それに名前をつければいい。
名前をつければ、コンパイラの型チェックの恩恵を受けられるし、何を扱ってるのかも分かりやすくなる。
Feature Envy
外部モジュールの内部データを使おうとしてるということは、つまりそのデータを使って何かやりたい操作があるということ。
なら、その操作は名前がつけられるはずだし、それはその外部モジュールの機能として(=メソッドとして)提供できることになる。
ただ、データオブジェクトの場合はちょっと微妙で、たとえば設定値を保持してるConfigクラスがあったとして、その設定値を使う処理をConfigクラスが持つべきかというと、かなり微妙。
この場合、Configクラスは値を保持するという責務だけをやるべきで、保持してる値を他のクラスが目的に合わせて使う方が、責務が明確になる。
Data Clumps
一緒に扱われるべきデータが個別に扱われているというのは、つまりそのまとまりに名前がつけられてないということ。
名前をつけてクラスにすれば、一緒に扱われるようになる。
こんな感じで、大抵のことは「いい命名をしましょう」の一言で片付く。
なので、命名は本当に大切。
これは以前書いた言葉にするということ。 - いものやま。にも通じたり。
言葉にすることで曖昧だったものが明確になるように、命名することで曖昧だった概念も明確になる。
今日はここまで!