目次:
まえがき
ソフトウェアの品質で最も重要なのは発展性。発展性がビジネス価値を生み出す。良く聞く理論だが実際にどのようにソフトウェアを設計すれば良いのか。その具体的な方法論を、実装という設計行為を中心に考えてみたい。
発展性とはビジネスに合わせて変更できること。つまり修正と拡張である。ではどうしたら変更しやすい設計になるのか?次のように考えた:
- 変更の影響範囲を特定できる設計。
- 最小限の影響範囲で変更できる設計。
こういった設計を実現するためにオブジェクト指向という理論があるのだと筆者は捉えている。
オブジェクト指向が邪魔になる
とはいえソフトウェアを複雑さを回避し発展性に富んだものに仕上げるには時に、オブジェクト指向自体が邪魔になることもあるようだ。オブジェクト指向を施すにも1つのおまじないでできるわけではない。
ソフトウェアは現実の世界で役割を与えられた構造を持つ道具だ。だから人が使える具体的なものあだ。そこでオブジェクト指向という理論を実装しようとすると色々とおかしくなってくる。
大事なことは人が使うものを作っているということだ。
変更履歴
2021/04/30: 新規作成
小さい、捉えやすい、読みやすい
タイトルにオブジェクト指向という文字が入っていないのは、この理論よりも複雑さや冗長性を回避するにはどうしたらいいかを考えたほうが、より良いソフトウェアを設計できると考えたからだ。
というわけで最初にやるべきことはソースコードという文章を「小さく、捉えやすく、読みやすい」ものにすることから始めよう。
変更が難しい設計
その特徴:
- メソッドが長い。
- クラスが大きい。
- 引数が多い。
- 名前(クラスや変数)が分かりにくい
- 改行がない。
- 複数の目的がある。
- 同じ処理が別々のクラスにある。
それはこんなコードだ。読みづらいコードは書きづらいのでかなりストレスだ。
class Fee{ public Integer cost(int cst, int typ, Map<String, List<int, int>> itm){ // 送料 int adrr = 0; switch (type) { case 1: adrr = 1; break; case 2: adrr = 2; break; case 3: adrr = 3; break; default: break; } // 商品代金 int total = cost * adrr; int itemCost = 0; for(Map.Entry<String, List<int, int>> entry : item.entrySet()) { entry.getKey(); itemCost += entry.getValue().get(0) * entry.getValue().get(1); } Integer totalFee = total + itemCost; return totalFee; } } class DisplayFee{ public Integer displayCost(Map<String, List<int, int>> item){ Itenger itemCost = 0; for(Map.Entry<String, List<int, int>> entry : item.entrySet()) { entry.getKey(); itemCost += entry.getValue().get(0) * entry.getValue().get(1); } return itemCost; } }
ツッコミどころ満載のソースコードの出来上がり。わざわざ分かりにくいコードを書くのは非常にストレスフルな行為で難しいことがわかった。単純に分かりずらい文章はどんな職種の人間でも嫌がるだろう。そういう意味では今回の記事は誰にでもわかってもらえる内容かもしれない。
変更がし易い設計
特徴を引用:
・名前は略さず普通の単語を使う
・数行のコードを意味のある単位として「段落」に分ける
・「目的別の変数」を使う(1つの変数を使いまわさない)
・意味のあるコードのまとまり(段落)を「メソッド」として独立させる
・業務の関心事に対応したクラス(ドメインオブジェクト)を作る
これに倣って、コードをリファクタリングしてみよう。まずはFeeクラスだ。
import com.2501.ShippingFee; import com.2501.ItemsFee; // Feeは料金という意味で漠然としすぎている。 // 最終的に請求する料金という身でTotalをつけた。 class TotalFee { // 発送料と商品の代金が請求料金となるのでそれぞれ「目的別」の変数名とする。 Integer shippingFee; Integer itemsFee; Integer totalFee; ShippingFee shippingFee; ItemsFee itemsFee; public Integer getTotalFee(){ shippingFee = shippingFee.calcShippingFee(); itemsFee = itemsFee.calcItemsFee(); totalFee = shippingFee * itemsFee; return totalFee } } /** * 発送料データと振る舞いに関わるクラス */ class ShippingFee{ private static final float TAX = 0.1; Integer shippingBaseCost; Integer shippingAddressType; Integer shippingFee; public Integer calcShippingFee(){ return shippingFee } } /** * 商品代金データと振る舞いに関わるクラス */ class ItemsFee{ Integer quantity; Integer unitPrice; Integer itemsFee; public Integer calcItemFee(){ return itemsFee; } }
私たちは非英語圏のためどの単語を使うべきかは少し慎重になるべきだろう。FeeとPriceではどちらがいいのか?これは請求する側と請求される側で言葉が変わるとにも注意しなければならない。保育園から言われてきた「相手の立場に立って考えろ」は人類永遠の命題である。
思いのほか悩ましいクラス名、変数名の付け方
引用
筆者などは、基本的に英単語でつけるべきだと考えている。なぜならプログラミング言語が英語だから、全て英語の方が単純になるから。
SyouhinRyoukinでも良いのだが、ItemsFeeの方が引っ掛かりがない。最悪なのは一部の単語だけ日本語になること。可能な限り英単語とするべきだ。(それってあなたの意見ですよね?)
単語がわからなければGoogle翻訳を使えばいい。もちろんプロジェクトによって規約は違うだろうが私が決める立場にあるなら基本的に業務上の単語は英単語とする。絶対じゃないけどね。
しかし略することなく英単語を使うとどうしても名前が長くなりがちである。計算はCalcurationとなるが、これでは長すぎる名前になる可能性が高いので、calcとした。実際業務上の呼称を英単語に置き換えようとすると20文字くらいは軽く超えてくることがある。先にも述べた通りプロジェクトの規約に従うしかないかもしれないが、全てをとにかく3文字に略して名前をつけるなどはやめた方がいい。(それってあなたの意見ですよね?)
事実というより私の意見が目立ったが、読み易さを損なうことのない命名を心がけるのが良いだろう。
改行して段落を分ける。
単純に改行のない文章は読みづらい。というか読む気がしない。Feeクラスのままクラスを分けないにしても、やりたいことは2つはあるわけだから、やりたいこと毎に段落を分けた方が読み易い。
目的毎に変数を分ける。
同じ変数を使い回すことはここではしなかったが、それは所謂、破壊的代入というやつでやっては行けない。私もやったことがないと思っているが本当の最初は良くわからず入れたりしていたかも。
複数箇所で使う処理はメソッドとして抽出する。
FeeクラスとDisplayFeeクラスで商品代金を求める処理が重複しているので、商品代金専用クラスにした。
商品代金に関するデータとその計算式だけが与えられたItemsFeeクラスとなった。
こういった、業務上の関心事に合わせたクラスはドメインオブジェクトと呼ばれる。
むすび
初歩的なことをあえて間違った形でコーディングすると気持ち悪くてしょうがない。この気持ち悪さは初心者でもすぐに気がつくはずである。実はこの気持ち悪さが大事だと私は思っている。コードを見た時に感じる「スマートじゃない感」が今ままで何度もあった、思い返せばそれは全て合理的に文章が構造化されていない、あるいは整理されていないからなのだろう。
次の記事でも初歩的だがさらに、安全性も付け加えた方法をまとめていく。
参考書籍
現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 増田 亨 技術評論社
コメント