要求とポリシーの分離
引き続き新装版 達人プログラマー 職人から名匠への道を読んでいます。
要求とポリシーの分離についての章を読んで猛烈に反省しました。
達人プログラマーに示されている例を出発点にして、過去に自分が「やっちまった」ミスを振り返り、理解を深めたいと思います。
達プロの例
きれいな要求の例:
従業員レコードは、あらかじめ決められた人々にしか閲覧できない
これはそもそもクライアント(ユーザ)が「従業員の監督者と人事部だけが従業員情報を閲覧できる」と依頼されたのかもしれない。
この依頼のままだとビジネスポリシーが埋め込まれてしまっている。
ビジネスポリシーはいつか変更される可能性がある。
要求とポリシーは個別にドキュメントにし、ハイパーリンクで関連付けしたほうがベター。
要求は一般的な記述にし、ポリシーはメタデータとしてアプリケーションに提供するようにしよう。
要約すると上のような内容が書いてあります。
実装に活かす部分としては、
- 特定の情報は「特定の人」だけが見れるようにし
- ユーザが「特定の人」がどうかはランタイム時に都度判別されるようにする
- どの範囲のユーザが「特定の人」に該当するかは設定ファイルなどで調整できるようにしておく
のようにまとめることができると思います。
この逆は閲覧ユーザの属性(監督者||人事部所属)をプログラム内にハードコーディングしてしまうことです。
当然後者のほうがコーディングは簡単だし、頭を使わなくても手っ取り早く実装できるのですが、ポリシーが変更になった際に修正が必要になります。
依頼から要求を切り出す際に、そこからポリシーを切り分けるところまで注意を怠らないようにするのが難しいと感じました。
依頼そのものが複雑だとそれを紐解くだけで力尽きてしまうことが自分はよくあります。
また、依頼主がなぜその依頼をピックアップしたのかを完全に理解しきれないこともあります。
そのような状態でコードを書き始めると、ポリシー(つまり簡単に変更される部分)を見抜くのは困難です。
これらは作業の見積もり段階/インタビュー時の詰めが甘いことが原因なので、半分はコミュニケーション力の問題とも言えます。
難しいのは分離
自分がやってしまいがちなBADな例を示します。
まず、下に示す箇条書きの状態までクライアントの依頼を分析できたとしましょう。
- ネットショップを運営しているクライアントが、ショッピングモールを展開するA社とB社に出店している
- A社、B社共にCSVフォーマットで商品を管理している
- それぞれのCSV中にある、
A社:商品ID
とB社:商品CODE
で商品を関連付けることができる -
A社:販売価格
の値とB社:セール価格
の値は一定のルールで変換できる - クライアントはA社のフォーマットを使って商品を登録している
- システムで定期的にA社のフォーマットを読み込んで、B社のフォーマットに販売価格の値を反映させたい
これをそのまま、しかもひとつのプログラムに詰め込んでしまうことが(むしろ簡単に)できてしまいます。
# 全ての処理をひとつのPythonプログラムに書き込み、
# コマンドラインでデータファイル(CSV)を与える
$ python bad_calc_sale_price.py items.A.csv items.B.csv >items.B.sale_price.csv
bad_calc_sale_price.py:
# -*- coding: utf-8 -*-
# $ python bad_calc_sale_price.py <A社の商品CSV> <B社の商品CSV>
# -> B社:セールス価格が計算済みの状態になっているCSV
# (簡単のためエラー処理はしていません。)
import sys, csv
_, a_csv, b_csv = sys.argv
def csv2price_dict(csv_fname, item_id_field, price_field):
'''
CSVファイルから
商品を識別できるフィールドをキーに
商品の価格を値にした辞書を作って返す
'''
with open(csv_fname, "rt", encoding="utf-8") as f:
cin = csv.DictReader(f)
item_id_price = dict()
for row in cin:
item_id_price[row[item_id_field]] = int(row[price_field])
return item_id_price
a_item_id_price = csv2price_dict(a_csv, "商品ID", "販売価格")
b_item_id_price = csv2price_dict(b_csv, "商品CODE", "セール価格")
def calc_sale_price(src_price):
'''
B社で使用するセール価格(1割引、四捨五入)を計算する
'''
return round(src_price * 0.9)
cout = csv.writer(sys.stdout)
# ヘッダーを出力
cout.writerow(["商品CODE", "セール価格"])
# B社CSVにすでに商品が存在していて、
# セール価格が現在のA社価格から計算したものと一致するなら
# 出力をスキップする
for a_item_id, a_price in a_item_id_price.items():
b_current_sale_price = b_item_id_price[a_item_id] if a_item_id in b_item_id_price else None
b_new_sale_price = calc_sale_price(a_price)
if not b_current_sale_price or b_current_sale_price != b_new_sale_price:
cout.writerow([a_item_id, b_new_sale_price])
このコードの寿命は大雑把に言って下記のアサーションが成功する期間となります。
assert(入力フォーマットはCSVである)
assert(A社フォーマットがソースである)
assert(B社フォーマットが変換先である)
assert(B社で使用するセール価格は、A社の販売価格に対して1割引かつ小数点以下は四捨五入で計算した価格である)
assert(出力は差分だけで良い)
書き出そうと思えばもっと書けますがこのくらいにしておきましょう。
このコードへの改善提案としては次のようなことが言えると思います。
- 入力フォーマットがXMLやJSONに変更されても必要な項目が抽出できるようにデシリアライズするレイヤーを設ける
- 出力に関しても任意の形式にシリアライズするレイヤーを設ける
- 入力データの項目名や出力結果の項目名を任意に設定できるようにする
- 価格の計算に使う設定値を任意に設定できるようにする
- 出力データの条件指定を任意に設定できるようにする
つまり「ある値をベースに別の価格を計算する」という十分に抽象化された動作のみを要求と捉え、変更されることが容易に想像できる部分はメタデータとして捉えるわけです。
自分が難しいと感じるのは、常日頃からこのような視点で依頼を分析し、コードが柔軟であるようにリファクタリングを継続的に行うことです。
怠惰なコーディングに慣れてしまっていると、こういうストイックなやり方を常識と言えるようになるまでかなりの精進が必要かと思います。
反省
自分自身が要求とポリシーを偶然に任せて分けたりわけなかったりしてきた結果、上の例のような要リファクタリング案件を複数抱えている状態です。
これ以上余計なタスクを未来の自分に押し付けるような真似を繰り返さないためにも、これから書くコードでは気をつけて|よく考えて|ちゃんと整理して行きたいと思います。
テクニック、今のところは
何が要求で何がポリシーかを見極める方法として、
- 気をつける
- よく考える
- ちゃんと整理する
では何の具体性もないなと半日考えてました。
そんな中G.M. ワインバーグのコンサルタントの道具箱を読んでいてはっとさせられる内容に出会いました。
「不変の言葉」という章で、絶対的と感じてしまう言い回しに「今のところは」を付けると冷静に内容を反芻することができるというような内容です。
つまりこうです。
- 従業員の監督者と人事部だけが従業員情報を閲覧できるように設計してください、今のところは
- A社のCSVをソースにしてB社のデータを作成してください、今のところは
内容的にどの部分に「今のところは」を適用するのかは文脈と状況によると思いますが、このひと工夫だけで「あ、この仕様は絶対ではないんだな」「変更に備えなくちゃ」と冷静に考えてみる心の余裕が生まれると思います。
顧客の依頼に対してそれがカチッと決まっていて絶対なものだという思い込みをさっさと取り除けるようになると、要求とポリシーを分離する作業に思考のフェーズを移しやすいと感じました。