クラス設計

業務用のシステムにおいてクラス設計(分割)をどのように考えるか-2

2020年10月27日

このページに書いてあること

前の記事で業務用システムで機能を分割するさいに頭に入れておくと良いことを書きました。
今回は具体的にどう分けるかをまとめます。


業務と仕組みは縦と横で分割する。

例えば、URL1,2,3を持つWebアプリがあったとした場合を考えます。
URL1,2,3はアプリの外部に公開されています。

分割イメージ

言っていることは意外とシンプルでして、上図が基本的には全てです。
要は、業務処理はそれぞれ固有の手順があるので共通化は基本できないはずです。しかし、手順は共通化できないですが、手順を実現する仕組み(上の例ではDB接続やHTTP通信、認証処理など)は共通化できるよねという話です。

例えばDBに接続するときにCRUD操作自体は変わらないですよね。
変わるのは「どんなデータを操作するか」であったり、「どのテーブルを操作するのか」という部分だと思います。

なので、どんなデータを操作するかやどのテーブルを操作するかは、業務部分の方で定義してやるようにし、CRUD操作自体の仕組みに関する部分は共通化してやろうよということです。

仮に業務処理を共通化しようとした場合何が起こるかを一応考えます。
(ちなみに、私のいう業務処理とは所謂Controllerクラスなどのリクエストを受け付けて、どのような手順で処理を実行するかを司る部分です。もちろんControllerの役割をプレゼンテーション層のみに限定して、実際の業務処理は一段下のServiceやLogicなどに任せても良いです。その場合は、一段下のServiceやLogicの話として理解してください)

機能の共通部分

例えば、最初の開発時点においては業務に共通の処理があったのでAbstractクラスなどを使って共通化していたとしています。しかし、新機能開発が決まり、以下のようになったらどうでしょう。

機能の共通部分

こうなったとき、既存のAbstractは使えなくなりますから、多重継承して部分的に使い回すとかをすることになるでしょう。地獄の始まりです。

異なる層はDTOでデータを連携する

前の記事で書いた通り、基本的なクラス設計はSOLID原則のDIPやISPに則ってクラス分割を行って割り付けていきます。

ここで問題になるのは、共通化するときに「どのようにインタフェースを使いまわすか」です。

例えばあるインタフェースがStringを2つ要求するとします。
この時、別の業務を追加(URLの追加)しようとした際に、そちらではint引数もこのIFに引き渡す必要があるとどうなるのでしょうか。

クラス図

こんな風に作っていたクラスがあって、IFとしてやりたいことは同じなのに引数が違う。やり方は2パターンですかね。全ての実装クラスの引数を追加する。ただしこの引数は一切使わない。

もう一つは、IFを分ける。

クラス図2

どっちもよくないですね。IFの意味を成してないです。

これを解決するために、IFの引数にはDTOを引き渡すようにしてやります。
ただし、適当な「ServiceDto」みたいなモノを作ってしまうと、ServiceDtoの中には自分のクラスでは使わないフィールド変数が大量にできる可能性が高まります。
不要なフィールド変数はやはり消しておきたいです。

そこでIFの引数には総称型を使うようにし、業務部分がどの処理をするかを知っているので個別に専用のDTOを作って実体を生成してもらいましょう。

クラス図3

こんな形です。これの良いところは、先述の通りで改修にあたりIFの変更が生じた際でも、DTOのフィールドを変更することにより実体への影響を極力抑えることができます。ちなみに、上の図ではResponseもDTOに纏めています。戻り値が明らかにString1つや、booleanであることがわかる場合はDTOにしないほうがいいでしょう。これにより、戻り値が変更になった場合もIFの修正は不要になります。

異なる層はDTOで連携しますが、例えば同じService層のクラス同士でIFを呼ぶような場合は、DTOに変換してメソッドを呼び出さなくても良いと思います。同階層を変更する場合には、他のServiceを弄っても別にいいと思うからです。

DTOの数がかなり増えますが、オブジェクト指向を活かして改修に強くするためには仕方ないことだと割り切ってください。意味のわからないIFなどが出来上がるよりいいと思います。

層を跨ぐDTOはメソッドで初期化する。

先の方法に則ると、大量のDTOが出来上がります。で、DTOといえば、Setter/Getterメソッドなのですが、各Serviceに中継するDTOをSetter/Getterで生成していると、コードがやばいことになります。
無駄なSetter/Getterに埋め尽くされて意味がわからなくなると言うことです。

そこで、中継用のDTOの生成はDTOクラス内に配置したメソッドで値をつめて、インスタンスを返してやるようにしましょう。
大抵の場合、中継用のDTOは別のオブジェクトから必要なモノをかき集めて詰め替えるという作業になるためです。例えば以下のようなDTOにします。

クラス図4

この例では、USERID/USERNAMEを画面のフォームから受け取り、AGEをユーザー検索結果を詰めたDTOから受け取るパターンを扱います。

@Setter
@Getter
public class Dto{
 //せっかくだから、外部からのインスタンス生成を禁ずる
 private Dto(){
 }

 private String userId;
 private String userName;
 private String age;

 public static Dto initialize(HttpServletRequest req, UserSearchResponseDto uRes){
  Dto dto = new Dto();
  dto.userId = req.getParameter("userId");
  dto.userName = req.getParameter("userNamse");
  dto.age = uRes.getAge();
 
  return dto;
 }
}

こんな感じにしてやれば、DTOを作る側は必要な情報をinitializeに引き渡してやるだけでオブジェクトを生成できますから、Settet/Getter地獄からは解放されそうですね。
上のコードでは、ついでに外部からのDTOクラスのnewによるインスタンス生成を禁止しています。


そうすることで、後から改修に入ってきた人がinitializeを使わずにインスタンスを作るというヒューマンエラーを防げますね。

まとめ

2回分の記事をまとめると、以下のようにすることで人の入れ替わりの多い案件でも、修正時に要らぬバグを引くことを防げる上に、コードの独立性も上がりますから、生産性を高めることが期待できます。

  • 業務部分(Controllerクラスなど)は無理にコードを共通化しない
  • SOLIDの原則のうち、DIPとISPを重視する
  • DIPにおいてパッケージをまたいで情報を連携する際にはDTOにつめる
  • DTOの初期化はDTO内のメソッドで行う
###############お知らせ################
ブログランキングのITカテゴリに参加してみました。
この記事が役に立ったなどお力になれたら、 このバナーを押していただけると嬉しいです。

#####################################

-クラス設計