はじめに
上記の記事の別のパターンとなる。
繰り返しになるが、MVCの体裁を持った多層システムにおいて、Modelの仕様が表示仕様に強く影響されているケースがある。ViewがModelに依存するのではなく、ModelがViewに依存してしまっているのだ。これは明らかな アンチパターン であるが、割とこうなってしまっている現場を良く見る。
このようなコードを書いてしまう原因は、「なぜModelとViewが分かれているのか」を考える余裕がなかった人たちがコーディングしているからだと思われる。
そういう人たちは、「なんでSQLで抽出したものをわざわざModelなんていう箱に入れて、ビューまで運んで描画するなんて面倒なことをしているんだ」と思いながらコードを書いているのかもしれない。
なぜそんな面倒なことをしているかというと、「システムの変更による影響範囲を極小化するため」だ。 仕様が変わりやすいビューの変更が意味もなくモデルの変更にまで影響してしまうのは極力避けたい。モデルは様々な箇所から参照されている可能性が高く、変更に対するコストが高い為、仕様を極力安定させる必要がある。ビューを変更したいからモデルを変更する、なんて状態は、本末転倒もいいところなのである。
コードが作りっぱなしで良かった時代はとうに終わっており、今は継続的なシステムの改善が続いていく「資産としてのコード」が強く意識される時代となっている。モデルを抽象化し、ビューの仕様とはしっかり切り離さなければならない。
今回は、金額の単位変換をビューではなくモデルの抽出側(SQL)で行ってしまっているパターンについて解説する。
金額を千円単位で表示する
部署別売上がSalesByDeptクラスのAmountプロパティに格納され、画面に渡される。
この金額は、データベース上ではもちろん円単位で格納されているのだが、表示は「千円」単位にしたい。
これに対し、あるシステムでは次のようになっていた。
public class SalesByDept
{
public int DeptNo {get; set;}
public int DeptName {get; set;}
public decimal Amount {get; set;}
}
SELECT
deptno
,dept.name AS deptname
,ROUND(SUM(sales.amount) / 1000) AS amount
FROM
sales
LEFT JOIN dept ON sales.deptno = dept.deptno
GROUP BY
sales.deptno, dept.name
<span>{{item.amount}}千円</span>
つまり、DBから取得する時点で1000で割って千円単位にし、SalesByDeptのAmountプロパティには千円単位になった値が入っている。
表示側は、渡された金額に「千円」を付けてそのまま表示するだけで良いから楽だ、ということだろう。
しかし、「千円単位で表示したい」は画面側の表示要件であり、このシステムでの金額は「円単位」で金額を扱っている。
モデルがビューの仕様に引きずられている状態だ。ビューの仕様というのは基本的にコロコロ変わるものだが、モデルは本来、そうコロコロ変わらない方が望ましい。
案の定、ある日クライアントから次のような指示が下りた。
「今、千円単位で小数点以下は切り捨てて表示してもらってますが、小数点第一位まで表示するようにしてください」
お客さんはまさか画面側には既に千円単位に丸められた値しかないとは思わないので、簡単な修正だと思って指示を出しているのだが、実際にはサーバからデータを取得する箇所から修正しなければならない変更だ。
とはいえ「今回はSQLを変えるだけでいいから楽だな」と思っただろうか?
SELECT
deptno
,dept.name AS deptname
- ,ROUND(SUM(sales.amount) / 1000) AS amount
+ ,ROUND(SUM(sales.amount) / 1000, 1) AS amount
FROM
sales
LEFT JOIN dept ON sales.deptno = dept.deptno
GROUP BY
sales.deptno, dept.name
しかし、実際にはここから事態は大きく動いた。
この画面では、上記のAmountプロパティを全部署分合計した値を表示していた。
その金額が、修正前と後で変化したのである。
例えば部署AのAmountが1000、部署Bは2000、部署Cは3000、だったとする。
合計は6000となる。
しかし今回の変更により、これが1000.3、2000.4、3000.5となった。
合計は6001.2だ。小数点以下第一位どころか、一の位が変わってしまっている。
これを見て、クライアントは言った。
「あれ?この合計って、ちゃんと千円単位にする前の値を合計してるんですよね? どうして前は6000だったんですか?」
残念ながらこの合計は千円単位にする前ではなく千円単位にした後の合計金額である。
丸めた後の金額を合計しているので、誤差が生じていたのだ。やってはならないやり方である。今回の変更でその問題が露呈した形だ。
ちなみに私が作った処理ではない(海外オフショアの成果物)のでどうかご勘弁願いたい。
このように、「表示仕様」に併せてサーバ側でデータを加工してしまうと、その後の処理が「表示仕様」といううつろいやすいものに強く依存し、その後の修正箇所が増大してしまう上に、今回のような問題も生じてしまいがちだ。
あるべき形の実装は次の通りとなる。
SELECT
deptno
,dept.name AS deptname
- ,ROUND(SUM(sales.amount) / 1000) AS amount
+ ,SUM(sales.amount) AS amount
FROM
sales
LEFT JOIN dept ON sales.deptno = dept.deptno
GROUP BY
sales.deptno, dept.name
roundAmount(amount){
// Decimal.jsを使用して千円単位(小数点第一位まで表示)に整形
return Decimal(amount).div(1000).toFixed(1);
}
<span>{{roundAmount(item.amount)}}千円</span>
item.amountに格納されている金額は生の金額となる為、その合計計算も正しい値となる。
合計金額もroundAmountを通せば千円単位で表示できる。
「そもそも合計処理をサーバ側でやっておけばよい」という考え方もあり、それももちろん正しいのだが、古いシステムのようになんでもかんでもサーバで処理して「画面側は結果を表示するだけ」というやり方は、ウェブシステムのような動的なクライアントと合わないことが多い。
例えば、条件に応じて画面の表示項目をオンオフしたい、という場合にも、JavaScriptで対応できるはずのことが、画面側で必要な情報を持っていない為に毎回サーバへ問い合わせが必要になってしまう。
ちなみに別の箇所では、項目の背景色を決める「Color」というデータをModelに持ち、そのColorの値はSQLのCASE文で決めているものも見つけてしまい、頭が痛い。