てぃってぃの楽しい副業日記!

ワイと嫁(てぃってぃ)夫婦の、副業、育児、プログラミング、筋トレ、ゲームを綴ったブログです☆

オブジェクト指向プログラミングとは?(初心者向け解説)

こんばんは、てぃってぃ(嫁)の夫です。

本記事では、「オブジェクト指向」とは何か、プログラミングにおいてどう活用できるのか、実際のコードはどう書くのか、イラストや実際のソースコードを用いて、実践で使えるようになるための情報を記載します。

  • プログラミングに興味がある
  • ゲーム作りに興味がある
  • Java、C#、Kotlin、Swiftに興味がある
  • 背景、歴史より実践的に使えるようになりたい
  • オブジェクト指向でプログラミングしたい


「オブジェクト指向」に限らずプログラム全般に言えますが、概念をすべて理解しきるより、実際に物を動かせるようになるために必要な知識を抑えて、実践で使えるようになった方が実用的です。実際の製品プログラムでもそこまで細かくは意識していません。本記事では、プログラマー歴10年以上になる一応プロの筆者が、「オブジェクト指向」を実践で使えるようになるための記事をどこにも負けないぐらい分かりやすく書きたいと思います!

Java、C#等の開発環境(コンパイル環境)があれば、実際にソースコードを書きながら理解できて、効率よく理解が進むと思うのでオススメです。開発環境がない方でも、文章とイラストでも十分勉強になると思うので読んでみてくださいね。
(※ 特に「VisualStudio2019」が無料でインストールできて、Unityでのゲーム開発にも使用できるのでオススメ環境です。)


【はじめに】オブジェクト指向とは?

ずばり、「プログラムを『物』を使って動かすという概念」です。これにつきます。…よく分からないという方も、とりあえずこのまま読み進めてください(笑)。(本記事を最後まで読んで、もう一周読むときには、きっとしっくり来ているはず)

最近の商用プログラムでも多く使われています。大規模監視装置(工場、発電所、高速道路等)や、Windows・Android・iOSアプリ、ゲームソフト開発にも使える、万能な概念です。開発スタイルに関しても、流用や改造、大人数での開発にも非常に向いており、開発現場でも好まれて使用されています。今時開発現場では、VBやCといった言語からオブジェクト指向言語(Java、C#、等)にどんどん置き換わりつつあり、SEとして働きたい方、趣味や副業にしたい方もぜひマスターしておきたい手法です。

オブジェクト指向による代表的なプログラミング言語には、以下のようなものあります。

・Java

・C#

・Kotlin

・Swift


オブジェクト指向の有用性はわかっていただけたでしょうか。現場の私から見ても間違いなく今後必要となる知識です。
それでは第1章からは、オブジェクト指向の内容について、イラストとソースコードを用いて、順番に説明してきたいと思います。

本記事では、特にオブジェクト指向の3大要素と言われる「継承」「カプセル化」「ポリモーフィズム」を中心に解説していきます。

【第1章】「クラス」と「インスタンス」

オブジェクト指向を理解するために、まず最重要なのは「クラス」「インスタンス」について理解することです。オブジェクト指向の基礎中の基礎になります。

クラス

クラスとは、簡単に言うと「設計図」のことです。

イメージしやすいために、RPGゲームの敵モンスターを思い浮かべてください。敵モンスターは、どのような機能を実装していないといけないでしょうか?パラメータ面では名前や攻撃力・防御力、動作面では通常攻撃や特殊攻撃などなど、色々ありますよね。

例えば、今回ではモンスターの設計図(クラス)として、「名前」「HP」「攻撃力」「通常攻撃」を実装するとしましょう。これらを定義した「設計図」をクラスといいます。

「モンスター」クラスを絵、コード(例としてC#)にしてみました。

【オブジェクト指向】クラス(設計図)
【オブジェクト指向】クラス(設計図)

/* MonsterBase.cs */
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApp
{
    /// <summary>
    /// モンスター基底クラス
    /// </summary>
    public class MonsterBase
    {
        protected string Name;    /* モンスター名 */
        protected int HP;         /* HP           */
        protected int Power;      /* 攻撃力       */

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="Name">モンスター名</param>
        /// <param name="HP">HP</param>
        /// <param name="Power">攻撃力</param>
        public MonsterBase(string Name, int HP, int Power)
        {
            this.Name  = Name;
            this.HP    = HP;
            this.Power = Power;
        }

        /// <summary>
        /// 通常攻撃
        /// </summary>
        /// <returns>ダメージ</returns>
        public int NormalAtack()
        {
            Console.WriteLine($"{Name}の攻撃!");
            return Power;
        }
    }
}


以下、解説します。
・クラス名を「MonsterBase」としています。パラメータ(変数)は「プロパティ」、動作(関数)は「メソッド」と呼ぶので、覚えておいてください。両方含めて「メンバー」とも呼んだりします。
・クラス名と同じ関数「MonsterBase」があると思いますが、これは「コンストラクタ」と呼ばれる特別な関数でして、この設計図をもとに実体(モンスター)を生み出す時に、真っ先によばれる関数となります。(後ほど説明)
・メンバーの修飾子は「protected」でないといけません。クラスを「継承」する際に、派生クラス側が親クラスのメンバにアクセスする際に必要となります。(後ほど説明)

これで無事にモンスタークラス(設計図)ができました。ただ、現実と同じで設計しただけではダメなので、次はこの設計図をもとに、モンスターを生み出して動かします

インスタンス(オブジェクト)

クラスが「設計図」であれば、インスタンス(オブジェクトともいう)は「設計図を元に生み出した実体」です。 先ほど書いたモンスタークラス(設計図)をもとに、「スライム」というモンスターを生み出してみましょう!

設計図をもとに、実際のパラメータを入れて実体化したものが「インスタンス」になります。絵にするとこうなります。

【オブジェクト指向】クラスのインスタンス化
【オブジェクト指向】クラスのインスタンス化

「モンスター」という設計図を元に、「スライム」という実体を生成した形になります。これをソースコードでは、以下の様にしてインスタンス化(実体化)します。

/* Program.cs */
using System;

namespace ConsoleApp
{
    class Program
    {
        /// <summary>
        /// メイン関数
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // スライムのインスタンス化
            MonsterBase Slime = new MonsterBase("スライム", 5, 2);

            // スライムの通常攻撃
            Slime.NormalAtack();
        }
    }
}


ポイントは、「new」によって、クラス(設計図)をインスタンス化(実体化)できるというわけです。そして「new」する際に指定している引数が、先ほどモンスタークラスで作成したコンストラクタにわたっていき、モンスターのプロパティ(名前/HP/攻撃力)が引数で指定した値で設定された、というわけです。
実際に動かせる人は動かしてみてください。実行すると、以下の様に表示されると思います。

スライムの攻撃! 2のダメージ!

ここまでが超基礎編になります。「クラス」と「インスタンス(オブジェクト)」の違いについて、以下を理解できていたら、OKです。

  • 「クラス」は設計図
  • 「インスタンス(オブジェクト)」は実体
  • 「new」でインスタンス化できる
  • 「new」で最初に呼ばれる関数がコンストラクタ


【第2章】「継承」(基底クラス/派生クラス)

では、スライムを2匹生み出したい!というとき、どうすればよいでしょうか。
先ほどのソースをもとに、悪い例を書くと、こうなります。

    :
    MonsterBase Slime1 = new MonsterBase("スライム", 5, 2);
    MonsterBase Slime2 = new MonsterBase("スライム", 5, 2);
    :

スライムを生み出す度に、「スライム」「5」「2」を指定しないといけないですよね。
じゃぁ100匹生み出したとして、スライムのHPを「6」にしたいとき、どうする?100行直す?
…そんなことしたら、どこか修正間違えそうですよね。

答えは簡単で、「スライム」もクラス(設計図)にするということです。

そして、ここで、考えてほしいのが、これからスライムだけじゃなくて、スライムベス、キングスライムも実装するとしましょう。どのクラスにも、モンスタークラスで定義した「名前」「HP」「攻撃力」をそれぞれ実装するのか?後からすべてのモンスターに「防御力」を増やそうと思ったとき、スライムクラス、スライムベスクラス、キングスライムクラス、全部直すのか??

⇒こういった問題を解決するために、「継承」という概念があります。"あるクラス(=基底クラス)の持つプロパティ、メソッドをすべて引き継いだクラス(=派生クラス)が作れるのです。
※ 基底クラスは親クラス、スーパークラス、ベースクラスという言い方もします。

例えば、「スライム」クラスは、「モンスター」クラスを継承して、独自に「強攻撃」が行えるモンスターとします。絵に描くとこのようなイメージです。

【オブジェクト指向】クラスの継承
【オブジェクト指向】クラスの継承

また、ソースコードにすると、こうなります。

/* Slime.cs */
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApp
{
    public class Slime : MonsterBase
    {
        public Slime()
        {
            this.Name  = "スライム";
            this.HP    = 5;
            this.Power = 2;
        }

        /// <summary>
        /// 強攻撃
        /// </summary>
        /// <returns></returns>
        public int StrongAtack()
        {
            Console.WriteLine($"{Name}の攻撃! {Power * 2}のダメージ!");
            return Power * 2;
        }
    }
}


ポイントは、クラスを「Slime : MonsterBase」と定義しているところです。これで「スライム」クラスは「モンスター」クラスを継承することができます。

見ての通り、Slimeクラスの中に基底クラスのプロパティ(名前/HP/攻撃力)や、メソッド(NomalAtack())等のコードは書いていませんが、MonsterBaseを継承しているため、Slimeクラスでもメンバとしてそのまま使用することができるのです。一方、「StrongAtack()」は、Slimeクラスのみが使用できます。

この概念により、例えば全モンスターに「防御力」を追加したいときは、基底クラスである「MonsterBase」さえ修正すればよいのです。

また、メイン側は、以下のようにしてスライムを生成すればよいだけです。

    :
    Slime slime1 = new Slime();
    Slime slime2 = new Slime();
    :

だいぶ簡単、かつ量産しやすい見た目になりました。

以上で第2章は終わりです。ちなみにここまでの知識でも、十分現場で動くものは作れます。第2章としては、以下を抑えられたらOKです。

  • 「基底クラス」のメンバを「継承」して「派生クラス」ができる
  • 「派生クラス名 : 基底クラス名」と宣言することで継承できる


【第3章】「カプセル化」

先ほど、スライムはどのスライムもHPが5なんだから、共通の設計図にしたらいいじゃないか、と記載しました。
その続きになるのですが、例えばオブジェクト指向も何もしらないプログラマーが、スライムのHPを6にしようと思って、このようなソースを書いたとしましょう。

    :
    Slime slime = new Slime();
    slime.HP = 6;
    :

実は、第2章までに記載しているコード上、できない(コンパイルエラー)のですが、仮にできたとします。
もしこのようにコーディング出来てしまうとすると、ここで生み出された「slime」というインスタンスだけはHP=6になりますが、クラス(設計図)はHP5のままなので、次に生み出すslimeはHP=5なわけです。意図してそうしているならアリですが、基本、設計図を無視して実体を作るのはやめるべきです。

現実に例えるなら、家電量販店で同じ東芝のテレビが5台ぐらい並んでて、よく見ると1台だけ変な形している、みたいな。他の4台の東芝TVは設計図通りなので同じですが、1台だけは生み出した後にいじくってしまったわけです。

現実ならそのようなことをした生産者は当然クビ(笑)ですが、オブジェクト指向の場合、「カプセル化」という概念によって、これを防ぐことが出来ます。作ったもの(インスタンス)のパラメータは後から触れない様にすることが可能です。

アクセス修飾子

「パラメータをインスタンス化してからは触れなくする」ことを可能にするのが「アクセス修飾子」です。メンバーについている「protected」という修飾子です。もう一度見てみましょう。

    /// <summary>
    /// モンスター基底クラス
    /// </summary>
    public class MonsterBase
    {
        protected string Name;    /* モンスター名 */
        protected int HP;         /* HP           */
        protected int Power;      /* 攻撃力       */

        /// <summary>
        /// 通常攻撃
        /// </summary>
        /// <returns>ダメージ</returns>
        public int NormalAtack()
        {
            Console.WriteLine($"{Name}の攻撃! {Power}のダメージ!");
            return Power;
        }
    }


「protected」「private」とついているメンバーは、「slime.HP = 5」という風に、インスタンスからアクセス(read/write)することが出来ません。両者の違いは、protectedは派生クラスからアクセス(read/write)することは可能なのに対し、「private」はどのクラスからもできない、一番頑丈な修飾子になっている点です。
一方、「public」は、外部からも自由にアクセスすることが出来ます。(メイン関数側で「slime.NormalAtack()」という風に読んでいましたね。)

上記の関係をまとめると下表のようになります。

修飾子自クラス内派生クラス他クラス
public
protected×
private××

※ 〇:アクセス可能、×:不可能

アクセサ

また、他クラスに対して、「読み取りだけは許可したい」、または「書き込みだけは許可したい」という場合があると思います。そのようにしたい場合は「アクセサ」というものを定義して、読み取り/書き込みだけを許可するといったことが可能です。
このようなコードになります。

        /// <summary>
        /// 読み取り専用パラメータ
        /// </summary>
        private int _ReadParam;
        public int ReadParam
        {
            get { return _ReadParam; }
        }

        /// <summary>
        /// 書き込み専用パラメータ
        /// </summary>
        private int _WriteParam;
        public int WriteParam
        {
            set { this._WriteParam = value; }
        }


アクセサを使用することで、「slime.ReadParam」は読み取りだけ可能で、「slime.WriteParam」は書き込みだけ可能になります。外から触る必要があるパラメータだけ「public」で指定して、その他は基本「private」にすると、より安全なソースコードを作ることが出来ます。また、アクセサは関数と同様に処理が書けるため、「何かをセットされたら同時に他のメンバを更新する」といったような処理も行えます。
現場では、状態を表すメンバのSet内で、「前回状態を更新」みたいな処理をよく入れたりします。

以上が「カプセル化」の説明になります。ここを上手に意識できるようになるには、ある程度経験が必要かと思われますので、たくさんコードを書いて慣れましょう。「カプセル化」のポイントは、以下の通りです。

  • 「private」「protected」「public」のアクセス範囲を知る
  • 「アクセサ」でアクセス範囲を調整する


【第4章】「ポリモーフィズム」(オーバーライド/オーバーロード)

三大要素の最後、「ポリモーフィズム」について説明します。
日本語では「多態性」「多様性」とか言われるみたいですが、現場で使ったこともほとんどないので、単語は覚えなくていいです。

オーバーライド(上書き)

例にならって、モンスターで考えましょう。「ほとんどのモンスターで同じような処理になるけど、あるモンスターだけはちょっと動きを変えたい…」という場合があったとします。例えば、「通常攻撃を2回行える」モンスターを考えます。

【オブジェクト指向】オーバーライド

通常攻撃はほとんどのモンスターは1回攻撃ですが、この子(キラーマシン)のように、2回通常攻撃できる敵を作りたいとします。こういうときのために、基底クラスの関数を、派生クラスで書き換えることが出来ます。これを「オーバーライド」(上書き)と呼びます。
イメージはこんな感じです。

【オブジェクト指向】オーバーライド
【オブジェクト指向】オーバーライド
ソースコードでは、以下2点がポイントになります。
① 基底クラスの関数をオーバーライド可能にする修飾子「virtual」を付ける
② オーバーライドする側は「override」を付ける

まず、基底クラス側のソースです。通常攻撃をオーバーライド可能にするために、「virtual」を付けて宣言します。

    public class MonsterBase
    {
        :
         /// <summary>
        /// 通常攻撃
        /// </summary>
        /// <returns>ダメージ</returns>
        public virtual int NormalAtack()
        {
            Console.WriteLine($"{Name}の攻撃! {Power}のダメージ!");
            return Power;
        }
    }


次に、オーバーライドする側のソースです。「override」をつけてオーバーライドします。「base.NormalAtack()」の「base」とは、基底クラスという意味です。つまり、キラーマシンは、通常攻撃をオーバーライドし、2回基底クラスの通常攻撃を行うようにしました。

    public class KillerMachine : MonsterBase
    {
        public KillerMachine()
        {
            this.Name  = "キラーマシン";
            this.HP   = 80;
            this.Power = 15;
        }

        public override int NormalAtack()
        {
            base.NormalAtack();
            base.NormalAtack();

            return Power;
        }
    }


では、メイン側でインスタンス化して、実際に動かしてみましょう。

        static void Main(string[] args)
        {
            // スライム、キラーマシンのインスタンス化
            Slime slime1 = new Slime();
            KillerMachine killerMachine = new KillerMachine();

            // スライムの通常攻撃
            slime1.NormalAtack();

            // キラーマシンの通常攻撃
            killerMachine.NormalAtack();
        }


実行結果はこちらになります。

スライムの攻撃! 2のダメージ!
キラーマシンの攻撃! 15のダメージ!
キラーマシンの攻撃! 15のダメージ!

ちゃんと、オーバーライドできているのが分かりますね!

抽象クラス

また、必ずオーバーライドされることを前提としたクラスやメソッドを作ることもでき、「抽象」クラス/「抽象」メソッドと呼ばれます。イメージとしては、どのクラスでも必ず実装しないといけないが、クラスに応じて必ず独自の処理が必要な場合、使用します。(先ほどのキラーマシンの例と逆ですね。)
※ 現場では「ガワを作る」とか言ったりします。

例えば、モンスター毎に必ず「特殊攻撃」を実装したい場合。モンスター毎に異なるが、「特殊攻撃」という処理は必ず必要なので、基底クラスにて「abstract」で宣言しておきます。オーバーライドが前提なので、メソッドの中身は一切せず、関数宣言のみにします。(しないとコンパイルエラーになります)

    public abstract class MonsterBase
    {
        :
        /// <summary>
        /// 特殊攻撃
        /// </summary>
        public abstract void SpecialAtack();
    }

そして、オーバーライドする側は、先ほどのキラーマシンと同様の記述方法でOKなので、記載は省略しますね。

オーバーロード

また、ポリモーフィズムには「オーバーロード」という手法もあります。こちらは、関数名は同じだが引数が異なる関数を定義することが出来ます。使い道としては、実行したい処理は一緒だが、入力引数のパターンが複数ある様な場合にオーバーロードで定義することが多います。

ソースコードの例は以下の様になります。 どちらも同じ関数名ですが、引数だけ違いますね。

        public virtual int NormalAtack()
        {
            Console.WriteLine($"{Name}の攻撃! {Power}のダメージ!");
            return Power;
        }

        public virtual int NormalAtack(string targetName)
        {
            Console.WriteLine($"{Name}の攻撃! {targetName}に{Power}のダメージ!");
            return Power;
        }


以上で、ポリモーフィズムの説明は終わりです。単語としての頻度は「オーバーライド」が一番使う機会が多いです。以下を抑えておいてください。

  • 「オーバーライド」=派生クラスで基底クラスの関数を上書きできる
  • 「オーバーロード」=引数のみ異なる関数を複数作成できる


【まとめ】

大分長くなってしまいました(15000文字以上w)が、以上で、オブジェクト指向の三大概念と言われる内容は終わりです。ゲームに向いているというのも何となく伝わったのではないでしょうか。

1回ですべて覚えてきるのも大変だと思うので、コードを書いて動かしながら、何周か読んでもらえると、頭に入ってくると思います。わからないことがあれば、気軽にコメントなりメールなりして下さいね。

一生使えるスキルなので、絶対身につけておきましょうね!

ご購読、ありがとうございました。