【オブジェクト指向】リモコンで覚えるオブジェクト指向のメリット

オブジェクト指向オブジェクト指向

この記事の内容

プログラミングを学習している方で、以下のような悩みを抱えている人が多いのではないでしょうか?

  • オブジェクト指向が何のためにあるのかわからない
  • インターフェースとか別に使う必要ないと感じている

この記事では、上記の悩みを解決するための内容を記載しています。

ここではリモコンを例にして、オブジェクト指向を解説していきます。

オブジェクト指向のすばらしさを学んでいただきたいですが、やはり理解には多少時間がかかります。

しかし、しっかりと読んでいただければ、オブジェクト指向のすばらしさが理解いただけると思ってますので、ぜひ最後まであきらめずにご覧ください。

今回解説するアプリの仕様について

以下の仕様のアプリを例に解説していきます。

  • TVリモコンアプリ
  • A社、B社、C社のメーカーに対応しており、切り替えが可能

TVのリモコンアプリで、メーカーを切り替えることにより、A社、B社、C社に対応することができるリモコンになります。

実際には、メーカーの各ボタンごとに送信する赤外線データの内容は変わりますが、ここでは説明のために、赤外線データではなく、文字列でどのメーカーのどのボタンが押されたのかアプリ上で分かるようにしています。

また、4~9のボタンは、今回使用しません。

リモコンアプリの動作イメージを以下に示します。

このように、メーカーを切り替えると、そのメーカーに合わせた送信内容となるアプリを例にして説明します。

上記のリモコンアプリを、

  • オブジェクト指向を意識しない場合
  • オブジェクト指向を意識する場合

でそれぞれ作成し、違いを見ていきます。

オブジェクト指向を意識しない場合

まずソースコードを記載します。(C#で記載)
少々長いですが、やっていることは単純ですので眺めてみてください。

using System;
using System.Diagnostics;
using System.Windows.Forms;

namespace RemoteControllerForOOP
{
    public partial class MainForm : Form
    {
        /// <summary>
        /// A社が選択状態であるかを表す
        /// </summary>
        private bool _isAManufacturerSelected = false;

        /// <summary>
        /// B社が選択状態であるかを表す
        /// </summary>
        private bool _isBManufacturerSelected = false;

        /// <summary>
        /// C社が選択状態であるかを表す
        /// </summary>
        private bool _isCManufacturerSelected = false;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainForm()
        {
            InitializeComponent();
        }

        /// <summary>
        /// フォームロード時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_Load(object sender, EventArgs e)
        {
            //初回はA社を選択
            rdoAManufacturer.Checked = true;
        }

        /// <summary>
        /// //メーカー選択変更時のイベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Manufacturer_RadioButton_CheckedChanged(object sender, EventArgs e)
        {
            //ラジオボタンがONになった場合のみ処理する
            RadioButton radioButton = sender as RadioButton;
            if (!radioButton.Checked)
            {
                return;
            }

            //全てのメーカーの選択状態をいったんOFF
            _isAManufacturerSelected = false;
            _isBManufacturerSelected = false;
            _isCManufacturerSelected = false;

            //どのメーカーのラジオボタンがONになったか判定
            //そのメーカーのフラグをONにする
            if(radioButton.Text == "A社")
            {
                _isAManufacturerSelected = true;
            }
            else if(radioButton.Text == "B社")
            {
                _isBManufacturerSelected = true;
            }
            else if(radioButton.Text == "C社")
            {
                _isCManufacturerSelected = true;
            }
            else
            {
                Debug.Assert(false);
            }
        }

        /// <summary>
        /// 電源ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnPower_Click(object sender, EventArgs e)
        {
            if (_isAManufacturerSelected)
            {
                txtSendingContent.Text = "A社-電源ボタンON";
            }
            else if (_isBManufacturerSelected)
            {
                txtSendingContent.Text = "B社-電源ボタンON";
            }
            else if (_isCManufacturerSelected)
            {
                txtSendingContent.Text = "C社-電源ボタンON";
            }
            else
            {
                Debug.Assert(false);
            }
        }

        /// <summary>
        /// 「1」ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOne_Click(object sender, EventArgs e)
        {
            if (_isAManufacturerSelected)
            {
                txtSendingContent.Text = "A社-「1」ボタンON";
            }
            else if (_isBManufacturerSelected)
            {
                txtSendingContent.Text = "B社-「1」ボタンON";
            }
            else if (_isCManufacturerSelected)
            {
                txtSendingContent.Text = "C社-「1」ボタンON";
            }
            else
            {
                Debug.Assert(false);
            }
        }

        /// <summary>
        /// 「2」ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnTwo_Click(object sender, EventArgs e)
        {
            if (_isAManufacturerSelected)
            {
                txtSendingContent.Text = "A社-「2」ボタンON";
            }
            else if (_isBManufacturerSelected)
            {
                txtSendingContent.Text = "B社-「2」ボタンON";
            }
            else if (_isCManufacturerSelected)
            {
                txtSendingContent.Text = "C社-「2」ボタンON";
            }
            else
            {
                Debug.Assert(false);
            }
        }

        /// <summary>
        /// 「3」ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnThree_Click(object sender, EventArgs e)
        {
            if (_isAManufacturerSelected)
            {
                txtSendingContent.Text = "A社-「3」ボタンON";
            }
            else if (_isBManufacturerSelected)
            {
                txtSendingContent.Text = "B社-「3」ボタンON";
            }
            else if (_isCManufacturerSelected)
            {
                txtSendingContent.Text = "C社-「3」ボタンON";
            }
            else
            {
                Debug.Assert(false);
            }
        }


    }
}

 

どうでしょうか?
これでも動きますが、各メーカーごとの切り替えするためのif文の分岐が多く、冗長な印象をうけますね。

また、以下のケースを考えた場合、どのようなコードになるのか想像できるでしょうか。

  • 4~9のボタンやほかのボタン(番組表など)の実装
  • 他のメーカーに対応する場合
  • メーカーを削除する場合

一つ一つ考えていきます。

4~9のボタンの実装やほかのボタン(番組表など)

ボタンごとに各メーカーの分岐が必要になりますね。

分岐が多くなり、メンテナンスがし辛くなります。

他のメーカーに対応する場合

各ボタンに追加するメーカーの分岐追加が必要になりますね。

また、分岐箇所には他のメーカーの分岐もあるため、間違って触ってしまうと、もともとあったメーカーの処理に影響を与えかねません。

メーカーを削除する場合

各ボタンの削除対象のメーカーの分岐を削除する必要があります。

これも、他のすでにあるメーカーの処理が同一関数に記載されているため、誤って触ってしまう可能性があり危険です。

では、どうすればいいのか

適切なインターフェース分け、クラス分けを行うことにより、これらの問題を解決できます。

次に、オブジェクト指向を意識して作成したソースコードを解説していきます。

オブジェクト指向を意識する場合

では、オブジェクト指向を意識してコーディングした場合を解説します。

まず、どのようなクラス構成になるのか、クラス図を示します。

クラス図が読めない方は、なんとなく登場するクラスやインターフェースを把握してもらい、ソースコードを見ていただければと思います。

MainFormソースコード

以下に、MainFormのソースコードを記載します。

if文の分岐がかなり消えて、すっきりしたことがわかると思います。

using System;
using System.Diagnostics;
using System.Windows.Forms;

namespace RemoteControllerForOOP
{
    public partial class MainForm : Form
    {
        /// <summary>
        /// リモコンインターフェース
        /// </summary>
        private IRemoteController _remoteController = null;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainForm()
        {
            InitializeComponent();
        }

        /// <summary>
        /// フォームロード時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_Load(object sender, EventArgs e)
        {
            //初回はA社を選択
            rdoAManufacturer.Checked = true;
        }

        /// <summary>
        /// //メーカー選択変更時のイベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Manufacturer_RadioButton_CheckedChanged(object sender, EventArgs e)
        {
            //ラジオボタンがONになった場合のみ処理する
            RadioButton radioButton = sender as RadioButton;
            if (!radioButton.Checked)
            {
                return;
            }

            //メンバ変数に操作用インターフェースを格納
            _remoteController = this.CreateRemoteController(radioButton.Text, txtSendingContent);
        }

        /// <summary>
        /// メーカー名をもとに、コントローラインターフェースを返却
        /// </summary>
        /// <param name="manufacturerName"></param>
        /// <returns></returns>
        private IRemoteController CreateRemoteController(string manufacturerName, TextBox textBox)
        {
            //指定されたメーカーに対応したインスタンスを、インターフェースの形で返却
            if (manufacturerName == "A社")
            {
                return new AManufacturerController(textBox);
            }
            else if (manufacturerName == "B社")
            {
                return new BManufacturerController(textBox);
            }
            else if (manufacturerName == "C社")
            {
                return new CManufacturerController(textBox);
            }
            else
            {
                Debug.Assert(false);
                return null;
            }
        }

        /// <summary>
        /// 電源ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnPower_Click(object sender, EventArgs e)
        {
            _remoteController.PushPowerOnOff();
        }

        /// <summary>
        /// 「1」ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOne_Click(object sender, EventArgs e)
        {
            _remoteController.PushOne();
        }

        /// <summary>
        /// 「2」ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnTwo_Click(object sender, EventArgs e)
        {
            _remoteController.PushTwo();
        }

        /// <summary>
        /// 「3」ボタン押下時イベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnThree_Click(object sender, EventArgs e)
        {
            _remoteController.PushThree();
        }
    }
}

IRemoteController ソースコード

続いて、リモコンインターフェースのソースコードを示します。

どのメーカーのリモコンでも必要な関数の定義が並んでいます。

namespace RemoteControllerForOOP
{
    /// <summary>
    /// リモコンインターフェース
    /// </summary>
    public interface IRemoteController
    {
        /// <summary>
        /// 電源ボタン押下処理
        /// </summary>
        void PushPowerOnOff();

        /// <summary>
        /// 「1」ボタン押下処理
        /// </summary>
        void PushOne();

        /// <summary>
        /// 「2」ボタン押下処理
        /// </summary>
        void PushTwo();

        /// <summary>
        /// 「3」ボタン押下処理
        /// </summary>
        void PushThree();
    }
}

AManufacturerController ソースコード

A社 メーカーのコントローラクラスになります。

B社もC社も基本同じ構成になりますので、ここでは割愛します。

using System;
using System.Windows.Forms;

namespace RemoteControllerForOOP
{
    /// <summary>
    /// A社コントローラー
    /// </summary>
    public class AManufacturerController : IRemoteController
    {
        /// <summary>
        /// メーカー名
        /// </summary>
        private const string MANUFACTURER_NAME = "A社";

        /// <summary>
        /// 送信内容テキストボックス
        /// </summary>
        private TextBox _txtSendingContent = null;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="textBox">送信内容テキストボックス</param>
        public AManufacturerController(TextBox textBox)
        {
            _txtSendingContent = textBox;
        }

        public void PushPowerOnOff()
        {
            _txtSendingContent.Text = String.Format("{0}-{1}", MANUFACTURER_NAME, "電源ボタンON");
        }

        public void PushOne()
        {
            _txtSendingContent.Text = String.Format("{0}-{1}", MANUFACTURER_NAME, "「1」ボタンON");
        }

        public void PushTwo()
        {
            _txtSendingContent.Text = String.Format("{0}-{1}", MANUFACTURER_NAME, "「2」ボタンON");
        }

        public void PushThree()
        {
            _txtSendingContent.Text = String.Format("{0}-{1}", MANUFACTURER_NAME, "「3」ボタンON");
        }
    }
}

結局何がうれしいのか

オブジェクト指向を意識しない場合、以下の問題があると話ました。

オブジェクト指向を意識するとなにがうれしいのか一つ一つ解説します。

  • 4~9のボタンの実装やほかのボタン(番組表など)
  • 他のメーカーに対応する場合
  • 次のリリースでメーカーを削除する場合

4~9のボタンの実装やほかのボタン(番組表など)

各ボタンの分岐がMainFormから不要になりました。

それは、MainForm内の以下の処理で、メーカー名に応じたインスタンスを生成して、インターフェースの形で返却する処理が肝になります。この関数のおかげで、MainFormの各処理は、どのメーカーのリモコンなのかを意識せずに関数呼び出しを行えるためです。

     /// <summary>
        /// メーカー名をもとに、コントローラインターフェースを返却
        /// </summary>
        /// <param name="manufacturerName"></param>
        /// <returns></returns>
        private IRemoteController CreateRemoteController(string manufacturerName, TextBox textBox)
        {
            //指定されたメーカーに対応したインスタンスを、インターフェースの形で返却
            if (manufacturerName == "A社")
            {
                return new AManufacturerController(textBox);
            }
            else if (manufacturerName == "B社")
            {
                return new BManufacturerController(textBox);
            }
            else if (manufacturerName == "C社")
            {
                return new CManufacturerController(textBox);
            }
            else
            {
                Debug.Assert(false);
                return null;
            }
        }
この生成関数をさらに別クラス化することで、より疎結合にすることができます。
興味のある方は、デザインパターンのFactoryパターンを参考にしてください。

他のメーカーに対応する場合

オブジェクト指向を意識しない場合、各ボタン押下処理に、追加するメーカーの処理を追加する必要がありました。オブジェクト指向を意識して作る場合、以下の追加処理のみで、追加対応ができます。

  • 追加メーカーの、ManufacturerControllerクラスを作成
  • MainFormのCreateRemoteControllerへ追加メーカーの分岐を追加

メーカーを削除する場合

オブジェクト指向を意識しない場合、各ボタンの削除対象のメーカーの分岐を削除する必要がありました。

また、他のすでにあるメーカーの処理が同一関数に記載されているため、誤って触ってしまう可能性があり危険です。オブジェクト指向を意識して作る場合、以下の削除処理のみで、対応ができます。

  • 削除メーカーに対応した、ManufacturerControllerクラスの削除
  • MainFormのCreateRemoteControllerへ追加メーカーの分岐を削除

まとめ

このようにオブジェクト指向を意識することにより、

  • 追加が容易
  • 削除が容易
  • 変更時の影響範囲を縮小化

などのメリットがあることがわかりました。

オブジェクト指向の良い所はまだまだ伝えきれていませんが、より学習したい方はデザインパターン等の学習をされるとよいと思います。デザインパターンのオススメ書籍は以下で紹介していますのでご参考ください。

【プログラミング全般】新人プログラマに読んで欲しい書籍のまとめ
新人のプログラマに向けて是非読んでほしい書籍をまとめています。 また、新人のみならず、基礎スキルのないプログラマにも有効です。 プログラミングの現場では教育が行き届いてないことが多く、どうしても目先の作業のことだけに集中してしま...

 

また、今回紹介したソースコードは、GitHubに格納しておりますので、興味のある方はご参考ください。

GitHub - remix-yh/RemoteControllerForOOP
Contribute to remix-yh/RemoteControllerForOOP development by creating an account on GitHub.

コメント