概要
掲題の通りです。異論は認めますだからオブジェクト指向警察の皆さん見逃して下さいお願いします。
この投稿は「オブジェクト指向(OO/ object oriented)ようわからん」って人向けになるべくわかりやすく説明しようとする試みになります。一応は「1冊くらいは入門書読んだ人」を対象にしています。
ちなみにぼくのオブジェクト指向力は100メートル走で例えると多分12~13秒台くらいです。よくわからないけど。
オブジェクト指向は難しい?
初めてプログラミングに触れてオブジェクト指向について学び始める時、その概念を理解するのに苦労してる方は結構多いのではないかと思います。カプセル化だとか、ポリモーフィズムだとか、よくわからないアカデミックな名称が次々と出てくるのに比べ、実践的にはどうすれば良いかの説明に関しては結構貧弱な書籍が多いというのが理由のひとつだろうなと思ってるのですが、その大きな理由として、別にOOを理解してなくてもシステムは作れる って点にあるのだろうと思っています。
実際、OOを守らなくてもシステムは作れてしまいます。プログラミングにおいてOOは必須ではありません。
むしろ全体のステップ数が1kstepを下回るような小さなプログラムの場合、OOの恩恵はほとんど受けることができません。
(といっても最近の言語はOOを取り込んで設計されているものがほとんどなので、知らなくても実際には恩恵を受けているのですが)
そうなると、入門書ではまずプログラミングに必須な内容(変数だったり、クラスだったり)を詳しく教えることに注力され、OOに関しての詳しい説明の優先度はどうしても下がってしまいます。特に入門書のような100step程度のごく小規模なコードを扱う場合、手続き型 で書く方が可読性が高くなります。入門書のコードの場合1行ずつ処理の意味を説明していくというのが一般的かと思いますが、OOに沿って書いていると、その度にOOの説明を入れなければならなくなり、ノイズにすらなってしまいます。同様の構造は研修やプログラム講座にも当てはまります。
ただ悲しいかな、大きなシステム、特にイテレーションを回して作っていくシステムでは、OOの知識は必要不可欠です。そして最近はそういうプロジェクトが日本でも増えてきています。
なのに一番最初に学んだ時にOOについて曖昧な状態で学んだりしたものだから、今までの書き方や考え方の違いの吸収に苦労しているという人は一定数いるんじゃないかなぁって思います。
別に入門書や研修や講座を批判してるわけではありません(限られた時間やページ数で多くのことを教えないといけないのだし、仕方ないんだろうなと思ってます)が、最初の学びとのギャップが、OOがなかなか理解しづらい概念という印象を与えてしまっているのではないでしょうか。
オブジェクト指向とは(入門書に書かれてる内容)
だいたいこんなことが書かれています。 最後の以外はだいたいは確かに合ってるような合ってないような、でも多分合ってるんだろうというような気がします。
- オブジェクト指向とはすべてを「物(=オブジェクト)」として捉える概念
- オブジェクト指向で書くことで「疎結合」が実現でき、「再利用性」や「保守性」や「拡張性」が高まる
- それらが高まることで「生産性」が上がる
- オブジェクト指向は以下の要素で成り立っている
- カプセル化
- 継承
- ポリモーフィズム
-
変数は外部からアクセスできないようにし、getter / setterを用いてアクセスする- 結局のところgetter/setterは要るのか?要らないのか? 参照
- ぼくは「なるべくなら使わない方がいい」派です。ただ使ってるの見ても別にまぁ仕方ないよねぇ程度の温度感です。
これらをパっと読んでどう書けばいいのか完全に理解できる人、すんごい才能あると思います。抽象的な言葉が並んでて、よくわからないですよねぇ。
オブジェクト指向とは(根本的な概念)
ここでは難しいことは言いません。これだけおぼえておいたら、多分上記の内容を勉強する時にも結構すんなり入ってくるんじゃないかなって思います。
オブジェクト指向は「整理術」
はい、皆さんご一緒に。
「おぶじぇくとしこーはせーりじゅつ!」
なぜ「整理術」なのか
実際に「オブジェクト指向誕生の歴史」を知ると、コードの整理のために作られたものなのだというのがわかってきます。
1960年代後半には、オブジェクト指向というものは存在しませんでした。
どんな複雑なシステムも、メモリに直接アクセスしたりとかしながら記述をしていっていましたが、ある時から
「生産性激落ちでぴえん超えてぱおん🥺」
「複雑すぎて無事死亡www」
ってみんなが言い始めました。
(それを「ソフトウェア危機」と言います。 このあたりの歴史は、 「新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡」が凄く丁寧に書かれていて詳しいです)
そこで、すごい人達があれこれと「コードの整理方法」についてまとめていきました。そしてその数々の「整理方法」を取り込みながら完成したノウハウの体系がオブジェクト指向というわけです。
オブジェクト指向はあくまでも整理術なので、システム開発において必須ではありません。
プログラムはオブジェクト指向を必要としません。プログラムはただ、書かれた処理を実行するだけです。
今プログラムを書いている人はオブジェクト指向を必要としません。疎結合とかカプセル化とか継承とか気にしなくてもプログラムは書けるからです。
オブジェクト指向は、後からコードを読む人が必要とします。今読もうとしている処理がなんなのか、そして何がどこにあり、どこをどう直し、どこにどう処理を追加しなければならないのか、コードがあるルールに基づいて整理されている方が理解しやすく、その理解のしやすさが「保守性」やら「再利用性」や「拡張性」やらへと繋がります。
整理術も同じですね。作業内容自身は整理されてる/されてないとか関係ないし、今作業している人も整理されてなくても作業ができますが、あとになって作業の続きをしようとするとき、書類や筆記用具が整理整頓されてる方がより早く確実に作業にとりかかれます。
整理されている方が探し物は見つけやすいですし、あとになってどこに何をしまえばいいかわかりやすいですよね。
人間は忘れっぽい生き物で、自分が書いたコードでさえも2~3週間も経つと何をしてたのか忘れていってしまいます。書いたコードの意味を思い出す時にもオブジェクト指向は役立ちます。
また、最近のシステムはコードを全部読めば全部の処理を完全に理解できるほど単純じゃありません。どこかを修正しないといけないとなった時、どの部分を修正すれば良いのかがわかりやすい方がバグが出にくくなります。オブジェクト指向はそれを手助けしてくれます。
補足:システムとは何をするものなのか
ちょっと、具体的な整理術の話をする前に、大前提となることをお伝えします。システムは何をするものなのかという話です。
それは**「データ(情報)の処理」**に外なりません。ゲームやWebアプリケーション他いろんなものがありますが、すべての根底にあるのは情報の処理であり、画面にはその結果が映されます。
となると、書かれる処理は必然的に「あるデータに関連する処理」になります。すべての処理は、突き詰めると以下のいずれかを行います。
- データの生成
- データの参照
- データの受け取り
- データの加工
- データの出力
- データの削除
このことを意識しておくことは、コードの整理をする上でとても重要となります。
どのように整理するのか
基本的な整理の方針を列挙します。
- 適切な名前をつけ、名前で何を表しているのかわかるようにする
- メンバ変数やメソッドをそのクラスに関係のあるものだけに整理する
- ひとつひとつの処理を独立させる
- クラスの純度を上げる
- あとから変わる可能性の高い部分を分離する
以下で具体的に説明していきます。ちょっと難しくなってきますが、なるべく丁寧に説明するよう頑張ります(`・ω・´)
適切な名前をつけ、名前で何を表しているのかわかるようにする
たとえば、以下の処理を考えます。
public class Hoge {
private float c = 1.1;
private String d = "85243626545";
public int fuga(int a, int b) {
int r = a * b * c;
if (r > 5000) {
r = r - 100;
}
return r;
}
public int fugafuga(int a, int b) {
int c = b - a;
DBConnection con = DBConnectionFactory.createInstance();
con.exec("INSERT INTO receipt VALUES (?, ? , ?, ?, ?)", d, a , b, c, new Date());
return c;
}
}
これで一体このクラスとメソッドが何の処理をしているのかがわかるとすごいと思います。
「そんなの簡単だよ~、aとbかけた値にさらにcをかけた結果が5000より大きければ100引いて返してるんだよ~」
「もうひとつはDBコネクションに接続してごにょごにょ」
まぁそうなんですが、それは中の具体的な処理を説明しているだけになります。
重要なことはこの処理は何をしようとしているのか(システム的にどういう要件を満たすための処理なのか)がわかることです。システム的にaが何をあらわし、bが何をあらわし、cが何をあらわし、Hogeとfugaとrが何をあらわしているかが、この処理を見てもわかりません。
もし**「商品の定価と購入量に消費税をかけた合計が5000円を超えてる場合100円割引する」「精算時に社員番号と請求金額と受け取り金額とおつりと現在日時をDBに登録する」 って要件があってこれを作ったら、保守する人に呪い殺されてしまうかもしれませんね**。藁人形に釘を打たれないためにも、もっとわかりやすくしましょう。
public class CashRegister {
private float tax = 1.1;
private String employeeCode = "85243626545";
public int calculateTotalPrice(int price, int quantity) {
int totalPrice = price * quantity* tax;
if (totalPrice > 5000) {
totalPrice = totalPrice - 100;
}
return totalPrice;
}
public void register(int totalPrice, int paymentAmount) {
int charge = paymentAmount - totalPrice;
DBConnection con = DBConnectionFactory.createInstance();
con.exec("INSERT INTO receipt VALUES (?, ? , ?, ?, ?)", employeeCode , totalPrice , paymentAmount , charge , new Date());
}
}
多少わかりやすくなりました。
ただ、ここまでは別にオブジェクト指向の話とはあんまり関係がありません。 むしろ今後説明していくための準備段階です。
ここで、このクラス名とメソッド名だけをインタフェース化して見てみましょう。
public interface CashRegister {
int calculateTotalPrice(int price, int quantity);
void register(int totalPrice, int paymentAmount);
}
オブジェクト指向的なコード整理のコツは、これだけ見ても何の処理がされるかある程度わかるようにするという点にあります。
さて、消費税をかけた合計が5000円を超えてる場合100円割引する
精算時に簡易的なレシート情報をDBに登録する
という処理が行われているのかどうか、わかるでしょうか。
この視点が、整理術においてとても重要になります。なぜなら、システムを作るとコードは膨大になっていきますから、いちいち中の処理まで見ていたら効率悪いわけです。みなさんも、文字を出力しようとするたびにStringクラスの処理内部まで見に行ったりしないですよね。
「ドキュメントに説明で書いておけばいい」
というのもひとつの解決方法ではありますが、できればメソッド名から判断できる方が、全体の可読性があがります。
だって、呼び出し元でもいちいちコメントなんて書いていられませんから。
オブジェクト指向の本でよく「カプセル化」の説明で、単純にアクセス修飾子(privateとかpublicとか)を使って外からアクセスできなくすることだと書かれることが多いですが、神髄はそこではありません。アクセス修飾子は単に「基本外部からアクセスできないことをプログラムで保証する」ためのツールにすぎず、**カプセル化の神髄は「見れないようにする」ではなく「見なくてもいいようにする」という点にあります。オブジェクト指向はコードを読む人のためにあるのですから、中身を読まなきゃ処理がわからない時点でカプセル化は崩されてます。**インタフェースだけでわかるようにして初めてカプセル化は完成します。
というわけで、より分かりやすい名前に変更をしましょう。
※ ちなみに、これはあんまりいい例ではなく今後修正していきます。「名前で何を表しているのかわかるようにする」というポイントで以下の修正をしています。
public class CashRegister {
private float tax = 1.1;
private String employeeCode = "85243626545";
public int calculateTotalPriceWithTaxRateAndDiscount(int price, int quantity) {
// 同じ処理
}
public void registerReceipt(int totalPrice, int paymentAmount) {
// 同じ処理
}
}
一応これで、中でどういう処理が行われるのかより具体的になり、このクラスを利用する際にはどんなメソッドがあるか知るだけで済みます。
これは、とても意味のあることです!
何故なら、このクラスを使う時にこの中身を読む時間を使わなくて済むようになって効率が上がるからです。
補足:本当はメソッド名は短い方がいい
上にも書きましたが、メソッド名が長いというのは「色々とさせすぎている」というバロメーターにもなります。なので本当はあんまりいいことではないのですが、最初から完璧を目指すのは難しいかと思います。なので最初はメソッド名が長くなっちゃっても仕方ないです。ただ何をしているかクラスやメソッドだけである程度正確にわかるというのは心掛けるようにするとよいと思います。
(正確に、というのが特に重要です。実は内部で他にも影響を及ぼすことをしていた、というようなケースがあると、内部処理を追わないといけなくなってカプセル化が崩れてしまいます)
ちなみに、メソッド名が正確かつ短いという状態にするには、以下を整理していくといい感じになります。
- 中でいろんな処理をさせすぎていないか整理する
- メソッド名に
And
とかつくと色々やりすぎてる率高いです - このあたりは経験積むのが一番かと思います
- メソッド名に
- 内部で作っている変数を引数にする
- 引数にする = メソッド名にしなくても「それを使って何かをする」ことを利用者に伝えることができます
- 具体的には、これ以降でも行います
メンバ変数やメソッドをそのクラスに関係のあるものだけに整理する
次に見るべきポイントは、そのクラスが必要以上のメンバ変数やメソッドを持っていないかどうか、です。
基本的に、 クラスひとつだけの意味を持たせ、それに対して責任を持たせる ことが理想です。
これだけだと何を言っているのかよくわからないと思うので、ちょっと別のクラスの例で説明します。
public class Price {
private final int price;
public Price(int price) {
this.price = price;
}
public Price totalPrice(int quantity, float taxRage) {
return new Price(this.price * quantity * taxRate);
}
public Price discount(Price discountPrice) {
return minus(discountPrice);
}
public Price minus(Price other) {
return new Price(this.price - discountPrice.price);
}
public boolean isGreaterThan(Price other) {
return this.price > other.price;
}
public int toInt() {
return this.price;
}
@Override
public String toString() {
NumberFormat format = NumberFormat.getCurrencyInstance();
return format.format(this.price);
}
}
これを見ると、「あぁ、このクラスは値段を表しているんだな」というのがなんとなくわかるかなと思います。
そしてこの Price クラスでは、自分のメンバ変数を扱ったメソッドのみが存在しています。
上でも述べた通り、システムとは 「データ(情報)の処理」そのものです。
コードの整理という点でいうと、あるデータの直接的な加工は、そのデータを持つクラスの中だけで完結しているとわかりやすくなります。
逆にいうと、あるデータの直接の加工が複数のクラスにまたがっていたり、またはひとつのクラスに複数の意味を持たせているとわかりづらくなります。
それを踏まえた上で CashRegister
を見ると、税率と社員コードの2つを持っています。これは必要なのでしょうか。
たしかに、実際のレジを考えると、レジが社員コードは覚えるかもしれませんね。ただ最近は軽減税率もありますから、メンバ変数として税率を持っているのはおかしい気がします。なのでそこを変えます。あと社員コードもそのまま持つのではなく、外部から入力されて保持しているものとも思いますから、そこも変えましょう。
更には calculateTotalPriceWithTaxRateAndDiscount
というメソッドも変えてよさそうです。引数に taxRate
を入れる以上は、税率も計算した結果を返すというのを利用者に直感的にわからせることができますから。
public class CashRegister {
private String employeeCode;
public void setEmployee(String employeeCode) {
this.employeeCode = employeeCode;
}
public int calculateTotalPriceAndDiscount(int price, int quantity, float taxRate) {
// 同じ処理
}
public void registerReceipt(int totalPrice, int paymentAmount) {
// 同じ処理
}
}
ひとつひとつの処理は独立させる
でも、ちょっと待ってください!
上記のコードの場合、 registerReceipt
は、 setEmployee
を呼び出すことが前提となっています。そうでないと、内部の処理でnullが入ってしまいます!
「いや、呼び出せばいいじゃない?」
というのはあまりよろしくありません。なぜなら、 registerReceipt
を見てもそんなこと書いていないからです。つまりは 内部処理を見るか、コメントを読まないとわからない ということになり、整理という点でよろしくありません。**なので基本的に1つの処理は、他の暗黙的制約なしで呼び出せるようにするのが理想です。**どうすればよいでしょう?
色々やり方はあります。たとえば、 registerReceipt
呼出し時に employeeCode
がnullだったら例外を出させるとか。
でも一番簡単かつ確実な方法は明示的制約、つまりはコード上で強制させる ことです。
つまりはこうやることで、このクラスを使う人は CashRegisterを生成する時に必ずemployeeCodeを入れることになるため、registerRecepitを呼びだす時には既にemployeeCodeが入っている状態にさせることができます。
public class CashRegister {
private final String employeeCode;
// コンストラクタで入れる
public CashRegister (String employeeCode) {
this.employeeCode = employeeCode;
}
public int calculateTotalPriceAndDiscount(int price, int quantity, float taxRate) {
int totalPrice = price * quantity* taxRate;
if (totalPrice > 5000) {
totalPrice = totalPrice - 100;
}
return totalPrice;
}
public void registerReceipt(int totalPrice, int paymentAmount) {
int charge = paymentAmount - totalPrice;
DBConnection con = DBConnectionFactory.createInstance();
con.exec("INSERT INTO receipt VALUES (?, ? , ?, ?, ?)", employeeCode , totalPrice , paymentAmount , charge , new Date());
}
}
「これだと、使う時毎回レジを新たに作るってことになってる!そんなの現実ではおきないよね?」
と思う方もいるかもしれませんが、ぼく達は別に現実のレジをそのまま再現しようとしているのではなく、レジが今やっていることに置き換わるシステムを作ろうとしているので、別に現実と違ってもいいいんです。
(入門書とかでたまにオブジェクト指向を「現実のものをコードで再現する」というような記述をしたりしてますが、その考え方は忘れた方が良いかと思います。広義的には間違えてないと思うのですが、ちょっと難しい話をすると、オブジェクト指向は抽象化したモデルを扱うため現実をそのまま再現しようとすると失敗したりすることが多いです)
クラスの純度を上げる
さて、大分いい感じになってきたような、なってきてないような、そんな感じです。正直まだ CashRegister
にいろんな役割を持たせすぎています。
今このクラスがやっていることは以下のことです。
- 従業員コードを保持する
- 税込の合計金額を計算する
- 5000円以上の場合100円値引きする
- 簡易レシートをDBに登録する
この時、「このクラスの役割は何か」というのを考えてみます。
クラス名からして、商品の販売額を計算、記録をするというのが役割でしょう。たしかにそういう点でいうと、全部このクラスの役割のような気もしてきます。
ただもう少し視野を広くし、システム全体から見た個々の役割を考えてみます。
その時、割引額を決めるのって、レジの役割なのでしょうか。むしろそれは、運用する会社のビジネスロジックに深くかかわる部分であり、レジがその判定をするのは少し異なっているような気がします。
というわけで、**割引サービス(DiscountService)**を作成し、そちらに処理を移動させます。
そうすることでレジは 商品の販売額を計算、記録ということだけに注力できますし、割引のロジックについては考えなくてよくなります。
また、割引のロジックを一ヶ所にまとめることができるようになるため、あとで修正が必要となってもそこを見るだけでよくなります。
ところで割引のロジックはきっと運用でよく変わりそうです。
なので、ここではインタフェースも定義します。
public interface IDiscountService {
Price discountedPrice(Price totalPrice);
}
public class DefaultDiscountService implements IDiscountService {
private final Price discountThreshold = new Price(5000);
private final Price discountPrice = new Price(100);
@Override
public Price discuntedPrice(Price totalPrice) {
if (totalPrice.isGreaterThan(discountThreshold)) {
return totalPrice.discount(discountPrice);
}
return totalPrice;
}
}
では、上のクラスを導入して書き直してみましょう。
public class CashRegister {
private final String employeeCode;
// コンストラクタで入れる
public CashRegister (String employeeCode) {
this.employeeCode = employeeCode;
}
public int calculateTotalPrice(Price price, int quantity, float taxRate, IDiscountService discountService) {
Price totalPrice = price.totalPrice(quantity, taxRate);
return discountService.discountedPrice(totalPrice);
}
// 以下略
}
calculateTotalPrice
が2行になっちゃいました。やろうと思えば1行にもできそうです。
こんな処理のほとんど書かれてないメソッド、果たして意味があるんですかね? って思うかもしれませんね。
安心してください。書く処理が少なくなっているというのは、やるべき処理をやるべきクラスに委譲させられているという一つの指標ですし、オブジェクト指向的な整理ができてきている兆しがあるってことです。
それに、オブジェクト指向整理術で重要なことはメソッドからそれが何をする処理かわかるようになっていることであり、それさえわかれば中の処理なんて1行だろうが2行だろうが気にしなくていいんです。
むしろOOPマスター達の書くコードはだいたい1メソッド内に書かれる処理が短いです。短いのは読みやすいからいいことです、多分(`・ω・´)
ところで、 calculateTotalPrice
の引数が4つになってしまいました。
より整理を進めるなら、 price
と quantity
を一つにするようなクラス (明細書クラスとか) を作ってそれを渡してあげる方が良いかもしれませんね。
(今回はそこは端折ります)
あとから変わる可能性の高い部分を分離する
ただ今の CashRegister
は、正直いってまだ修正に弱いです。
あからさまにやっていたDB操作部分ですね。
DBのような外部に依存する場所は、環境が変わったり、別の場所に出力したくなったりして結構変更がおきやすく、かつ変更となると修正が大変になったりすることがあります。
ですので、そこも外だしする方がよさそうです。
たとえば以下のような感じですね。 (IReceiptRepository
の処理詳細は端折りますが、実装部分で登録を代わりにしてると思って下さい)
public void registerReceipt(Price totalPrice, Price paymentAmount, IReceiptRepository repository) {
Price charge = paymentAmount.minus(totalPrice);
repository.insert(employeeCode, totalPrice, paymentAmount, charge);
}
これで、どのDBに入れることになろうが、DBの変わりにテキストファイルに出力することになろうが、 IReceiptRepository
の新しい実装を作るだけでよくなり、ここのメソッドには何も手を加えなくてよくなります。
(こう見ると、 Price
クラスとしたのは間違いでした。 Money
クラスとかの方が直感的でよかったかも。それも整理の一環でやった方がいいと思いますね)
まとめ
ちょっと題材としたクラスが微妙だったかもですが、こんな感じで整理を進めていくと自然とオブジェクト指向的になっていくかと思います。
まとめると、以下になります。
- クラスやメソッドだけ見て何の処理がされるかある程度正確にわかるように名前付けする
- クラス名やメソッド名が短くなるように分割する
- メンバ変数やメソッドをそのクラスに関係のあるものだけに整理する
- 基本的に1つの処理は、他の暗黙的制約なしで呼び出せるようにする
- 「このクラスの役割は何か」というポイントで整理する
- あとから変わる可能性の高い部分はインタフェースにして分離しておく
これらがうまくできていると、あるメソッド内の修正が必要になった時でも、特に説明がなくても何をしているのか設計レベルで理解が可能になります。
例えば、途中で出した DefaultDiscountService
を見ると、
public class DefaultDiscountService implements IDiscountService {
private final Price discountThreshold = new Price(5000);
private final Price discountPrice = new Price(100);
@Override
public Price discuntedPrice(Price totalPrice) {
if (totalPrice.isGreaterThan(discountThreshold)) {
return totalPrice.discount(discountPrice);
}
return totalPrice;
}
}
他を見なくても「5000円を超えている場合、合計金額から割引する」というのがある程度理解できるのではないでしょうか。きっと。
継承、インタフェース、ポリモーフィズム、カプセル化などなど、オブジェクト指向の話でよくでてくる用語は、オブジェクト指向の重要な概念というよりは整理するためのツール / フレームワークだと考えておく方が、理解はしやすくなるのではないかと思います。きっと。
最後に
もっと深く知っていきたい場合、以下の順で学んでいくのがいいかなぁ、と思います。デザインパターンの本は基本的に具体的なコードも書かれているのでコードの書き方という面でも早めに読む方が良いかと思います。
- デザインパターン
- オブジェクト指向原則
- ドメイン駆動設計
あとOOとの関連性は低いけど、「リーダブルコード」は是非読んだ方がいいです。読みやすい上に名著です。