MVCの体裁を持った多層システムにおいて、Modelの時点で、いやもっというと、Modelを生成するSQLの中に、表示用の整形処理が書かれていることがある。これは、ModelとViewを分離するというMVCの目的から大きく逸脱している為、アンチパターンと言える。
例えば画面に日付を表示する際に、DBの日付型フィールドの値を次のように整形して表示するシステムがあったとする。
2023年03月30日
この時、Modelのフィールド定義が、日付型ではなく文字列型になっているケースがある。
public class EmpModel {
public string BirthDate {get; set;}
}
そして、このMyDataを取得するためのSQL上に、こう書かれている。
SELECT to_char(birthdate, 'YYYY年MM月DD日') AS birthdate from mydata
このEmpModelを受け取ったViewは、このBirthDateをそのまま表示するだけだ。
何が問題なのか
「必要な結果が取得できているのだから問題ないじゃないか」と思う人もいるかもしれない。
それどころか、「SQLを修正するだけで画面表示まで変えられるなんてすごく楽だ」とまで思うかもしれない。
しかしそれは逆で、「SQLを修正すると画面表示にまで影響が出てしまう」のである。
私が遭遇した事例にこういうものがあった。
上記のBirthDateは、いろんな画面から利用されていたのだが、私はある時、画面上で「YYYY年MM月DD日」で表示されている箇所をすべて「YYYY/MM/DD」に変えてほしいという要望を言い渡された。
そしてコードを見て愕然とするのだが、「まぁ、SQLのこの箇所をいじればよいだけならそれでよいか」と思い直し、要望通りにSQLを次のように変更した。
SELECT to_char(birthdate, 'YYYY/MM/DD') AS birthdate from mydata
しかし、そのあと、謎の障害が発生する。ある画面の日付表示が行われないというのだ。
その画面を見てまた愕然とした。なんとその画面では、BirthDateからyear, month, dayをそれぞれ取り出す為に、"年"と"月"の位置からYYYY, MM, DDの数値を検出して処理し、その結果を日付型として用いて「翌月」の日付を算出し、画面表示を行っていたのである。
今回の変更で"YYYY年MM月DD日"ではなく"YYYY/MM/DD"でデータが取得されるようになったため、上記処理が正常に動かず、日付表示が行われなかったという障害であった。
同様に、ある帳票では"YYYY-MM-DD"形式で日付を出力するために、"年"と"月"を"-"に置換して出力していた為、この処理が正常に動かず、"YYYY/MM/DD"で出力されてしまっていた。これに気付くのはかなり後になってからだった。
どうすれば良かったのか
モデルのクラスは、日付型のデータであれば日付型にすべきである。
public class EmpModel {
public DateTime BirthDate {get; set;}
}
もし、このBirthDateがnullになる可能性があるのであれば、Nullable DateTimeとすればよい。
public class EmpModel {
public DateTime? BirthDate {get; set;}
}
表示整形処理はViewの責務である。
Viewは、受け取ったEmpModelのBirthDateを、日付整形処理ライブラリなどを用いて必要な形式へと変換する。
Webシステムであれば、moment.jsを使って次のように変換可能だ。
moment(model.birthDate).format('YYYY/MM/DD');
上記の整形処理を共通ライブラリに定義し、各画面で使いまわせば、共通フォーマットで出力したいときはこの共通処理を呼び出せばよいし、例外的なフォーマットで出力したいときは個別にmomentを呼び出せばよい。
また、日付を日付型として計算したい場合にも、そのまま処理すればよい。
カンマ区切りの数値、入力がある例、etc...
他にも、次のようなアンチパターンがあった。
- 数値型のフィールドをSQLの時点で3桁ごとのカンマ区切りにしてモデルに保持し、画面までもっていくようなパターン
- さらにその値を画面から入力させ、カンマ区切りのままサーバへ送信して処理するパターン
この画面ではバグが酷く、調べると、処理のタイミングによってその数値用の変数にカンマ区切りの文字列が入っていたり、カンマを取り除いて数値型に変換したデータが入っていたりとカオスであった。
その回避の為であろう、計算前には必ず変数の値を一度「カンマ区切り→数値」変換する関数を通し、画面表示前には再度「数値→カンマ区切り」にする関数を通す、ということをすべての処理に挟んでいた。しかし、それらの処理は改修の際に忘れられたりして、様々な箇所でバグを生み出していた、というわけである。
私はこれをすっきりさせるため、「数値フィールドをバインドすると、フォーカスがあるときはカンマ区切り無しで表示/入力し、フォーカスが外れるとカンマ区切りで表示するinput」のVueコンポーネントを作成した。もちろん、バインドされた数値フィールドはカンマ区切りで汚染されることは一切ない。空入力の際にはnullにするか0にするかをプロパティから設定できるようにもした。そして、モデル側は常に数値型でサーバと画面側のやりとりを行うように変更した。
つまり、「表示や入力のための煩わしい整形処理は、Vueコンポーネントの形でビューの中に閉じ込めた」のである。
この結果、あらゆる処理の見通しがよくなり、大量のコードが消え去るとともに、謎のバグの発生も収まった。
日付や数値のパーズをサーバ側で行わなくてはならない場合
「入力された日付文字列が正しい日付文字列かどうかをサーバ側でチェックするために、日付フィールドは文字列としてサーバに送信しなければならない」
という人もいるかもしれない。数値型も同様に、である。
それが不可避であるならば、それは画面の機能要件であるから、モデル(又はViewModel)のフィールドも文字列で持つのが妥当である。
しかし、おそらくほとんどのケースにおいて、日付のフォーマットチェックや数値のフォーマットチェックは画面側で完結できる。
日付自体の範囲チェックや存在チェック、数値の範囲チェックなどは、それぞれ日付型や数値型に変換した後、サーバ側でそれらを正しい型で受け取ってから行えばよい。(正しい型へ変換できなかった場合は、異常として検出する)
ViewModel
Modelを直接Viewから参照せず、ViewModelを介するように実装することは多いと思う。ここで言う「ViewModel」とは、画面から参照するためのいくつかのModelからの変換結果(写像)である。
例えば、画面に「社員一覧」を表示する際に、所属部署を表示するためにEmpModel.Dept.Nameが必要だからといって、EmpModelにDeptModelまで読み込んで接続し、画面に渡すのはパフォーマンスが悪い。
そのために、EmpViewModelを定義し、そこにDeptNameフィールドをつけることで、余計なデータを省き、必要なデータのみ渡す。
この時、ViewModelを画面の要件に合わせすぎるあまりに、この記事でいうアンチパターンでViewModelを実装してしまうケースがみられる。つまり、画面表示が「社員名(部署名)」だった場合に、()を含めた文字列をサーバ側で生成してEmpViewModelに設定してしまうケースがあるのだ。フィールド名は「EmpName」のままだったりする。
こういう実装はイージーであるが、後々、この記事の「YYYY年MM月DD日」を「YYYY/MM/DD」に修正した時のような地獄が待っている。
この例であれば、EmpNameとDeptNameをEmpViewModelに定義し、それを「社員名(部署名)」の形式で表示するのは画面に任せた方が柔軟な対応が可能なことが多い。
{{empName}}({{deptName}})
ViewModelに格納するデータ形式は、画面表示そのままである必要はない。可能な限り、画面に表示するための「生のデータ」を保持し、それをどう整形するかは基本的には画面に任せた方が、様々な面で便利だ。
但し、常に生のデータを画面に渡せばいいというわけではないので注意が必要だ。表示に必要のないデータまで画面に渡せば、それらはセキュリティ要件やコンプライアンス要件に引っかかることもある。
また、「表示整形をサーバ側で一元管理したい」ケースもあるにはあるだろう。
まとめ
- 表示のための整形処理にModelが煩わされてはいけない
- 数値は数値型、日付は日付型として一貫して扱う
- ViewModelだからといって画面表示用の整形結果を(必ずしも)保持しなくてもよい
- 上記を覆す上位の要件があるならその時改めてそれに従う
あなた自身のコードがこのようなアンチパターンに陥っていないか、目の前のイージーな結果を得るためにコードの簡潔さを逆に失うことになっていないか、改めて見直したい。