このページに書いてあること
この辺の記事とかでよく言っているSOLID原則とか業務は個別とか色々な話を書かせてもらっていますが、そのレベル(というか、プロジェクトの方針的にそのルールではないというか)に達していない場合どのようにクラス分けやメソッド分けをすればいいのかという質問を自社の若手からよくされます。
それに対する一つの目安についてです。クラス分割とメソッド分割にわけて考えます。
SOLID原則やオブジェクト指向などの概念は一旦捨てて、とりあえず巨大なクラスやメソッドを作らないようにするために最低限で分割するときの目安ということでご注意ください。
若手の人は、一旦これに沿って分割してみてください。多分すぐに使いづらさに気づくと思いますが、それもまた重要な一歩です。
クラスやメソッドが巨大な時に発生する問題
そもそもなぜクラスやメソッドの分割が必要なのかという話は最早必要ないかもしれませんが念のため確認しましょう。
理由は大体以下のようなものが挙げられることが多いです。
イメージとして、1メソッドしか書かれていないようなクラスを例にしましょう。で、その1メソッドだけで1000行のコードがあるとしましょうか。
わずかな修正が全ての機能に影響を与えてしまうため、テストが大変
例えば1000行のうち3行のソースコードを修正したとしましょう。この時、本来はその3行を含んだメソッドのみのテストを行っておけば良いわけです。
ですが全てが1メソッドに書かれていた場合は、1000行全てがテスト対象になってしまいます。
なぜかというと、分割されていないメソッドではわずかな修正がどこに影響を及ぼすかがわからないためです。
100行目で宣言した変数を使いまわしており、800行目でもそれを使っていたなんてことになっている可能性があると、些細な変更も全体に影響を及ぼすいうことです。
メソッド単位で区切っていれば、例えば戻り値がbooleanであれば使う側からすればtrue/falseしか戻ってこないので修正をしても大した影響はないですね。
機能の再利用性が無くなる
いろんな機能が詰まった1000行のソースコードが、そのまま他の機能でも使用できるなんてことはほぼ無いでしょう。
せっかく一生懸命作って、テストしたコードはそこ限りの使用となってしまいます。
一部の機能のみ切り出して使おうとしたとしても、別のコードなので結局再テストをする羽目になってしまいます。
であれば、最初から分割しておいた方が良さそうですね。
コードの変更が誰もできなくなる
先の2つに関連してということにもなるのですが、誰もソースコードをいじりたがらなくなります。
1000行の巨大なメソッドはどこでどんな変数をどういうふうに使っているかが簡単には見通せないですから、変なバグを踏む可能性が高くなってきます。
そんなコードって誰も編集したくないですよね?そうなると、基本的には書いた本人がメンテナンスしていくか、押し付けられた誰かが書いた人の悪口を言いながらメンテナンスしていくことになります。
OOPやSOLID原則などを適用する前の分割方法
ここからが本題です。
上記の問題を回避するにはオブジェクト指向に則ったプログラミングが基本策となります。そこには高度なOOPの概念やSOLID原則など、結構高度なスキルが必要になります。
そこで第一段階として以下のような考え方に基づいたクラス分とメソッド分割を提唱します。
とりあえずのクラス分割方法
1メソッド1000行のクラスを見た時、その中で明らかに種類の違う処理はないでしょうか。
例えば、メソッドを先頭から見て行った時に、先頭から
- 入力値のチェックをする
- データベースから検索をする
- 検索した値と入力値の一致チェックをする
- チェック結果をデータベースに格納する
- 返す画面を生成する
こんな感じになっていると思います。
で、これらを全て別クラスに持っていきましょう。表記は【クラス名#メソッド名】です。引数は適宜追加してください。
- 入力値のチェックをする→Validate#validate()
- データベースから検索をする→Database#select()
- 検索した値と入力値の一致チェックをする→Verify#verify()
- チェック結果をデータベースに格納する→Database#insert()
- 返す画面を生成する→Contents#create()
こんな感じで雑に分割しただけでも簡単に巨大なクラス、メソッドを整理することができるようになります。
ひとまずはこれで様子を見て、スキルが高まってきたらOOPやSOLI D原則に手を出すのがいいと思います。
先の例だと以下の二つの処理については、
- データベースから検索をする→Database#select()
- チェック結果をデータベースに格納する→Database#insert()
Database#execute()のインタフェースを作成して、それらを実装したDatabaseSelectクラスやDatabaseInsertクラスを作ることでOOPのポリモーフィズムとかを確保できそうです。
それについてはこの辺で語っているのでご参照ください。
とりあえずのメソッド分割方法
クラスは分割できました。続いて分割後のクラスでメソッド行数が大きくなってしまった場合の分割方法を確認します。
例えば以下のメソッドについて考えます。
- 入力値のチェックをする→Validate#validate()
このvalidateメソッドが100行くらいあった場合どうしましょうか。
というか、そもそもの話なのですが、若手の方が100行のメソッドを書いた時にそのメソッド名は絶対にvalidateにはなっていません。
validateといえば、文字種チェックや文字長チェックなどが定石の処理ですから、おそらく
- checkCharacterTypeAndLengtsh
とかになっていると思います。要は、処理の内容がメソッドの名前に来ているパターンです。
名は体を表す
と言いますから致し方ないことです。
はい。メソッドの分割方法は非常に簡単でして、メソッド名を上記のように単純に付けた際に二つ以上の処理が名前に現れた場合は、それぞれ分割したほうがいいです。
- checkCharacterTypeAndLengtsh
の例で言うと、checkCharacterTypeとcheckLengthに分割することができそうです。
こちらは非常に単純でしたね。
そのようにメソッド名ベースで分割していくと、大体3,40行くらいのメソッドに分割されていくので若手にはおすすめです。
1000行あっても別に分割しなくていいケース
もあります。今まではとにかく分割分割で来ましたが、分割しなくてもいいようなケースもあります。もちろん、分割してもいいのですが
このまま記事を終わると「とにかく分割しろ」という方針になりそうなので念のため。
呼び出しがわの使い方を考えた時に、ひとまとまりの機能としてしか使わない場合は分割する必要はないと思っています。
例えば、外部のAPIに通信するようなケースを考えた時、その処理は
- コネクションを確立する
- ボディやヘッダデータを書き込む
- リクエストを送信する
- レスポンスを読み込む
- コネクションをクローズする
- タイムアウトが発生した場合はリトライ処理を実施する
この辺が通信をするときのひとまとまりの処理といえそうです。
この辺はメソッドを分割しなくても良いと考えています。理由のなかで最大のものは、「上記処理をメソッドかしても再利用はしないから」です。再利用するとしたら、このクラスごと持っていくことになります。どこに通信するにしても、上記の仕組みは共通です。このような考え方はこちらでも書いてますのでご確認ください。
要は、十把一絡げに線引きするのではなくてしっかりと考えた上で適材適所で技術を適用したいですね。
とはいえ、若手の方は一旦乱暴でもいいのでクラスとメソッドを分割してみてください。