LoginSignup
21
19

More than 5 years have passed since last update.

オブジェクト指向プログラミング(OOP)がちょっとわかるようになるための記事

Last updated at Posted at 2018-09-29

はじめに

OOPというパラダイムが生まれて実は結構な年数がたっているが、OOPというのはなかなか定着していないように思う。
理由として思うのはOOPの機能面でいえば、理解するのにはそう難しくはないが、どう活用していくかまで含めて考えれば困難だからな気がする。
とりあえず何となくオーバーライドしてみた。何となくカプセル化してみたみたいな考え方の人は多いし
基底クラスをis asで派生クラスに変換してしまってややこしいコードを書いてしまうのは、やはりそういうことなんだろうと思う。
筆者もそれなりにはできるが、マスターしているわけではないし、勘違いしている部分もあるだろうと思う。

※間違っている点はぜひ指摘してほしいし、または編集リクエストを投げて欲しい。
極力言ってもらえれば文章に含めるようにする(しかし私の理解力もあるので、そういった点で不安な記事の修正はしないでおこうと思う)

OOPというのは当初のうたい文句としては現実のものを表現するのに便利だったり、継承を使うと今までの資産を再利用できるなどという触れ込みが目立ったものだが、今となってはそういうことは聞かない。
というのも現実のものを表現し、プログラムに適用しようとすると大抵の場合は「一応完成はするものの保守が困難で、要件などの変更に弱いものとなる」のである。
近代のプログラミングは作って終わりではなくその先がある。
バグを修正したり、バージョンアップをしたり、なんなら開発しつつ市場動向やユーザの反応を見て修正をする場合もある。
そういう時代に、変更に弱いと現場が大変な目にあうのは火を見るよりも明らかである。
保守をなるべく容易にしたり、変更に強いプログラムを作ることを目的に、オブジェクト指向プログラミングは、オブジェクト指向分析とか、オブジェクト指向設計という概念でとらえたりもする。

継承もまた、当初の触れ込みは再利用だったし、今もまぁ機能的には再利用と言えば再利用ではあるのだが、多用するとコードががちがちに固められてしまい身動きが取れなくなり、それに泣きを見た人が多いのだろう。

この記事のスタンスと説明範囲

この記事はOOPのすべてを説明するわけではない。筆者自身OOPは複雑だと考えているし、懇切丁寧に教えると専門書が出来上がってしまう。
そこから派生する様々な開発手法を例に挙げ始めるとこれまた専門書は積み重なっていく。

なので、そういうのは置いといてこの記事ではひとまず以下について述べることとする。
説明内容もざっくりとしたものではあるが、ただ説明するだけではなく、それが前述したソフトウェア品質(保守性とか)にどういう影響をもたらすのか考える。

  • オブジェクト指向プログラミングと構造化プログラミング
  • オブジェクト指向プログラミングの機能について
    • クラスとインスタンス
    • カプセル化
    • 継承と集約
    • 多態性(オーバーライド、インターフェイス)

コードについて

コードはC#で記述します。筆者が主に使う言語だからです。

対象読者

  • オブジェクト指向プログラミングを学び始めた。基本的な機能はわかったけど、それを何に役立てばいいのかわからない。
  • 記事の改善に助力してくれる方々

対象としない読者

  • プログラミングを学び始めて間もない -> 入門書を読み漁るかいい師匠を見つけてください。

オブジェクト指向プログラミングと構造化プログラミング

構造化プログラミング

まずは構造化プログラミングの時代である(歴史の授業ではないのですっ飛ばして構造化プログラミングである)。
構造化プログラミングの正式な定義というとOOPもそうだがバズワード化してしまっていて、筆者もダイクストラが当初提唱していた厳格な定義を知るわけではないが
構造化プログラミングはWikiによると
* トップダウンに動作を決めて、粒度を詳細化していく。
* 条件分岐、ループ、逐次実行の手続き型で記述する。
* 構造体とかも実は存在している(クラスではないが、メソッドとか変数とかをまとめ上げる機能)。

とすればひとまず構造化プログラミングとしての要件は満たしていそうだ。

具体的には以下のようなものになる。もちろん現実はもっと残酷に膨大で、いろんなメソッドがいろんなところで使われているし、引数や戻り値や処理なども空っぽのままなわけがない。
カオスの世界に誘いたいわけではないので、この部分は頭の中で想像してみて欲しい、戻り値を条件分岐やループに使ったり、参照引数で中身を変えたりするコードが沢山でてくるのである(実際今でもそういうコードを描く人は沢山いる、彼らにとっては動くことが仕事を全うすることであり、管理するということにかけては無頓着である)。
また今回は構造体を使ってはいない。


void Main()
{
    SubFunc1();
    SubFunc2();
    SubFunc3();
}
void SubFunc1()
{
    SubSubFunc();
}
void SubSubFunc()
{
    SubSubSubFunc();
}

デメリット

さて、この構造化プログラミングは構成としては非常にわかりやすいが実は変更や追加に弱い。
トップダウンに物事を決め詳細化していくというのだが、それはつまりトップは子に対して使用依存がある。
つまり依存関係でいうと
Main → SubFunc → SubSubFunc → SubSubSubFunc
みたいな関係である。
矢印は使用しているということである。
ということはSubSubSubFuncというのは間接的にMainによって使用されているとみなせる。
SubSubSubFuncを変更するためにどこに影響が出るのかを見なければいけない。

そしてSubSubSubに変更が発生する場合を考えてみる。
適当な例を考えてみると
「法改正によって新しく計算式が変わった、起算日と改正日の比較によってロジックが変わる」
「今までSQL Serverを使っていたけど、MySqlにも対応しなければならなくなった」
「使用しているライブラリのバージョンがV1からV2に変更される」
「郵便物の送料の計算はA国とB国では違うし、B国でもB1とB2では違う」
「Aのページで使っているこのオプション機能はBのページでも使うが、全部ではなく、文言も微妙に変わる」
などがありそうだ。

変更する箇所は一つではなく依存関係がある箇所はすべてそうなる可能性がある。
つまり、さっきの例でいうと
SubSubSubFuncに引数が追加されると当然SubSubFuncにて影響が出るし、SubSubFuncに影響が出るということはSubFuncに影響が出る(その戻り値を使ったり、参照渡しの引数を使って変更をしたりすると影響はさらにさまざまなところに飛散する)

オブジェクト指向プログラミングの機能について

ここからが本題だが、構造化プログラミングのみで対応しようとすると依存関係に悩まされる。
前述した例でいうと、SubSubSubFuncを考える際にMainに対しても影響を考えないといけないし、後から仕様変更・追加などが発生すると大変である。
Mainを考えるときはSubFuncについてあまり深く考えずに自らのロジックの正しさのみ考えたい。
そこで依存関係をなるべく疎結合にしようという考え方が出てきた。
プログラムのモジュール間は疎結合であればあるほど、変更に対して強いのである。
そして疎結合であるにはカプセル化と多態性が重要となってくる。

カプセル化

カプセル化といえば、publicでアクセスするようにしてprivateでみんなに見られたくないものは隠して置こうってやつでしょ?
複数人で開発してれば触られたくないメンバとかもいて、勝手に状態書き換えられたら大変だもんね。
という人が出てくる。確かに一理(というか1分ぐらい)あるがそれですべてではない。
publicとprivateの重要性に気付いている人は暗黙的にそれらに対しても配慮が行き届いている場合もあるが念のため説明すると
実はカプセル化とは以下のものが含まれる。

振る舞いのカプセル化

単純にメソッドにしてしまえば内部の振る舞いに対しては秘匿できるよねということである。

派生クラスのカプセル化

基底クラスでアクセスするようにすれば派生クラスの実装は隠ぺいしてしまえる。
実装者は派生クラスのことを考えなくて済む。

データのカプセル化

おなじみ、publicとprivateによるカプセル化である。誰かに触られたくないものはprivateにしておくのが鉄則である。
というか、基本的にprivateにしておき、徐々にアクセスレベルを広げていくほうがよい。

アセンブリレベルでのカプセル化

アクセスされたくないものはアセンブリレベルで公開してはならない。
C#にはinternalという修飾があり、クラスは通常internalだ。
これをpublic classに変更すると別のアセンブリでもアクセスしていいよということになる。
しかし、アクセスされたくないものはinternalが望ましい。
インターフェイスとファクトリのみ公開し、内部の実装クラスに関してはinternalにしておくと誰かが勝手に

var childInstance = instance as ChildClass;
if(childInstance != null){
    childInstance.ChildMethod();
}

などという裏技を使うことを阻止できる。
asとかisで派生クラスに変えてしまうと大抵の場合は問題が起こる。
そもそもなんで基底クラスでアクセスできるようにしているか考えよう。

多態性

多態性とはインスタンスが様々に挙動を変えることである。
例えば以下のものは多態性である。

class Base{
    public virtual void Method()
    {
        Console.WriteLine("Called base method.");
    }
}
class Derived1 : Base
{
    public override void Method()
    {
        Console.WriteLine("Called derived1 method.");
    }
}
class Derived2 : Base
{
    public override void Method()
    {
        Console.WriteLine("Called derived2 method.");
    }
}
Base instance = createInstance();
// ここはcreateInstanceで生成するインスタンスによって処理が変わる。
instance.Method(); 

多態性と言われて、オーバーライドというとまぁその通りであるが実際はインターフェイスも含むと筆者は考えている。
というか、インターフェイスのほうがオブジェクト指向プログラミングにおいては重要だ。
インターフェイスとは振る舞いの定義であり、そのクラスが持つ仕様である。
カプセル化でも述べたが、基本的にインターフェイス経由でアクセスするとよい。

クラスとインスタンス

説明が遅れたが対象読者はすでにクラスというもの、インスタンスというものの概念を知っているからこそ後回しでもいいと判断した。

単純な機能でいうとクラスは内部にメンバを定義する。メンバとはメソッドかフィールド(C#的にはプロパティもそうだが、これは内部的にはメソッドである)のことである。
これらを集めておき、インスタンス化をすることで量産する。
現実問題メソッドとフィールドをかき集めれば何でもクラスにしてしまえるが、それは誤った使い方である。
クラスとは責務を持っていなければいけない(同様にメソッドなどもすべて責務を持たせるべきである)。
例えば、Fileというクラスがあったとすると、Fileはファイルに関する責務を保持するし、File.WriteTextというメソッドがあればそれはファイルに対してテキストを出力するという責務である。
実際のプロジェクトにおいてどのように責務を切り分けていくかという方法論まで踏み込むとオブジェクト指向分析とかオブジェクト指向設計の話にもなってくるので”かなりわかるようになる”ような記事を探してほしい。

継承と集約

継承と集約は重要だが多態性やカプセル化よりも優先順位は低いと判断した(といっても集約は別かもしれないが)。
継承は基底クラスのメンバをそのまま引きついだ形で派生クラスを作るという機能であり、
集約はメンバとしてクラスを持つような構造である。
集約の中でもそれ単体で存在できないクラスをコンポジションとも言う。

継承

継承は再利用するのだが、あまり多用はしてはならない。
なぜなら、オブジェクトの責務が必然的に大きくなるからである。
例えば車とレーシングカーを想像してみると、車は車としての責務を保持するしレーシングカーは車とレーシングカーの責務を保持する。
つまりレーシングカーは必然的に基底クラスよりもファットなクラスになる。
しかも基底クラスの変更に振り回されるし、基底クラスは派生クラスでどういう実装をするかわからないのでやがて変更されなくなってしまう。
また派生クラスから見ると基底クラスのprotectedメンバやvirtualメンバは隠れているので見通しも悪い(IDEが進歩しているのであまり気にするものでもないが)。
変更されないコードは腐臭が漂い始める。
それはみんなが変更に対して及び腰になってしまい無難な変更のみを良しとする風潮になってしまい、腫物のように誰も触りたくないコードということである。

集約

一方集約は継承に比べるとずいぶん使い勝手がいい。
そもそも以下のように継承は集約と等価である。

継承を使った場合
class Base
{
    public virtual void Func(){ }
}

class Derived : Base
{
    public override void Func(){ }
}
集約を使った場合
class Base
{
    private IDerived derived;
    public IDerived GetDerived(){ return derived; }
}
//本当は名前が不適切なことはわかっているが、継承と比較するためにこうした
interface IDerived
{
    void Func();
}

class Derived : IDerived
{
    public void Func(){ }
}

これのいいところは、なんといっても実装を分散できるところである。そして同時に責務も分けることができる。
継承を使った場合、Baseでそのままやりたいことを表現しているがそうすると

  • 本来はそのメソッド固有のフィールドでもBaseに対して依存する。
  • コードの修正をしようと思った時に引きはがすのが大変なことがある。
    • テストコードを書いているとそのテストコードも引きはがし作業のとばっちりを受ける
    • 引きはがした結果、baseInstance.Func()がbaseInstance.GetDerived().Func()となってしまい修正箇所が多岐にわたる。外部に提供しているライブラリなら、、、ご愁傷様です

一方集約はフィールドのスコープもコンパクトにまとまるし、そのクラスが何を必要としているか?はインスタンス化するときにコンストラクタ引数として渡せばよい。
さらにbaseInstance.Func()として記述していた箇所がderived.Func()で記述するようになるので、Baseから切り離して考えられるようになる。

おわりに

本当はもっと伝えたいことはあるが、OOPに関して思っていることを書きなぐるとこんな感じになった。
この後の発展としては、SOLID原則とかもあるし、開発手法としてはアジャイル開発、テスト駆動開発などもOOPの恩恵は十分に受けていると思われる。

後色々急ぎで作ったこともあり、文章がぐちゃぐちゃしている。頃合いを見て訂正していくこととする。
ここもうちょっと詳しく教えて欲しいとか、ここ間違ってる(どういう風に改訂すればいいのか教えてね)とかあれば教えて欲しい。

21
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
19