リスコフの置換原則とは

リスコフの置換原則とはオブジェクト指向の設計の原則の一つであり、「基本クラスをサブクラスに入れ替えても問題なく動かなけらばならない」という原則です。
もしサブクラスに入れ替えると正しく動かなくなるのであれば、クラスを利用する側はサブクラスを使用してはならないというのを意識しながらコーディングしなければならず、保守性が低下します。

クラスの入力・出力に目を向けると、以下の条件を満たすとリスコフの置換原則を満たすことができます。

1.サブクラスの入力のパターンは、基本クラスの入力のパターンよりも緩い

2.サブクラスの出力のパターンは、基本クラスの入力のパターンよりも厳しい
 

もし、サブクラスの入力のパターンが基本クラスの入力のパターンよりも厳しいと、利用する側はサブクラスを利用する際にサブクラスが許容する入力であるかどうかを意識する必要が出てきてしまいます。
また、サブクラスの出力のパターンが基本クラスの出力のパターンよりも緩いと、利用する側はサブクラスを利用する際にサブクラスが予期せぬ出力をしないか意識する必要が出てきてしまいます。

以下のサンプルコードは、割り算を行う基本クラスであるDivideクラスを、サブクラスのDivideBusinessクラス・DivideBad1クラス・DivideBad2クラスで入れ替えてみるサンプルです。

DivideBusinessクラスは、基本クラスが許容する入力パターンに加え分母=0のパターンも許容し、基本クラスが出力することがある負数を出力しません。Divideクラスと入れ替えても特に考慮するパターンが増えることはなく、リスコフの置換原則を満たした良いクラスです。
しかし、DivideBad1クラスは、基本クラスが許容する分子=負数のパターンを許容せず、基本クラスと入れ替えた場合は分子が負数になっていないか意識する必要が出てきてしまいます。
また、DivideBad2クラスは、基本クラスと異なりnullを出力するパターンがあり、基本クラスと入れ替えた場合は出力がnullになる場合を意識する必要が出てきてしまいます。

【サンプルコード】

・DivideMain.java

・Divide.java

・DivideBusiness.java

・DivideBad1.java

・DivideBad2.java

【結果】


いかがでしたでしょうか。

ポリモーフィズムを利用するとソースコードを簡潔にできるのですが、それを実現するためにはリスコフの置換原則を満たすことが必要です。
デザインパターン等で良い設計に触れている方なら自然に満たせるものかもしれませんが、今一度意識してみるのも良いかもしれません。

単一責任の原則とは

オブジェクト指向の設計ではいくつかの原則があります。
その中でも有名な原則の一つが「単一責任の原則」です。

この原則は、その名の通り「一つのクラスに持たせる責任は一つにする」という原則です。
この原則が何のために用いられるのかと言うと、複数の理由で1つのクラスを修正するのを防ぐためです。
ここで言う「理由」を「案件」、「クラス」を「ソース」と捉えると分かりやすいと思います。
複数の案件で1つのソースを修正する必要が出てしまうと、並行開発手順やバージョン管理等が煩雑になってしまい、品質に悪影響が及びます。

「一つのクラスに持たせる責任は一つにする」と捉えると具体的なイメージが浮かびにくいと思うのですが、「複数の案件で1つのソースを修正しないような粒度で設計する」と捉えると分かりやすいと思います。


以下では、javaのソースコードで例を出します。
商品の名前と値段を登録し、それを表示するという例です。

まずは悪い例からです。

【サンプルコード1】

・CommodityMain.java

・RegistShowCommodity.java

【実行結果1】


ここで、「商品の税込み価格を計算可能にする」という案件と、「表示を綺麗にする」という案件が発生したとします。
今の設計だと、どちらの案件も RegistShowCommodityクラスの修正となってしまいます。

そこで、商品の登録を行う RegistCommodityクラス、商品の価格計算を行う CalcCommodityクラス、価格表示を行う ShowCommodityクラスの3クラスに分割します。

【サンプルコード2】

・CommodityMain.java

・RegistCommodity.java

・CalcCommodity.java

・ShowCommodity.java

【実行結果2】


3クラスに分割した結果、「商品の税込み価格を計算可能にする」という案件は CalcCommodityクラスの修正、「表示を綺麗にする」という案件は ShowCommodityクラスの修正で対応できるようになり、別々の案件で1つのクラスを修正する状態を防ぐことができました。

【サンプルコード3】

・CommodityMain.java

・RegistCommodity.java

・CalcCommodity.java

・ShowCommodity.java

【実行結果3】


いかがでしたでしょうか。

オブジェクト指向の設計と言うと覚えることが多くて大変だという印象を持つかもしれませんが、いくつかの原則を抑えるだけでも良い設計ができます。
(デザインパターンも、原則に則って設計を考えると自ずと導き出されるものだと思っています)

単一責任の原則は代表的な原則の一つなので、覚えておいて損はないと思います。

「現行機能の踏襲」という要件定義のまずさと、苦し紛れの対応策

私自身は幸運なことに、要件が「現行機能の踏襲」であるプロジェクトに携わったことはないのですが、要件がこれに近いプログラムをプライベートで作成しているので、参考までにその経験を記事として残します。


私が作成しているプログラムはとあるゲーム(RPG)の戦闘部分の計算を行うシミュレータであり、ゲーム攻略のために作成しています。
要件としては下記であり、同じ計算が行われるということ自体に価値があるプログラムです。

・ゲームで行われる計算と同じ計算を実装する

・諸事情によりゲームを解析することはできない

・ゲームには場当たり的な対応やバグが多数含まれており、それも仕様として実装する

 

これを実務に置き換えると以下のような要件であり、限りなく「現行機能の踏襲」に近い状況です。

・現行システムと同じ機能を実装する

・諸事情により現行のソースコードを入手できない

(他ベンダーが開発、ソースコード紛失、等の理由で)

・場当たり的な保守・運用が多数行われており、それも仕様として実装する


私が作成しているプログラムで最も複雑な部分は、攻撃手段毎のダメージ計算式です。
攻撃手段は数百種類存在し、それぞれ異なった計算を行う必要があります。

本来であれば、各々の攻撃手段毎で共通するルールが存在するため、そのルールを抽象化して実装することで保守性を確保することができます(図「本来あるべき姿」)。
(「本来」というのは、「シリーズ初期は」や「運用当初は」といった言葉で置き換えてください)

しかし、実態としては、場当たり的な対応が行われる過程でそのルールを逸脱する計算が行われる攻撃手段が存在します(図「実態1」)。
また、実装の誤りにより、攻撃手段によっては一部計算式が異なる物もあります(図「実態2」)。

ゲームを解析することができれば、既存のソースコードをそのままコンバートしたり、ロジックを真似ることで現行のソースコードと同程度のレベルで抽象化したりすることができるのですが、今回はその手は使えません。
更に、実装を始めた段階では現行のゲームの全ての挙動が判明しているわけではなく、プログラムを作ってみて初めて判明する挙動というのも存在するため、このことも開発の難易度を高める要因となっています。
つまり、十分な抽象化が行えない状況で、将来の変更(新たに見つかった挙動への対応)を見据えたプログラミングを行う必要があります。

私が作成しているプログラムでは、下記のような対応をとっています(図「対応策」)。

1.攻撃手段毎にクラス分けする

一番避けなければならないのが、攻撃手段の分岐を計算式中で多様することでソースコードが複雑化し、保守困難になることである。
そこで、ルールに従った抽象化を行わず、攻撃手段毎にクラス分けした。
重複した記述は多くなるが、ある攻撃手段に対する変更が他の攻撃手段に影響を及ぼすのを避けることはでき、攻撃手段毎の処理内容を追うことも容易になる。
(なお、小規模な開発である場合、「クラス」は「関数」に置き換えても良い)

2.各々のクラスで使用する計算式は部品化する

ダメージ計算式は分解可能であるため、分解した計算式を部品(共通関数)として用意し、その部品を組み合わせることで各々のクラスの実装を可能とした。
攻撃手段毎の微妙な計算式の違いを、部品の組み換えや追加で吸収することが可能になり、保守性が向上する。

このプログラムは公開してから(使い物になる状態になってから)数年経っていますが、今の所は保守困難な状態にはなっていません。
しかし、十分に抽象化を行えなかった影響で、保守性は正直良くありません。
攻撃手段の数だけコピペが必要になるような対応も何回か発生しています。


「現行機能の踏襲」という要件はビジネス上の観点から見ても問題がある(業務改善ができない)と思いますが、実装上も

・共通ルールを見つけるのが困難で、十分な抽象化を行えない

・作ってから初めて明らかになる仕様があり、変更を見こさなければならない

 

という困難さがあり、実装を工夫したとしても十分な保守性を確保するのは難しいです。
保守性が悪いということは、将来の開発でQCDを確保することも困難になります。
「現行機能の踏襲」という要件は、開発コストの低減やリスクの低減に繋がらないと考えた方が無難だと思います。
今回の例のように、現行機能通りにすること自体に価値があるのであれば別ですが、そうでなければ、要件を「現行機能の踏襲」とすることには慎重になる必要があります。


プライベートでもプログラムを作っているので、その中で仕事に活かせそうなスキルや経験が得られることがあります。
また何かあれば、記事にしていきたいと思います。

WindowsPowerShellでHelloWorld

WindowsPowerShellとは、Windows7以降に標準搭載されているスクリプト言語です。
比較的新しい言語であり、従来のWindowsバッチ(cmd.exe)に慣れていると取っつきにくい面もあるのですが、後発である分高機能であり、実務でも使うことがあります。

今回は、WindowsPowerShellの簡単な説明を添えながらHelloWorldを3つ試してみます。


1つ目は、echoコマンドを用いたHelloWorldです。
WindowsPowerShellでは、従来のWindowsバッチやLinuxのBashシェルに近い感覚で処理を書くことができます。
他には、変数やif文やパイプといった、お馴染みの書き方が可能です。
ちなみに、少し変わった所では、例外処理をtry-catchやthrowといった書き方で記述することも可能です(こちらはjavaやC#に近いです)。

なお、WindowsPowerShellは、セキュリティ上の関係で、標準設定では直接実行することができません。
設定を変えずに実行するためには、一時的にポリシーを変更して実行する必要があります。
(今回の例では、Windowsバッチを介して実行します)

【サンプルコード】

・Hello.ps1

・Hello.bat

【実行結果】


2つ目は、コマンドレットを用いたHelloWorldです。
コマンドレットは、「動詞-名詞(-パラメータ名 パラメータ値)」という規則で命令を記述します。

今回は、”Write-Host”を使用します。
“Write”は文字列の書き込み、”Host”はコンソールを意味します。
(なお、WindowsPowerShellでは、コンソールへの出力とストリーム(パイプ)への出力は別々に制御します。ストリームに出力したい場合は、”Host”の代わりに”Output”を指定します。)

【サンプルコード】

・Hello.ps1

※Hello.batは変更無し

【実行結果】


3つ目は、.NetFrameworkの機能を使用したHelloWorldです。
WindowsPowerShellでは、.NetFrameworkの機能を使用することもできます。
例えば、”Hello World!!”と書かれたポップアップを表示することができます。

今回の例では、コマンドレットでWindowsFormを取り込み、MessageBoxクラスのShowメソッドを使用してポップアップを表示します。
記述方法はサンプルコードの通りですが、クラス名とメソッド名の記述方法はC++に似ています。

【サンプルコード】

・Hello.ps1

※Hello.batは変更無し

【実行結果】


いかがでしたでしょうか。

Windows7の前の時代からIT業界に居た方にとっては、WindowsPowerShellはもしかしたら馴染みが薄いかもしれません。
また、標準の設定では直接実行できない、というのも取っつきにくいと思います。

しかし、コマンドレットや.NetFrameworkによって提供される機能は強力で、実務でもWindowsPowerShellのスクリプトを見かけるようになってきています。
なので、WindowsPowerShellにも慣れておいた方が無難ではないかと思います。

便利な機能があれば、今後紹介していこうと思います!