ブログ

Blog

オブジェクト指向の基本を押さえる

オブジェクト指向の基本を押さえる

はじめに

今回はオブジェクト指向について書いていきます。
オブジェクト指向について「知ってるような、知らないような」そんな感じの方が多いのではないでしょうか。
エンジニア歴3年目に入った私もまさにその程度で、
「言葉はもちろん知ってるんだけど、説明しろと言われるとなぁ」みたいな感じでした。
今回はそんなふんわりとした理解を改めるために記事を書きましたので、
皆様の参考になると幸いです。

オブジェクト指向とは

それではオブジェクト指向について解説していきます。
まず、「オブジェクト指向ってなに?」と聞かれると、「ん~、なにって言われるとなぁ」と、困ってしまいますよね。

Wikipediaによれば「オブジェクト指向プログラミングとは、
「オブジェクト」という概念に基づいたプログラミングパラダイムの一つである。」と記述されています。
Wikipediaでさえ、良く分からない回答をしているので、我々がパッと回答を出せるわけありません。

次にGeminiに聞いてみました。
Gemini「プログラムを『手順の羅列』から、『便利な部品の組み合わせ』に変える考え方」
さすがGemini!めっちゃ分かりやすい!
つまり、オブジェクト指向とは「部品を組み立てるようにプログラミングすること」と言えますね。

なぜ必要なのか

なんとなく、概要がつかめたところで「じゃあ、なんで便利なの?」が、気になります。

今やオブジェクト指向型ではないプログラミング言語を見る方が珍しくなりましたので、
使われているのはそれなりに理由があるはずです。

これはプログラミング言語の歴史を辿ると分かりやすくなります。

初期の言語: すべてのコードに番号(行番号)が振ってありました。

指示の方法: 「50行目に飛んで!その次は85行目!」というようにイチイチ指示を出さなくてはいけませんでした。

問題発生: インターネットの時代に突入するとソースコードが巨大化。関数が膨大な量になり、管理不能な状態に。

そこで生まれたのが「オブジェクト指向型」というわけです。

オブジェクト指向はこれまでのプログラミング言語の大きな2つの課題を解決しました。

1点目はバグの減少です。
2点目は再利用性の向上です。

これらがどのような機能によって解決されたのか見ていきましょう。

「クラス」と「インスタンス」

「クラス」と「インスタンス」は、オブジェクト指向の根幹ともいえる機能ですので、解説していきます。
一般的にクラスは設計図、インスタンスは実態と言われます。
ただ、オブジェクト指向の理解が浅い中で「設計図」や「実態」と言われても、
「ナニソレ・・・」状態だと思います。

今回はおにぎりを例に出して説明していきます。
いきなりですが、おにぎりの型って使ったことありますか?
こういうやつです。

おにぎりを頻繁に作る私ですが、正直言っておにぎりは型なしでは綺麗な三角形に作れません。
しかし、この型を一度購入すると、綺麗な三角形をしたおにぎりが100個でも200個でも作れるようになるわけです。
型があると再現性が生まれるというわけですね。
ここまで説明した中で「型」に当てはまるのがいわゆる「クラス」です。
そして型に米と具を注入する流れのことを「インスタンス化」と言います。
そして出来上がったおにぎりが「インスタンス」ということですね。

こんなイメージです

実際にコードにしてみるとこんな感じになります。

型(クラス)は1つですが、中に入れる具(データ)を変えることで、
無限に違うおにぎり(インスタンス)を生み出せるのがわかります。

オブジェクト指向の3大要素

ここからはオブジェクト指向が持つ機能をもう少し深堀していきます。

カプセル化について

カプセル化とは中身を隠して、外からは決められた操作しかさせないようにすることです。
カプセル化のメリットは、意図しないデータ操作を防ぐことです。
もしカプセル化をしなかった時のことを考えてみると、カプセル化のありがたみがよく理解できます。

カプセル化されていない(悪い)例
これがカプセル化をしていない例です。

このように、カプセル化が行われないと、
どの関数からでも好きなように変数の値を書き換えることができてしまいます。

カプセル化された(良い)例
では、カプセル化が存在するコードを見てみましょう。

このようにカプセル化をすることによって、
「どの値をどこから変更することができるのか」を定義することができます。
カプセル化によって、予期しないエラーを防ぐことができるのです。

継承について

次に、継承について解説していきます。

継承とは、すでに存在するクラスを用いて、新しい機能を追加することです。
これが存在しないと、すでに存在するクラスと似たようなクラスを作る場合でも、
新しく自分でゼロから作らなきゃいけなくなります。
めちゃくちゃ面倒くさいですよね。

継承することによって、今まで存在するクラスに「1個だけ機能を追加する」といったことができるようになります。
具体的な事例を見ていきましょう。

継承を使用しない(悪い)例
以下の内容が、継承を使用していない状態です。同じようなコードを何度も書く羽目になります。

以下は継承を用いてなるべく重複を減らした良い例です。

このように重複した記述を減らせるのが継承のポイントとなります。

ポリモーフィズム

オブジェクト指向の機能の3つ目のポリモーフィズムについて解説します。
ポリモーフィズムは日本語で言うと多態性という意味で、
同じ名前の関数を複数のクラスで使用することができるようになります。

例えば、電子レンジクラスを作るとして、
製品Aの電子レンジと製品Bの電子レンジで共通の機能は必ずありますよね。
温める機能とか、自動洗浄機能とか。
共通の機能は同じ関数名にしておきたいです。

microwaveCleanAやmicrowaveCleanBなんて関数を作ったら
製品ごとに関数が増えていってしまって、「どの関数名が○○の商品だっけ?」と混乱してしまいます。
microwaveClean関数を1つだけ作成し、中身だけ製品ごとに変わっている方が分かりやすいですよね。
それをできるのがポリモーフィズムの良い点です。

どのように実装にするのか見ていきましょう。
先ほどExtendsの事例は見たので今回はinterfaceを使用していきます。

続いて、定義した内容を用いて機能を作成します。

上記のように記述しインスタンス化すると、
高級レンジの温め機能も安いレンジも同じwarmUpという関数で呼び出すことができます。

以上のように、オブジェクト指向は開発者の脳みそをスッキリさせてくれる機能が様々存在します。
全てマスターすると大規模なシステムがきれいに作れるようになるかもしれません。

オブジェクト指向を用いた設計

ここからはオブジェクト指向を用いた設計について解説していきます。

既存のアプリケーションに新しいクラスを作る際に
過去の実装方針を踏襲したいが、何を方針にしているのかが分からない」という経験をしたことはありませんか。

過去の開発者が何を目指してアプリケーションを設計していたのかは、
オブジェクト指向のよくある2つの考え方を理解すると分かりやすいですのでご紹介していきます。

結合度と凝集度

1つ目は結合度凝集度です。
これはオブジェクト指向が目指すゴールのようなものです。

具体的な定義は以下の通りです。
結合度:1つのクラスを変更した時にどれだけ他のクラスに影響を与えるか
凝集度:1つのクラスの中にどれだけ機能が詰まっているか

なるべく結合度は低い方が良くて凝集度はなるべく高い方がいいです
具体的な例を見ていきましょう

このケースは密結合で低凝集です。
なぜ密結合と判断できるかというと仮に MySQLDatabaseクラスがOracleDatabaseへ書き換わると、
StickyOrderManagerクラスも動かなくなってしまうからです。
様々なクラスで同じようなことが起きるとその回数分、修正を加えなきゃいけなくなります。

凝集度が低いと判断できる理由はこの1つのクラスに
「計算ロジック」「保存ロジック」「通知ロジック」が全て入っているからです
仮に「消費税計算ロジック」に修正を加えて間違えた修正をした場合、
なぜかSQLの保存がうまくいかなくなる可能性があることが低凝集の問題です。

それでは良い事例を見ていきましょう。
まずはインターフェースを設置します。

そのインターフェースを使用した形で関数を作成します

それらを用いて メイン クラスを作ります

このように1つのクラスに様々なものを集中させるのではなく、
役割を分担することで保守性の高い良いアプリケーションになります。

委譲とコンポジション

委譲とコンポジションは先ほど説明した「継承」を使用するのはあまり良くないという発想から生まれた概念です。
以下の例を見てください。

このように勇者クラスを剣クラスから継承すると勇者は剣士にしかなれません。
ただし勇者の攻撃は弓もあれば呪文もあるかもしれません。
そのような場合、上記のように剣クラスを直接継承すると剣クラスしか使えないという状況に陥ります。

では、良い事例を見ていきます。

上記のような形で、直接継承するのではなく部品定義をした後に、
部品の中の値を更新してあげると同じfightの関数で剣を使用するのか弓を使用するのか、などを切り分けることができます。

SOLIDの原則

ソフトウェアの開発や設計に用いられる考えとしてSOLIDの原則というものがあります。
このルールを守れば良いソフトウェアになるという基本的な考え方のことです。
SOLIDの原則を頭に入れてソースを書いていけば、以下のメリットがあります。

・変更しやすい
・変更に強い
・理解しやすい

SOLIDの原則の定義を表にしてみました。

原則 正式名称 (英語) 定義 (意味) 目指す状態 (メリット)
S 単一責任の原則
(Single Responsibility)
クラスを変更する理由は、ひとつだけでなければならない 高凝集
修正の影響範囲を限定できる
O 開放閉鎖の原則
(Open/Closed)
拡張に対しては開き、修正に対しては閉じているべきである 拡張性
既存コードを壊さずに機能追加できる
L リスコフの置換原則
(Liskov Substitution)
派生クラスは、基本クラスと置き換え可能でなければならない 堅牢性
継承関係が正しく機能する
I インターフェース分離の原則
(Interface Segregation)
クライアントに、使用しないメソッドへの依存を強制してはならない 不要な依存の排除
関係ない機能変更の影響を受けない
D 依存性逆転の原則
(Dependency Inversion)
上位は下位に依存せず、抽象(インターフェース)に依存すべきである 疎結合
部品交換やテストが容易になる

「こういうのがあるのか~」と思いつつ、いつもイマイチピンとこないので、自分流に砕いて書いていきます。

S(単一責任の原則)

これは「一つのクラスには、一つの役割にしましょう」という原則です。
なぜ「一つの役割」に限定すべきかというと、複数の役割が同じクラスに存在すると、
一つのロジックの修正が他のロジックの機能にまで影響する可能性があるからです。
具体例を見ていきましょう。

どこかで見たことがあるコードだと思った方、よくぞお分かりになりました。
この単一責任の法則は先ほど解説した「凝集度」を高めようという考え方です。
どちらも中身を整理しようという類のものです。

O(開放閉鎖の原則)

開放閉鎖の原則は「機能追加はOK、機能変更はNG」という考え方です。
なぜ機能変更はNGかというと、既に出来上がっている機能を変更するということは、
思わぬバグを仕込んでしまう恐れがあるためです。

実例を見ていきましょう。
決済処理について現金とカードの処理がある以下のようなソースがあったとします。

もしここに「PayPayも追加して!」と言われたら?
このPaymentProcessorクラスを変更するしかないですよね。
開放閉鎖の原則ではクラスの変更はNGです。
ではどうすれば良いかと言うと、以下のように記述します。

これなら仮にPayPayクラスの追加依頼があっても、
同じインターフェースを活用して新たなクラスを作成すれば済みます。
開放閉鎖の原則は結合度を弱める法則と言えます。

L(リスコフの置換原則)

リスコフの置換原則とは、
サブタイプは、そのスーパータイプと置換可能でなければならない」という原則です。

サブタイプ:子クラスやインターフェースを実装して作ったクラス
スーパータイプ:親クラスやインターフェース

つまりリスコフの置換原則とは、「親クラスを使っている場所を、子クラスに置き換えたとしても、
バグなどが発生しないようにしなければいけない」という原則です。

この原則に則ってソースを書かないと、機能追加などの改修を加えた時に、
呼び出し元のコードが予期せぬエラーを吐く
ようになってしまいます。

具体例を見ていきましょう。まずは悪い例から見ていきます。
この例では、子クラスが親クラスの決め事を守らなかったためにエラーが発生する仕組みとなっております。

この例では、Volunteerクラスは給料計算が必要ないため、
給料計算メソッドの中でエラーを吐いています。

しかし、経理システム側では「全ての従業員(Employee)」を取得してfor文を回しているため、
ボランティアのユーザーの順番が来た時にエラー
となります。

正しい実装はどのような形かというと、以下のようになります。

上記のように実装することによって、役職によって給料の支払いが必要か不要かを、
型のレベルで分けることができました。

このようにリスコフの置換原則は、上位のクラスと下位のクラスが正しく置き換え可能になっていることによって、
実装エラーや不具合を削減することができます。

I(インターフェース分離の原則)

インターフェース分離の原則(ISP: Interface Segregation Principle)とは、
インターフェースには最小限の機能を付与することが大事である」という原則です。
具体的に言うと、「クライアント(実装するクラス)に、不要なメソッドの実装を強制させない」ことが重要です。

具体例を見ていきましょう。 以下の事例は悪い例です。

このように、すべての機能を詰め込んだ大きなインターフェースを定義してしまうと、
仮に「印刷機能のみ」を実装したい場合でも、
スキャンとFAXについて「何もしない」あるいは「エラーを投げる」ような無駄な実装をする必要が出てしまいます。

しかし、インターフェースを機能ごとに細かく分離しておけば、印刷は印刷だけで使うし、
スキャンはスキャンの時にだけ使えるというように、ロジックを整理することができます。

以下は良い事例です。

分割することで、必要なインターフェースのみを選択して実装することができます。

ちなみに、名前に「able」を付与するのはインターフェース命名の慣習です。
インターフェースは「〜できる(能力)」を有することを表すため、
そのような英語表現(Printable = 印刷できる)にするのが一般的です。

上記の良い事例を使用した実装は、以下の形になります。

このようにインターフェースを分けて実装することによって、
クラス構成がシンプルになり、拡張性や保守性が向上します。

D(依存性逆転の原則)

最後に「依存性逆転の原則」について説明します。
これは、「上位モジュールは下位の具体的なクラスに依存するのではなく、
インターフェースや抽象クラスに依存すべきである
」という考え方です。

なぜ「逆転」と呼ばれるのかは、以下の図とコードを見比べるとよくわかります。

通常、何も意識せずにプログラムを書くと、
上位モジュール(使う側)が下位モジュール(使われる側)を直接 new して利用します。
この時、依存の矢印は処理の流れと同じく「」を向いています。

この状態で、例えば「A社の決済」を「B社の決済」に変更しようとすると、
上位モジュールにまで修正の影響が及びます。
以下のコードでは、OrderService が CompanyAPay にガッチリと依存しているため、
ここを書き換えない限り決済方法を変更できません。

ではどうすれば良いかと言うと、インターフェース(抽象) を間に挟んで、依存度を下げます
こうすることで、A社からB社へ変更があったとしても、
OrderService クラスを書き換える必要はなくなります。

上記のように書くことで、依存関係の矢印の向きが変わりました。
図に表すと以下のようになります。

注目すべきは、下位モジュールからインターフェースに向かって矢印が上を向いている点です。
処理の流れ(上から下)に対し、依存の方向(下から上)が逆になったため、「依存性の逆転」と呼ばれます。

おわりに

今回はオブジェクト指向について詳しく見ていきました。
本当はデザインパターンについても解説していきたかったのですが、文量が多くなったためここまで。
改めて勉強してみて思ったのは、「ミスを減らそう」とか「読みやすくしよう」とか、
「人に優しい設計にしようという考えで動いているんだなぁ」と思いました。

もっとこう、「速く動かそう!」とかそういう意識高い系の思想なものかと思ってました。
あと、コード量は少ない方がいいものだと考えてましたが、
結果見やすくなったりミスを防げるならコード量は増えてもいいんだと気づきました。
ミスを減らすためとかなら頑張れそうなので、オブジェクト指向をより深く勉強していければなと思います。

 

 

株式会社BTMではエンジニアの採用をしております。 ご興味がある方はぜひコチラをご覧ください!
  • SNS
  • 投稿日
  • カテゴリー

    Tech Blog