BETA

オブジェクト指向に見る「理想のプログラム」

投稿日:2018-12-11
最終更新:2018-12-11
※この記事は外部サイト(https://willow-leaf.net/ideal_program_seen...)からのクロス投稿です

はじめに

本記事では「オブジェクト指向」そのものについての内容は扱いません.

その代わりに,オブジェクト指向という考え方から見えてくる

「プログラミングはこうした方が上手くいく!」

という事例を見ていくことで,

「プログラムの質を上げていくために普遍的に言えること」

が見つかるのではないかという試みを行っています.

ということで,本記事を読むにあたって特に前提知識は要らないので安心してください.

コーディング設計 は別のスキル!

私達は プログラミング という作業をする際,

  • コーディング
  • 設計

という二つの異なるスキルを駆使しています.

自分がいま行っている作業について,それがコーディングをしているのかそれとも設計をしているのかを常に考えておくことは中々に大切なことです.

プログラミングの流れとしては,まず始めに "設計" があります.

プログラムの骨格を作る大切な工程です.

設計の段階ではまず始めに,

どのようなプログラムを作りたいのか(要件定義)

を決めた後で,

  • どのように機能を実装していけばプログラムが作れそうか
  • 取り回しの効いた拡張性の高いプログラムにするにはどうすればよいか
  • メンテナンスがしやすい(可読性の高い)プログラムにするにはどうすればよいか

などを考えていきます.

設計が終わったら,今度はそれをもとに実際にプログラムを書いていきます.

これが "コーディング" の段階です.

コーディングの際は以下のようなことに注意していきます.

  • 実行速度は十分か.動作が遅くなるような処理をしていないか.
  • エラーやバグが発生する余地を残していないか
  • 他人が見ても分かりやすいように適切なコメントやネーミングが行えているか

"設計" がまずいプログラムは,再利用や将来の変更といった問題に対して脆弱になります.また,全体の構造を把握するのに余計な時間を要求されます.

"コーディング" がまずいと,実行が遅すぎる,エラーを吐いてプログラムが止まる,結果が間違っているといった弊害が発生します.

逆に,

"設計" が上手く行くと,ほとんど中身を弄らなくても他の場所で使いまわしできる優秀なプログラムが出来たり,

"コーディング" が上手く行くと,動作が劇的に軽く,信頼性の高いソースコードが作れたり,

そんなことが出来るようになります.

設計とコーディングの違いと重要性は理解していただけたでしょうか.

オブジェクト指向に見る設計論

オブジェクト指向はプログラムの "設計" に大きな影響を与えた考え方です.

オブジェクト指向は簡単に以下のような特徴を備えています.

名称 概要
カプセル化 コードを機能毎にまとめてブラックボックス化
継承 既にどこかで書いたコードを使いまわし・拡張する技術
多態性(ポリモーフィズム) 継承と組み合わせて,共通の操作をまとめてゴリゴリ実行させる

これを踏まえて,オブジェクト指向について少し詳しく見ていきましょう.

カプセル化

カプセル化には,

  • 中でどんな処理をしているか分からなくても皆がその機能を簡単に使える
  • 不用意に誰かが機能を改変してしまうことを防ぐ

といった効果があります.

一つ例を出しましょう.

私達は冷蔵庫を使って「モノを中に入れる」だけでそれを冷やすことが出来ますね.

このとき私達は,「冷蔵庫がどうやって中を冷やしているのか」といった中身まで知っていなくても,簡単に使いこなすことができています.

こんなことが出来るのは,中にある複雑な機械や電子部品を "カプセル化" してしまい,利用者には「ドアを開ける」「ドアを閉める」といった単純な動作だけで冷蔵庫が動かせるよう開発者が設計したからに他なりません.

おかげで専門知識を持たない人でも簡単に使えるようになった他,不注意や悪戯で冷蔵庫の内部を壊してしまうことを防ぐことにも繋がりました.

冷蔵庫を細かく分解して見ていくと,中からはカプセル化されたモノがたくさん出てきます.

例えば,"モーター"などもそうです.

言わずもがなモーターは「電気を流すと回る」部品ですが,「何故回るのか」は知らなくても私達はモーターを使うことが出来ます.また,モーターも簡単には壊れされたりしないように硬い金属のケースに入っていることがほとんどです.

モーターは冷蔵庫以外にも様々な製品の中で使われている汎用的な製品です.

適切な粒度でカプセル化されたモノは,他の場所に持って行ってもすぐに使えることができ,これを移植性(もしくは可搬性)が高いといいます.モーターは非常にすぐれたカプセル化が行われており,とても移植性が高く便利なので色々な場所で使われていると言えそうです.

上の例え話は,プログラムにもそのまま適用できます.

文字列を画面に表示させたいとき,私達は printf("文字") のように関数を呼び出して一行でそれを行うことが出来ます.

しかし,実際は 表示させたい文字の情報を受け取り,0と1のデジタルデータに変換してからディスプレイに文字を映し出すよう信号を送って……のように裏で行われている処理がたくさんあるはずです.私達はそうした仕組みを知らずとも printf() を使うことが出来ますし,私達の目的が「printf()の中身を知ること」ではなく「文字列を画面に表示させたい」である以上,こうした難しい処理を隠蔽してくれることは非常に有難いことなのです.

転じて,私達が書くプログラムもまた,出来るだけ上手くカプセル化されたモノであるべきでしょう.

目的をなるべく簡単に達成できるような,関数一つ実行させればそれで万事解決してくれるような,完成されたプログラムの部品が出来たとき,それはまさにプログラマーにとっての財産となってくれるはずです.

カプセル化されたモノは与えられた任務を忠実にこなしてくれます.さらに使用者は積み木のようにカプセル化されたモノを組み合わせることでより複雑なモノを作り出すことが出来るのです.

この章の内容をまとめると,

  カプセル化を行うことで,プログラムは利便性移植性信頼性を獲得した

と言えそうです.

継承

継承によって得られるメリットも色々ありますが,最も大きい恩恵は,

  • プログラムのソースコード全体の中で,「同じ記述は一カ所にだけ」を徹底させることが出来る.

という事でしょう.他にも「あるコードを継承させることで他のコードに簡単に機能を追加できる」というのもありますが,結局のところは上のメリットに収束します.

ここでも一つ例をだしてみましょう.例えば,あなたがいまゲームを作っていたとします.

ゲームには主人公と敵キャラを登場させます.

主人公と敵キャラは別の存在なので,それぞれ別個に設計をしコーディングしていました.

しかし,主人公と敵キャラには共通点も多いことにあなたはすぐに気づくはずです.

「歩く」「攻撃する」「ダメージを受ける」etc…

主人公と敵キャラのコードを行ったり来たりしながら開発しているとあなたはこう思うはずです.

「似たような処理なんだからどっかにまとめて書かせてよ……」

そこで,継承という仕組みが出てきます.

主人公も敵キャラもどちらもまとめて,"キャラクター" として扱いましょう.

そして,"キャラクター"に共通する処理はすべて"キャラクター"に記述します.

あとは主人公も敵キャラも"キャラクター"から派生させて作れば良いのです.

これにより,散らばっていたコードを集約し,「同じ記述は一カ所にだけ」にすることが出来ました.

この「同じ記述は一カ所にだけ」の法則は絶大なメリットをもたらします.(逆に「同じ記述が色んな場所にある」は絶大なデメリットしかもたらしません)

色々あるのですが,一番の理由は,

  • プログラムを修正するとき一カ所だけ直せばいい

ということに尽きるでしょう.

先程の例で,継承をせずに主人公と敵キャラに別々に「歩く」処理を実装していたとします.

全体的に歩く挙動が気に入らなかったので,歩く速度を三倍に調整しようとしたとき,あなたは主人公と敵キャラの両方のソースコードを見て,それぞれの「歩く速度パラメータ」を三倍に調節してこなければいけません.

さらにキャラクターが増えていくと,まず主人公の「歩く速度パラメータ」を3倍にして,次に敵キャラ1のソースコードを見に行って3倍にして,忘れず今度は敵キャラ2のコードも修正して…….となっていき,とてもじゃないですが人力で管理するのが嫌になってくるでしょう.

また,単に面倒臭いという理由以外にも,「同じ記述がいろんな場所にある」ことは発見しづらいバグの温床になりかねません.

  主人公の「歩く」機能は修正したけれど,敵キャラの「歩く」機能は修正し忘れてた.

なんてことが起こるのは日常茶飯事ですし,これがゲームのシステムに関わるような機能だった場合はより深刻です.

継承を使うことで,記述を一カ所にまとめ,様々な恩恵を得ることができるのです.

多態性(ポリモーフィズム)

多態性は継承と組み合わせて真価を発揮します.

先の「主人公と敵キャラ」の例をそのまま使いましょう.

「歩く」機能を別々に持たせていた場合,全体の歩く処理は以下のようになると思います.

// 主人公のコード  
class Player {  
    public int Walk() {  
        // 主人公の歩く処理  
    }  
}  

// 敵キャラのコード  
class Enemy {  
    public int Walk() {  
        // 敵キャラの歩く処理  
    }  
}  

static void Main() {  
    // 主人公キャラの生成  
    var player =  new Player();  

    // 敵キャラの生成  
    var enemy = new Enemy();  

    // 「歩く」処理の実行  
    player.Walk();  
    enemy.Walk();  
}  

これは,継承と多態性を組み合わせると以下のように書き直すことが出来ます.

// キャラクターに共通のモノを定義  
class Character {  
    public int Walk() {  
        // 歩く処理  
    }  
}  

// キャラクターを継承した主人公のコード  
class Player : Character {  
}  

// キャラクターを継承した敵キャラのコード  
class Enemy : Character {  
    public int Walk() {  
        // Walk()を再定義することで,敵側の歩く処理だけちょこっと変更できたりする  
    }  
}  

static void Main() {  
    // キャラクターリストの作成  
    var list = new List<Character>();  

    // 主人公キャラを生成してリストに追加  
    list.Add(new Player());  

    // 敵キャラを生成してリストに追加  
    list.Add(new Enemy());  

    // 「歩く」処理の実行  
    for(int i = 0; i < list.Count; ++i) {  
        list[i].Walk()  
    }  
}  

一見すると,コードの行数が増えて複雑になったように見えますが,前の例と比べると後者の方が圧倒的に良いコードです.

前者のコードは,主人公や敵キャラを追加するたびにプログラマーの私達が一々Walk()を実行させてあげたりと面倒を見てあげなければいけません.

一方後者は,"キャラクター" から派生したものをリストに登録することで一括管理しています.当然,主人公や敵キャラの数がこれからいくら増えてこようとも,あるいは,主人公と敵以外のキャラクターを増やしたとしても,ほんの僅かにコードを変更するだけで私達はそれらに対応することが出来ます.

ここで,後者のコードの最後,"「歩く」処理の実行"をご覧ください.

    // 「歩く」処理の実行  
    for(int i = 0; i < list.Count; ++i) {  
        list[i].Walk()  
    }  

これが多態性(ポリモーフィズム)と呼ばれる処理の真価になります.

この list に入っているのは,実際は主人公だったり敵キャラだったり,有り体に行ってしまえば "データ型の異なるオブジェクト" になります.

通常,データ型の異なるオブジェクトをこのように一緒くたに扱うことは出来ないのですが,継承を使うことで,主人公も敵キャラもそれぞれ "Character" として振る舞うことが出来るようになっているのです.

Character には Walk() が定義されているので,上のような記述をしてもエラーを出すことなく正常に動作します.

PlayerでありながらCharacterのように

EnemyでありながらCharacterのように

まるで複数の"実態"を持っているかのように扱えるからこそ,"多態性" と呼ばれているのです.

結論

少し長くなってしまいましたが,まとめです.

オブジェクト指向はプログラマーに何を求めていたのでしょうか.

それは以下の三点に要約されると考えます.


  • プログラムを小さな単位に分割しカプセル化することで,「利便性」・「移植性」・「信頼性」これら三つの要素を備えた質の高いソースコードを書かせる
  • 「同じ記述は一カ所にだけ」を徹底させる
  • 将来の変更や拡張に柔軟で,管理の負担が少ない設計をさせる

極論を言ってしまえば,オブジェクト指向なんて知らなくても,この三つのお約束が守れていれば良いプログラムが書けるはず,という事です.

これらを達成する為の方法論に完璧な答えはなく,日夜プログラマーは設計に関して良い方法を模索しているものかと思われます.

オブジェクト指向はその解決策として一案を提示していますが,あらゆる状況で適用できる完璧な方法ではないことも事実です.

上記の3つのルールについて,どんな設計が良いか考えながら試行錯誤していくことで,少しずつ良いプログラムが書けるようになっていけるはずです.

蛇足1. コーディングについてもう少し

本記事ではその大部分を,"設計"の説明に割いてしまいましたが,コーディングも重要なスキルのひとつであることは間違いありません.

簡単な "並べ替え(ソート)" の問題にしても,無数のアルゴリズムや表現方法があり,中には高いコーディングの技術がないと理解できなかったり正しく書けないコードも出てくるでしょう.

また,プログラマーではない,プログラムの利用者が感じる不満はコーディングの拙さによるものが多いです.

処理が遅い,表示に時間がかかる etc…

上手なコーディングを行うことで,利用者へのストレスを減らし,好評価を得る事にもつながります.

コーディングについては,インターネットで検索すれば色々なものが見つかりますが,オープンソースのソフトウェアのソースコードを見ることも勉強になるかと思います.

また,競技プログラミングの問題を解いてみることもおススメです.やっている間はゲーム感覚で結果を競えるのでモチベーションにもなりますね.

"設計" と "コーディング" はプログラミングの両輪です.どちらも鍛えていきたいですね.

蛇足2. 具体的にはどうすれば?

三つのお約束を守ればいいコードが書けると言いましたが,抽象的過ぎて解り辛かったかもしれません.

具体的にどうしていけばいいか,個人的な意見で恐縮ですが書かせて頂きます.

まず最初は,とにかくコードを小さくパッキングするというつもりでコードのファイル分割に挑戦してみると良いかと思われます.

一般的に,一つのファイルは100 ~ 200行でまとめるのが理想と言われてはいますが,最初のうちは10行とか20行程度の短いコードであったり,逆に1000行とか2000行を超えてしまうような巨大なファイルでも構いません.

とにかく,すべての処理を一つのファイルに詰め込んで完結させてしまうのではなく,機能毎にどんどん切り離して,独自の関数をつくったり,クラスをつくったり,ファイルをつくったりしていくのが良い練習になると思います.

また,同時並行で「同じ記述は一カ所にだけ」も実践していければ最高です.

別に継承という仕組みを使えなくても問題ありません.似たような処理を(ソースコードの行数的な意味で)近いところに置いておくだけで,可読性は上がるものです.

「使うその場その場で変数を宣言する」というのも一つの方法ですが,「関連する変数(例えば主人公のステータスのような)はまとめて宣言→定義」とすると少し見やすくなります.

更にやる気があれば,継承や多態性といった仕組みを理解していけばいいでしょう.

これから新しく始めるプロジェクトからでも,既に今自分が持っているプログラムのリファクタリングからでも構いません.

少し意識してみるだけでも大分変わってくると思います.

蛇足3. 設計で迷ったときは

設計で迷ったときはいつも自分に以下の事を言い聞かせています.

  • 1つの関数には1つの機能しか入れない(「計算して結果を出力する」という関数があった場合 → "計算" と "出力" で機能を分割する)
  • 迷ったら常に "広い" 方へ (入力として数値と文字列どちらを受け取るか → どちらも受け取れるようにする)
  • 1つのファイルは長くても 100 ~ 200 行程度まで.収まらないようなら分割or設計を見直す

個人的に,プログラミングの世界はSimple is Beautifulだと思っています.

処理部が冗長になって行ったり複雑に感じたときは,思い切って設計からやり直してみることで,案外とすっきりした構造に落ち着くことは良くあることだと思っています.

終わりに

この記事が皆さんのプログラミングライフを少しでも豊かにすることが出来れば幸いです.
ご意見・ご指摘などありましたら是非コメント頂ければと思います.

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@yanagiの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!