VB6開発者向け:C#で始める.NETプログラミング

第6回オブジェクト指向プログラミング(1)

はじめに

本連載では、実際にVB6で開発したソフトウェアをC#に置き換えながら基本的な技術を学んでいただきます。

前回は、第2回 VB6で作ったサンプルソフトでご紹介したVB6のコードを、可能な限り忠実にC#のコードに単純に移植した後に、要点をご説明していきました。 これによってC#でまったく同じ動作をするソフトウェアを作成することができました。 また、その内容についてもVB6のコードを見比べていくことで比較的容易に理解できたのではないかと思います。

しかし、見比べるという点についてを意識したため、前回ご紹介したコードは、およそC#らしさというものがありませんでした。

では、VB6のサンプルコードをC#に移植するのではなく、VB6のサンプルソフトと同様のものを最初からC#で開発した場合はどうなるのでしょうか?

これを理解していただくために、今回は.NETプログラミングの基本とも言えるオブジェクト指向プログラミングについてを2回に渡ってご説明していきたいと思います。

オブジェクト指向の基本

最初にオブジェクト指向の基本的なことをご説明します。

オブジェクトとしてイメージする

第2回 VB6で作ったサンプルソフト」の終盤でも述べた通り、今回サンプルとしているソフトウェアは、いくつかの独立性の高い処理に分けることができます。
  • ユーザーインターフェイス関連
  • 処理ロジック関連
  • データアクセス関連

また、データを入れておくためのオブジェクトも用意したほうがよさそうです。

では、ひとつのアプリケーションを、このようにオブジェクトとして切り分けることに一体何の意味があるのでしょうか?

この回答ですが、私としては「保守性」という言葉で表現できると思っています。

デバッグ・テスト・機能追加など、コードは繰り返し読まれることになるため、常にわかりやすく整理されている必要があります。

つまり、我々は、開発と同時に既存コードの保守も行っていることになります。

この保守性を向上させる方法として、オブジェクト指向プログラミングが有効であると考えることができます。

例えば、上記のように、ソフトウェアの処理をオブジェクトという単位で分類し、各オブジェクト同士の結合度を低くすることによって、コードの独立性が高まります。 独立性が高まれば、各処理の変更によるほかへの影響がわかりやすくなりますし、再利用性も高まります。

オブジェクト指向三大要素

オブジェクト指向の三大概念、三大機能などと表現されている場合もありますが、内容は同じです。 各内容の実例は、実際にコードを解説する際にご説明させていただきますので、ここでは概念だけを理解していただければ大丈夫です。

カプセル化(隠ぺい)

例えば、テレビというオブジェクトのインターフェイスは、チャンネル切り替えボタン・電源ボタン・音量ボタンがあります。そして、テレビの中にあるさまざまな仕組みは、利用者から見えない場所に置かれています。

クラスが公開するメンバも同様に、そのオブジェクトのインターフェイスとして捉えることができます。

適切なアクセス修飾子(private、publicなど)を各メンバに用いることで、わかりやすいインターフェイスを持ったオブジェクトを作ることができます。

継承

例えば、一定時間が経過したら自動的に電源が切れるテレビを作る時、最初から作るよりも、既存のテレビを元にして機能を拡張したり一部の機能を置き換えるほうが生産性も保守性も高いと考えられます。 これと同様に、既存のクラスを新しいクラスに継承することができます。 また、継承したクラスをさらに新しいクラスに継承できます。

継承する元となるクラスを、継承元クラス、スーパークラス、基本クラスなどと表現します。 継承したクラスを、継承クラス、サブクラス、派生クラスなどと表現します。

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

例えば、タイマー付きテレビや、ビデオ付きテレビなど、さまざまな種類のテレビというオブジェクトがあるとします。 しかし、単純に「テレビ」として定義されている機能はすべて共通です。

従って、⁠タイマー付きテレビのスイッチを入れる」のように何かの「振る舞い」をしたい場合、いろいろな種類のテレビを単に「テレビ」として表現できれば、⁠テレビのスイッチを入れる」という「振る舞い」を共通化できます。

このように「タイマー付きテレビ」「ビデオ付きテレビ」「テレビ」と表現することが、オブジェクト指向におけるポリモーフィズムになります。

C#のコードにおいても、ひとつのオブジェクトは多種多様な型に代入できます。

例えば、家電クラスを基本クラスとするテレビクラスがあり、テレビクラスを基本としたタイマー付きテレビクラスがあるとします。 タイマー付きテレビクラスから生成されたオブジェクトは、その基本クラスであるテレビ型や、家電型に代入できます。 逆に、家電型に格納されているタイマー付きテレビオブジェクトを、元のタイマー付きテレビ型に代入することもできます。

つまり、継承を繰り返したクラスから生成されたオブジェクトは、それらの基本クラスのすべての型に代入できます。

森羅万象がオブジェクトである

すべてのオブジェクトはObjectクラスから派生しています。

例えば、基本クラスが指定されていないクラスの場合、Objectクラスを継承したことになります。 つまりは、以下のように記述したことと同じです。

class MyClass : Object {}

このため、すべての変数はobject型に代入できます。

データアクセスするクラスを作る

最初にデータアクセスクラスを作成してみましょう。 前回作成したコードを元に簡単に作成できます。

ソリューションエクスプローラーのプロジェクト名で右クリック後、⁠追加⁠⁠→⁠クラス]によってAdapterという新しいクラスを追加することで、新しいAdapterクラス用のファイルが追加されるとともに、Adapterクラスのコードエディタが画面に表示されます。

Adapterクラスは、データベースの操作に関する処理のみを受け持ちます。 コードは、以下のようになります。

なお、データを格納するためのRowオブジェクトが既に含まれていますが、Rowクラスは後で作成します。

using System.Data;
using System.Data.OleDb; // 追加
namespace WindowsFormsApplication1
{
    //*************************************************************
    /// <summary>電話帳サンプルデータベース操作クラス</summary>
    //*************************************************************
    class Adapter
    {
        private const string FILENAME = "c:\\Common\\Database.mdb";

        //*************************************************************
        /// <summary>レコードを追加します</summary>
        /// <param name="code">番号</param>
        //*************************************************************
        public void Add(int code) {
            string sqlText = this.GetAddSqlText(code);
            this.ExecuteQuery(sqlText);
        }

        //*************************************************************
        /// <summary>レコードを更新します</summary>
        /// <param name="row">Row オブジェクト</param>
        //*************************************************************
        public void Update(Row row) {
            string sqlText = this.GetUpdateSqlText(
                row.Code, row.Name, row.TelephoneNumber);
            this.ExecuteQuery(sqlText);
        }

        //*************************************************************
        /// <summary>レコードを削除します</summary>
        /// <param name="code">番号</param>
        //*************************************************************
        public void Delete(int code) {
            string sqlText = this.GetDeleteSqlText(code);
            this.ExecuteQuery(sqlText);
        }

        //*************************************************************
        /// <summary>レコードを読み込みます</summary>
        /// <param name="code">番号</param>
        /// <param name="row">Row オブジェクト</param>
        //*************************************************************
        public bool Read(int code, Row row) {
            using (OleDbConnection cn = this.CreateDbConnection()) {
                using (OleDbCommand cd = new OleDbCommand()) {
                    cd.Connection = cn;
                    cd.CommandType = CommandType.Text;
                    cd.CommandText = this.GetSelectSqlText(code);
                    using (OleDbDataReader dr = cd.ExecuteReader()) {
                        if (!dr.Read()) return false;
                        row.Code = dr.GetInt32(0);
                        row.Name = dr.GetString(1);
                        row.TelephoneNumber = dr.GetString(2);
                        return true;
                    }
                }
            }
        }

        //*************************************************************
        /// <summary>各 SQL 文字列を取得します</summary>
        /// <param name="code">番号</param>
        /// <returns>SQL 文字列</returns>
        //*************************************************************
        private string GetSelectSqlText(int code) {
            string s = code.ToString();
            return "SELECT * FROM MyTable WHERE 番号 = " + s;
        }

        private string GetAddSqlText(int code) {
            return
                "INSERT INTO MyTable(番号, 名前, 電話番号) VALUES(" +
                code.ToString() + ", '' , '')";
        }

        private string GetUpdateSqlText(
            int code, string nameValue, string telephoneNumber)
        {
            return
                "UPDATE MyTable " +
                "Set " +
                    "名前 = '" + nameValue + "', " +
                    "電話番号 = '" + telephoneNumber + "' " +
                "WHERE 番号 = " + code.ToString();
        }

        private string GetDeleteSqlText(int code) {
            string s = code.ToString();
            return "DELETE * FROM MyTable WHERE 番号 = " + s;
        }

        //*************************************************************
        /// <summary>クエリーを実行します</summary>
        /// <param name="sqlText">実行したい SQL 文字列</param>
        /// <returns>結果 [True:成功 / False:失敗]</returns>
        //*************************************************************
        private void ExecuteQuery(string sqlText) {
            using (OleDbConnection cn = this.CreateDbConnection()){
                using (OleDbCommand cd = new OleDbCommand()) {
                    cd.Connection = cn;
                    cd.CommandType = CommandType.Text;
                    cd.CommandText = sqlText;
                    cd.ExecuteNonQuery();
                }
            }
        }

        //*************************************************************
        /// <summary>DB接続オブジェクトを取得します</summary>
        //*************************************************************
        private OleDbConnection CreateDbConnection() {
            OleDbConnection r = new OleDbConnection();
            r.ConnectionString = this.GetConnectionString();
            r.Open();
            return r;
        }

        //*************************************************************
        /// <summary>データベースへの接続文字列を取得します</summary>
        //*************************************************************
        private string GetConnectionString() {
            return
                "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" +
                FILENAME;
        }
    }
}

コードの表記

既に前出のコードから適用している通り、今後は次の部分に関して表記を変更します。

  • {}と改行は、見やすさに応じて変更しています
  • 自インスタンスのメンバには、thisキーワードを付加します

thisはVB6のMeというキーワードと似て非なるものです。

VB6のMeは、同じモジュールでアクセスできても、外部からアクセスできないメンバに対しては無効でした。 しかしthisは、thisと記述した場所からアクセスできるメンバに対して有効です。

thisの付加は義務ではありませんが、共通性(自身のインスタンスもほかと同様に表記できる)という理由から、thisの付加を私はお勧めします。

カプセル化

このクラスのアクセス修飾子は、private(隠ぺい)とpublic(公開)が使われています。

publicなメンバ

  • public void Add(int code)
  • public void Update(Row row)
  • public void Delete(int code)
  • public bool Read(int code, Row row)

privateなメンバ

  • private const string FILENAME = "c:\\Common\\Database.mdb";
  • private string GetSelectSqlText(int code)
  • private string GetAddSqlText(int code)
  • private string GetUpdateSqlText(int code, string nameValue, string telephoneNumber)
  • private string GetDeleteSqlText(int code)
  • private void ExecuteQuery(string sqlText)
  • private OleDbConnection CreateDbConnection()
  • private string GetConnectionString()

publicなメンバを見ると、このクラスから生成されたオブジェクトは、Add、Update、Delete、Readというインターフェイスを持った装置であると考えることができます。

コンストラクタの省略

このクラスには、コンストラクタ(生成子)が記述されていません。 しかし、コンストラクタがないということではありません。

コンストラクタが省略された時は、以下のコンストラクタが記述されていることと同じ扱いになります。

public Row() {}

usingステートメント

このクラスでは、エラーメッセージの表示を行いません。このため、今回はusingステートメントを使うことにします。

using (OleDbConnection cn = this.CreateDbConnection()){
    using (OleDbCommand cd = new OleDbCommand()) {
        cd.Connection = cn;
        cd.CommandType = CommandType.Text;
        cd.CommandText = sqlText;
        cd.ExecuteNonQuery();
    }
}

例えば、上記のコードはusingステートメントでOleDbConnectionオブジェクトが設定されています。 これによって、usingブロック内から抜ける際には必ずDisposeメソッドが実行されます。

上記のコードは、最初にcd.Dispose()、次にcn.Dispose()が実行されます。

データを格納するクラスを作る

次にデータを格納するクラスを作ってみましょう。 前項と同様の手順で、Rowという名前のクラスを作成します。 Rowクラスは以下のようになります。

namespace WindowsFormsApplication1
{
    //*************************************************************
    /// <summary>レコードの値を格納するためのクラスです</summary>
    //*************************************************************
    class Row : NotifyPropertyChanged
    {
        //*************************************************************
        /// <summary>コンストラクタ</summary>
        //*************************************************************
        public Row() {
            this.Clear();
        }

        //*************************************************************
        /// <summary>Code プロパティ</summary>
        //*************************************************************
        private int _Code;
        public int Code {
            get { return this._Code; }
            set {
                if (this._Code == value) return;
                this._Code = value;
                this.OnPropertyChanged("Code");
            }
        }

        //*************************************************************
        /// <summary>Name プロパティ</summary>
        //*************************************************************
        private string _Name;
        public string Name {
            get { return this._Name; }
            set {
                if (this._Name == value) return;
                this._Name = value;
                this.OnPropertyChanged("Name");
            }
        }
        //*************************************************************
        /// <summary>Code プロパティ</summary>
        //*************************************************************
        private string _TelephoneNumber;
        public string TelephoneNumber {
            get { return this._TelephoneNumber; }
            set {
                if (this._TelephoneNumber == value) return;
                this._TelephoneNumber = value;
                this.OnPropertyChanged("TelephoneNumber");
            }
        }

        //*************************************************************
        /// Cliear 
        //*************************************************************
        public void Clear() {
            this.Code = 0;
            this.Name = "";
            this.TelephoneNumber = "";
        }
    }
}

継承

このRowクラスでは、オブジェクト指向の三大要素のひとつである「継承」が使われています。

class Row : NotifyPropertyChanged

これによって、RowクラスはNotifyPropertyChangedの機能を継承していることになります。

なお、NotifyPropertyChangedクラスは次回作成する予定です。

プロパティの実装

このクラスにはCode(番号⁠⁠、Name(名前⁠⁠、TelephoneNumber(電話番号)の値を格納するためのプロパティが実装されています。 例えば、以下はCodeプロパティを最もシンプルに実装したコードです。

private int _Code;
public int Code {
    get { return this._Code; }
    set { this._Code = value; }
}

VB6では、フィールド変数は先頭にまとめて記述しなければなりませんでしたが、C#では、上記のように関連の深い部分の近くに意図的に記述できます。

Codeプロパティは、このフィールド変数に対して実際の値を格納しています。

OnPropertyChanged()

以下は、CodeプロパティのSetブロックのコードです。

set {
    if (this._Code == value) return;
    this._Code = value;
    base.OnPropertyChanged("Code");
}

値が変更された時に、フィールド変数に値を代入してbase.OnPropertyChangedメソッドにプロパティ名を与えて実行しています。 このbaseインスタンスはRowクラスで生成していませんし、外部から与えられている訳でもありません。 つまり、いきなり登場しています。

この理由はbaseという名前からも容易に想像できる通り、Rowクラスの基本クラスであるNotifyPropertyChangedオブジェクトのインスタンスです。 Rowクラスのコンストラクタが実行されたことによって生成されています。

このように、継承元のオブジェクトにアクセスしたい場合は、baseインスタンスを指定します。 ただしRowクラスはNotifyPropertyChangeクラスを継承していて、自身のインスタンスからもアクセスできるため、this.OnPropertyChanged()と記述しても同じです。

さて、このOnPropertyChangedメソッドですが、これを実行することでPropertyChangedイベントを発行しています。

つまり、Rowオブジェクトのに格納されているCode、Name、TelephoneNumberの各プロパティの値が変わったことを、必要に応じて通知する方法を提供しています。

次回の予定

今回は、オブジェクト指向プログラミングについて簡単にご説明させていただいた後に、実際に2つのクラスを作ってみました。

このように、取り扱うデータや各処理をオブジェクトという単位で分けて考えることがオブジェクト指向プログラミングでは大変重要です。

その他のクラスについては、次回ご説明させていただこうと思います。

おすすめ記事

記事・ニュース一覧