C#でオブジェクト指向をする方法

記事内に商品プロモーションを含む場合があります

はじめに

オブジェクト指向でプログラミングをするってどういう意味か分かりますか? 

オブジェクト指向言語であるC#を使ってプログラミングをしていても,オブジェクト指向的にプログラミングできているものと,そうでないものがあります。 

プログラミング初心者にはこの違いは分からないでしょうが,初心者でなくてもこの違いが分かっていない方って結構います。 

特に,.NETが誕生する2002年より前からVB6などでプログラミングをしていた人は,なかなかこの「オブジェクト指向」という言葉の意味や,実装方法が「分からない」「理解できない」「難しい」という声をよく聞きます。 

ここ10年くらいの間にプログラミングを始めた人は,最初からC#という言語が存在していたので,オブジェクト指向的にプログラミングができているか?というとそうでもありません。

 大学で数年間プログラミングを勉強して会社に入ってくる新人を教育していても,オブジェクト指向的にプログラミングをする意味を理解していないし,入社して5年ほど経つエンジニアでも理解できていない人は結構います。

 オブジェクト指向的にプログラミングするというのは抽象的な表現であり,ズバリこれがオブジェクト指向で書かれたプログラムだ!となかなか示し辛いというのもあり,また人それぞれで解釈も異なるため,プログラマーがそれぞれに思うプログラミングをしてしまっているというのも,現代のプログラマー市場では往々にしてまかり通っています。

 そもそもプログラマーという職業,今はどうか分かりませんが,まったくプログラミングできない人間でもプログラマーになれました。大学でプログラミングを勉強していなくても,プログラムを書いたことがなくてもプログラマーになれました。

 というのも,大昔はコンピューターなんて無い時代でしたが,その後に現れたコンピューターというのはドデカイ物で,銀行や企業の限られた人が使うものでした。

 ここ20年で一人に1台(もっと?)パソコンが与えられて仕事をするのが当たり前になり,それに伴い,社内や個人で使用するアプリケーションといわれるソフト開発の需要が恐ろしく伸びたのです。

 アメリカも日本もそうですが,パソコンが普及し,一気にプログラマーの需要が伸びたため,次第に未経験者であっても,まったくプログラミングの知識の無い人であっても,企業を雇うようになりました。大手企業は自社で人を雇い,育てていくのでまだいいですが,プログラマーを雇い,企業に派遣する会社は,ロクに教育もせず,プロのプログラマーの顔をして,現場に派遣します。そして一人50万程度の単価で働かせて,当人には20万程度を給料として支払うという会社がたくさん現れました。 

あなたが今,職場で目にしているわけの分からないプログラムコードは,こういった未熟な人間によって作られています。少し勉強して,代入とIF文を独学で学んだだけの人間でも,プロのプログラマーとして働き,保守性の悪い,いや何もかもが悪いダメダメプログラミングを量産しています。

 マーチンファウラーの有名な著書「リファクタリング」でも,最初にケントベックと出会ったときのエピソードの中で,ケントベックは「1週間かけてすべてのプログラムを全員でリファクタリングさせた」と語っていますが,アメリカおきていたことが,10年ほど遅れて日本でも同じことが起きているのだと感じます。  

オブジェクト指向プログラミングとそうで無いもの

未熟なプログラマーによって,世の中には大量のオブジェクト指向でないプログラムコードがあふれかえっています。そしてその一部は恥ずかしながら私の手によって作り出しました。オブジェクト指向で作られたプログラムは,保守性が高いため,プログラム修正や,改造が容易で,プログラムをいじったために,どこにどのような影響が出るのかが,分かりやすいものになっています。ある程度知識のある人間が読むと読みやすく,複数人でプログラム開発を行っても,重複コードが出にくく,再利用可能なプログラムコードになっています。 

オブジェクト指向で作られていないプログラムは,どこに何が書いているか分かり辛く,修正したものがどこに影響するのか分かり辛い。同じようなプログラムコードが量産され,新規に配属されたメンバーはどの関数を呼び出せば正しく動くのかが理解できません。少しの修正や改造をするために膨大な時間を要し,テストにも非常に時間がかかります。

「理論上大丈夫」と思える感覚がもてません。 

なぜオブジェクト指向で作らないのか?

なぜオブジェクト指向で作らないのかわかりますか?

なぜオブジェクト指向で作らないのか?理由は次の3つだと思います。 

  • オブジェクト指向を知らない
  • オブジェクト指向を知っているけど理解できない
  • 自分で編み出すことができない 

オブジェクト指向を知らない

オブジェクト指向を知らない場合は仕方がありません。当然オブジェクト指向プログラミングはできません。 

オブジェクト指向を知っているけど理解できない

オブジェクト指向を知っているけど理解できない人も結構います。特に前述したように,昔からプログラミングをやっている人に多い傾向と思われます。 

オブジェクト指向は自分で編み出すことができない

理由の3番目が「自分で編み出すことができない」です。

結局これに尽きると思います。とても頭の良い人が,クラスやインタフェースというものを作り出し,javaC#といったオブジェクト指向言語を作ってくれました。あとは好きなようにオブジェクト指向プログラミングをすればいいのですが,人間の思考回路上,上から順番に処理されるコードと,サブルーチン(共通関数)化にしか普通の人はたどり着くことができません。オブジェクト指向を勉強せずに,オブジェクト指向言語だけを与えられても,オブジェクト指向プログラミングはできるようにならないのです。

 クラス,継承,インタフェースというものの使い方を理解しても,どのように使うのが有効的なのか?というのは,「オブジェクト指向設計」「デザインパターン」「テスト駆動開発」「ドメイン駆動開発」というものを勉強しなければ決して身につくものでは無いのです。

 だから「代入」と「IF文」書けます!程度でプロのプログラマーとしてプログラミングをしていてはいけないのです。学校では教えてくれない,正しいプログラミングの方法を学ぶ必要があります。

ちなみに学ぶ順番は次のような順番が良いかと思います。

  • 最低限のコーディング知識(IF文など。継承やインタフェースは分からなくてもオブジェクト指向設計を学びながらでも習得は可能)
  • オブジェクト指向設計
  • デザインパターン
  • テスト駆動開発
  • ドメイン駆動開発

 それでは次章より,オブジェクト指向とは何たるかを解説していきます。 

本章のまとめ

オブジェクト指向言語はなんとなくプログラミングをしているだけでは決して身につかない。先人の知恵を拝借して学んでいくしか道はなし!

 「基礎文法」「オブジェクト指向」「デザインパターン」「テスト駆動」「ドメイン駆動」の順番で勉強しよう。

なぜオブジェクト指向プログラミングは難しいと感じるのか?

はじめてオブジェクト指向に触れたとき,私はさっぱり意味がわかりませんでした。

プログラマーの会社に入社し,上司はCOBOLしか経験がなく,私は一人でオブジェクト指向の本を読んで勉強しました。

最初に読んだ本はこれでした。 

オブジェクト指向でなぜつくるのか 第2

 この本を読むと,なぜオブジェクト指向で作るのか?というと,「それは簡単にプログラムを作りたいから」というような内容で始まります。さっぱり意味がわかりませんでした。「オブジェクト指向は難しい!」って頭の中で感じていましたし,VB6しかやったことのない先輩は,オブジェクト指向はとても難しいと言って,まともに私に教育できる人はいませんでしたから。

 今ではこの本の意味がよくわかりますよ。

オブジェクト指向で作るのは,簡単にプログラミングをしたいから。

確かにそのとおりです。

 取っ掛かりとしてこの本はよかったと思います。

またこの本はオブジェクト指向を説明するときの説明に現実社会のもので例えるのが,かえって初心者の理解を妨げているという解説があります。これも確かにそうだと思います。

 「車」がスーパークラスで「トラック」「スポーツカー」がサブクラスみたいな感じです。

理解している人からすればその例えで十分理解できますが,理解していない人からすると,それを聞いてどのようにプログラミングすればいいのかはさっぱりわかりません。

 その後私はいろいろな本を読み,いくつものプロジェクトを立ち上げ,製品を世に出した経験で,実践の中で,オブジェクト指向を理解しました。そして今は新人や若手にマンツーマンでコードレビューをして,何度も何度も同じようなことをコンコンといいながら教育していますが,その中で感じるのは,オブジェクト指向をはじめ,よい実装を教育するには,まず,よい実装を見せてあげないといけないと思います。

 先ほどの例えではなく,C#ならC#でどのように実装し,そうすることでどのような利点があるのか?を教えてあげないと,なかなか理解できるものではありません。 

オブジェクト指向プログラミングをする上で必要な3つの知識

オブジェクト指向プログラミングをするのに必要な知識はいろいろありますが,確実に必要な知識は次の3つです。 

  • カプセル化
  • インタフェース(ポリモーフィズム(多態性))
  • 継承 

オブジェクト指向本によく出てくる3つです。でもこの3つのうち特に重要なのは1と2です。比重をつけると次のような感じです。 

機能

重要度(100%の振り分け)

カプセル化

43%

インタフェース

47%

継承

10

 若干インタフェースのほうが重要度は高めですが,カプセル化とほぼ同じ。

一方継承はそんなに重要ではありません。

 オブジェクト指向が出たてのころは,みんな珍しくて使いたくて継承ばかりを使っていましたが,有名なGOFのデザインパターン本では「継承はあまり使うな!できるだけ,振る舞いをカプセル化しろ!」的なことをいっています。振る舞いをカプセル化というのは,デザインパターンのストラテジーパターンのようなやり方で,インタフェース越しに,実装は見えないように,意識しないように作って,さらにそれを取替え可能なものにしなさい!といっています。継承というパワフルな機能を多用することで,プログラムが複雑になってしまうことに気づいたGOFからのメッセージです。

 そんな理由より,継承の重要度は10%と低めにしています。

継承は局所的に,できるだけ抽象クラスの継承以外の継承は避けましょう。本当に必要なときのみ継承機能を使うことで,非常に効果的に実装ができます。意味がわからない方も心配しないでください。この後の解説で,順番に解説していきます。わかる方のみ,覚えておいてください。 

本章のまとめ

オブジェクト指向の基本は次の3つ

  • カプセル化
  • インタフェース
  • 継承

継承はできるだけ使わず,インタフェースを多用しよう。

オブジェクト指向設計のカプセル化ってどういう意味?

C#でオブジェクト指向プログラミングをする上で大切な3つのこと「カプセル化」「インタフェース」「継承」がありますが,今回はその中の「カプセル化」について解説します。

 カプセル化とはどんな意味かといいますと,値をクラスで包むことです。

例えばこんな感じです。

    public class Money
    {
        private readonly decimal _value;
 
        public Money(decimal value)
        {
            _value = value;
        }
 
        public decimal Value
        {
            get { return _value; }
        }
 
        public decimal TaxValue
        {
            get
            {
                return _value * 1.08m;
            }
        }
    }

ポイントは一番上の「private readonly decimal _value; 」の部分ですね。

このMoneyクラスは「_value」というdecimalの値をカプセル化していることになります。

 _value」はprivateに設定されているため,他のクラスから直接「_value」の値にアクセスことはできませんよね?だからこの「_value」を書き換えることも,読み込むことも,Moneyクラスにお願いしないと変更することができない。こういう状態を「_value」はMoneyクラスにカプセル化されている状態といいます。

カプセル化されていると何がいいのか?

このMoneyクラスのポイントは「_value」がMoneyクラス経由でしか扱えないことです。

Moneyクラスの「_value」はお金の金額を示します。

 公開されているプロパティの「Value」はそのままの金額「_value」の値が返却されますが,「TaxValue」を呼び出すと,消費税込みの値「1.08倍」された値が取得できます。

 例えばあなたが,お金を扱うシステムの開発を行っている場合,消費税を加えた値というのは,いたるところで扱うことになります。

 このMoneyクラスを作らずに,単にプログラム上に「decimal」を宣言にしてコーディングしていた場合,消費税込みの値として扱いたくなった場合はどうすればよいでしょうか?

画面プログラムなどに直接decimal変数を宣言する場合は,次のようなコーディングになりがちです。

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            var value = GetAmount();
            this.Text = (value * 1.08m).ToString();
        }
 
        private decimal GetAmount()
        {
            return 100m; //仮実装
        }
    }

 取得したGetAmount()decimal値を直接取得すると,その値に対して消費税を足そうとすると,直接1.08倍をするコーディングになる可能性があります。

 こうなってしまうと,プログラムのあらゆるところで,消費税の計算ロジックが書き込まれ,消費税か変化したときに,修正箇所が膨大となります。

 1.08倍する共通関を作ればいいのではないか?」と思われる方もいるかもしれません。その場合,あらゆるところを修正する必要はなくなるかも知れませんが,それだけでは問題は解決しません。

 仮に「TaxFunc」という共通関数を作ったとしましょう。

 すべての金額を使用している箇所で「TaxFunc」がちゃんと使われていると確認できるでしょうか?

 確かに「TaxFunc」のような関数を作れば1.08倍というマジックナンバーは共通化できます。しかし,「金額」という値を扱うときに,全プログラマーが共通意識を持って,どの程度その「TaxFunc」を使えているか?そして,金額を扱っている場所が何箇所あって,すべてが「TaxFunc」になっていると確認できるか?という問題があります。

 Money」クラスのよいところはずばりこの一言に尽きます

 値とロジックが1つになっている

 オブジェクト指向設計がうまくできない人はこれが理解できていません。

オブジェクト指向設計の大切な考え方「カプセル化」は「クラス」というものが存在している理由そのものです。「値」と「ロジック」を1つにまとめておく。これが,可読性の高いプログラムを作る秘訣です。

 今回の場合はdecimal型の「_value」という「値」と「TaxValue」という1.08倍するロジックがいったいとなった「Money」というクラスを作成しました。decimal型だけでは到底表現できなかったことをMoneyクラスは実現しています。

カプセル化するとどんなメリットがあるのか?

これまで解説してきたようにカプセル化することで,次のようなメリットが得られます。

 コードが読みやすくなる

  • 保守性が高まる
  • ロジックが散らばらない(ロジックの住処になる)
  • 値とロジックをセット考えることができる

カプセル化されていないと何がいけないのか?

一方カプセル化しなかったら何が悪いのか?decimal型を単体で持つことになるので,値とロジックを別々に持つことになります。値が生成されたときに,その値自体にロジックがあれば,プログラマーはどこも探すことなく,値に付随しているロジックを使用します。

C#でいえば「.ドット」をつけたときに出てくる「インテリセンス」の中から,関数を選ぶだけでよくなります。

 だから,例えばデータベースから「金額」を取得する場合は「decimal」型などの数値クラスで取得するのではなく「Money」クラスなどのカプセル化したクラスで返却しましょう。そうすれば,「金額」を扱っている場所が何箇所あるのか?税込み計算はどのようにしているのか?ということも「右クリック.すべての参照を検索」とすれば調べることができます。

本章のまとめ

値を扱うときはintなどの数値型で扱うのではなく「Money」クラスのようなカプセル化されたクラスを多用する。

 カプセル化のポイントは「値」と「ロジック」を1つのクラスで扱う。

オブジェクト指向設計のインタフェースとは?

オブジェクト指向設計で大切な3つのこと「カプセル化」「インタフェース」「継承」とありますが,その中でも一番大切なものインタフェースです。インタフェースの使い方は少しクセがあり,初心者の方には一番難しく感じるテーマかもしれませんが,慣れてしまえば,それほど難しい技術ではありません。ここでは,インタフェースの使い方がよくわからないという方向けに,実装方法と,使いどころを解説して行きます。

 オブジェクト指向設計の中に出てくる「インタフェース」とは,実装を強制する規格を定義するものです。

 家の中にある電気のコンセントも,ちゃんと規格があって,メーカーごとにばらばら何てことないですよね?

 メーカーごとにばらばらだと,家電ごとにさせるコンセントとさせないものが出てきます。

そうならないために,コンセントの規格があって,どんなメーカーの家電も,家のコンセントに挿せるようになっています。

 オブジェクト指向設計のインタフェースも同じです。

ある事柄に関する決め事を定義しておいて,その規格に定まっていない実装ができなくなるように強制する力のある実装方法なのです。

インタフェースの使い方 実装例1

次のような画面を実装することを想定してみましょう。

3つの会員区分(シルバー,ゴールド,プラチナ)があって,

計算ボタンを押下すると「数値入力エリア(1000となっている)」に一定の割引処理をして結果ラベルに表示するという仕様にしてみます。

 会員区分ごとの割引率は次のとおりとしておきます。

シルバー

0%

ゴールド

20

プラチナ

40%

インタフェースを使わない場合

インタフェースを使用せずに,画面プログラムに処理をベタ書きすると次のような感じになります。

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void ExecutionButton_Click(object sender, EventArgs e)
        {
            var value = Convert.ToInt32(InputTextBox.Text);
            if(SilverRadioButton.Checked)
            {
                this.ResultLabel.Text = value.ToString();
            }
            else if (GoldRadioButton.Checked)
            {
                this.ResultLabel.Text = (value * 0.8f).ToString();
            }
            else if (PlatinumRadioButton.Checked)
            {
                this.ResultLabel.Text = (value * 0.6f).ToString();
            }
        }
    }

これでもとりあえず問題なく動作します。

インタフェースを使った場合

インタフェースを使った例を次に示します。

    public interface IMember
    {
        float Rate { get; }
    }
 
    public sealed class SilverMember : IMember
    {
        public float Rate => 1.0f;
    }
 
    public sealed class GoldMember : IMember
    {
        public float Rate => 0.8f;
    }
 
    public sealed class PlatinumMember : IMember
    {
        public float Rate => 0.6f;
    }

 IMemberというインタフェースを作って,そこに掛け率である「Rate」プロパティを作りました。 これでIMemberインタフェースを実装するクラスは,必ず「Rate」プロパティを実装する必要があります。 「SilverMember」「GoldMember」「PlatinumMember」のそれぞれのクラスはIMemberを実装しているため,「Rate」プロパティを実装しており,それぞれの会員区分にあった割引率を返すようにしています。

    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }
 
        private void ExecutionButton_Click(object sender, EventArgs e)
        {
            var value = Convert.ToInt32(InputTextBox.Text);
 
            IMember member;
            if(SilverRadioButton.Checked)
            {
                member = new SilverMember();
            }
            else if (GoldRadioButton.Checked)
            {
                member = new GoldMember();
            }
            else
            {
                member = new PlatinumMember();
            }
 
            ResultLabel.Text = (value * member.Rate).ToString();
        }
    }

 画面側は,チェックボックスの状態で切り分けるのは,先ほどのインタフェースを使用しない例と同じですが,直接テキストに値を設定するのではなくIMember変数を宣言し,「SilverMember」「GoldMember」「PlatinumMember」のいずれかのクラスが生成されて「member」変数に格納されるようになっています。 最後の行の「ResultLabel.Text = (value * member.Rate).ToString();」では,「member」にどの会員区分が設定されたかはわかりませんが,必ずRateプロパティを持っていることが保障されているため,このようなコーディングをできるようになっています。 この段階で会員区分にあった割引率を取得し,計算結果をResultLabelに表示しています。

 こんな風に思う方もいるかも知れません「インタフェースを使った例の方がコード量が多くて読みづらいだけだと思います。最初の例の方がシンプルでいいんじゃないでしょうか?」

 確かにこの単純な例だとインタフェースを使用する旨みはないですね。オブジェクト指向設計はどれもそうですが,処理が複雑化するほど効果を発揮します。次の有効的な利用方法を参考にしてみてください。

有効的な利用方法

先ほどの例は単純すぎて,インタフェースを使う意味がわかりづらかったかも知れません。ただ,インタフェースの使い方(書き方)を解説するには,シンプルなほうがよいかと思いそうしました。今回は少しだけ処理を複雑にしてみましょう。

仕様

先ほどはひとつの画面に会員区分と計算処理がありましが,今回はそれを分割します。ログイン画面で「会員区分」を選択し,その後の画面で金額を入力し,「割引後の値段」を表示するようにしてみましょう。

    public static class LoginInfo
    {
        private static IMember _member;
 
        public static IMember Member
        {
            get
            {
                if(_member == null)
                {
                    return new SilverMember();
                }
 
                return _member;
            }
 
            set
            {
                _member = value;
            }
        }
    } 

LoginInfoクラスはStatic(静的)な値を保持するクラスとして,アプリケーションのどこからでも参照できるクラスとして,IMemberを保持します。

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void ExecutionButton_Click(object sender, EventArgs e)
        {
            if (SilverRadioButton.Checked)
            {
                LoginInfo.Member = new SilverMember();
            }
            else if (GoldRadioButton.Checked)
            {
                LoginInfo.Member = new GoldMember();
            }
            else
            {
                LoginInfo.Member = new PlatinumMember();
            }
 
            using (var f = new Form2())
            {
                f.ShowDialog();
            }
        }
    }

 ログイン画面は入力された会員区分をLoginInfoクラスのMemberに設定し,計算画面を表示します。

     public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }
 
        private void ExecutionButton_Click(object sender, EventArgs e)
        {
            var value = Convert.ToInt32(InputTextBox.Text);
            ResultLabel.Text = (value * LoginInfo.Member.Rate).ToString();
        }
    }

 計算画面はLoginInfoを参照し,ログインされている会員区分で計算処理をします。 どうでしょうか?こういった使い方なら,最初のログイン画面で会員区分が設定されて以降,すべての処理はその会員区分の情報で処理することができますから,あらゆる画面で,同じ思想で値引き処理が行われることになります。 

本章のまとめ

インタフェースはコンセントと同じで,ある決まった使い方をする種類ごとに定義して,実装を強制する。 同じような特性を持ち,それぞれに異なる処理をする場合はインタフェース越しに処理を行う。 よくあるのはデータベースとの接続部分だったりします。SQLServeOracleどちらにも対応したり,ダミーデータとの差し替えを可能にしたりする使い方はよくします。

そもそもオブジェクト指向設計の「継承」ってなに?

C#でオブジェクト指向設計する時に欠かせないものとして,「カプセル化」「インタフェース」「継承」という3つがあります。今回はその中でも「継承」に関して,そもそも継承とは何なのか?使うメリットとデメリット,使わなかったらどうなるのか?そして,実際の使用例を示し,効果的な実践的プログラミング方法まで解説して行きます。

 継承とは,オブジェクト指向設計の中でもメジャーな考え方ですが,そもそも継承とは何なのでしょうか?

 継承とは,「とあるクラスのフル機能が全部入ったクラスを作れる」機能のことを言います。

 例えば「クラスA」に計算ロジックが書かれている場合,「クラスB」が「クラスAを継承します!」といえば,自動的にクラスBにはクラスAのフル機能が手に入りますから,クラスBは計算ロジックを手に入れることができます。

オブジェクト指向設計に「継承」があったら何がいいの?

継承という機能があればどんなうれしい事があるでしょうか?

継承のメリット1:コード量の削減

継承を使うと,コーディング量を減らすことができます。

親となるクラスにロジックを入れておき,サブクラスからその関数を呼び出すことで,コードの重複を減らすことができます。

継承のメリット2:画面レイアウトの統一

継承はロジックだけでなく,コントロールやフォーム(画面)を丸ごと継承できます。

なので,アプリケーション全体を通して,1つ継承されるためのBaseFormクラスを作成し,すべての画面はそのBaseFormを継承することで,アプリケーション特有のレイアウトを全画面に表示することができます。

例えば次のような2つの画面があり,画面下部のステータスバーのテキストやプログレスバーをすべての画面で共通化したい場合は,ひとつのBaseFormにコントロールをレイアウトしておけば,すべての画面で同じレイアウトで使用できます。 

個々の画面でコントロールを設置すると,実装がそれだけ大変になるだけでなく,ちょっとした幅の違いや,レイアウトのずれが生じる可能性があります。Excelでもwordでもどんなソフトでもそうですが,アプリケーション共通のレイアウトというものがあり,画面ごとにボタンの場所が違ったり,プログレスバーの出かたが異なると,ソフトとしては,ちょっと不恰好なものになりますが,この「継承」という機能を使うことで解消されます。 

継承におけるデメリット

コード量が減って,見た目の統一感も出せるのであれば,継承はいいことばかりのように思いますが,そうでもありません。オブジェクト指向が開発されたころは,この継承という機能のパワーがものすごく印象的だったせいで,プログラマーはこぞって「継承」プログラミングを始めました。その結果何が起こったでしょうか? 

「複雑なプログラム」 

可読性の悪い複雑なプログラムが出来上がりました。

継承を覚えたてのころは,うれしくて,できるだけ継承するようなプログラミングになってしまいがちです。ただ継承が多用されているプログラムは非常に読みづらくなります。

 読みづらくなる原因のひとつは,「継承元クラスを100%理解しないと,サブクラスの実装ができない」ことにあります。 

サブクラスは継承することで,継承元クラスのフル機能が手に入ると最初に言いましたが,逆にそのせいで,継承元を100%理解しないと,サブクラスの修正や変更を加えることは難しく,そのせいで可読性はかなり落ちます。 

パワーのある機能だからこそ,継承という機能は限定的に,局所的に実装することで,可読性を保ちながら,継承のパワフルな恩恵にあずかることができます。 

デザインパターンで有名なGOFも著書の中で,継承はできるだけ使わずに,ストラテジーパターンのようにインタフェースを多用せよ!と忠告しています。 

オブジェクト指向における再利用のためのデザインパターン 

私の印象では継承されることが前提である「抽象クラス」と先の例で出した画面の既定クラスであるレイアウト統一のためのBaseFormと,各種コントロール(ボタンとかパネルとか)は,アプリケーションで統一した動作をさせるために標準コントロールを継承して,独人関数を追加することで共通部品を作成しますが,この3種類以外は極力継承を使わないようにしています。継承したくなったら,抽象クラスかインタフェースでの実装を試みて,どうしても必要な場合のみ「継承」することにしています。 

継承がなかったら何が悪いの?

継承しなかった場合のデメリットはそんなにはないかなと思います。先の例であるフォームやボタンやグリッドなどを継承し,「ButtonEx」などの拡張クラスを作っておくと,すべてのボタンで同じ動作をさせることができるので,そういった意味では,継承を使わないと,共通関数をすべてのボタンが呼び出すことになり,関数がばらばらに管理されるので,統一された実装をしづらくなります。 

C#での継承の使い方

継承の例を簡単に紹介します。今回は先の例で示したとおり,継承されるだけの画面「BaseForm」にアプリケーション全体で共通のコントロールをレイアウトし,他の画面はすべてBaseFormを継承する実装とします。 

BaseFormの実装

まずはBaseFormの実装です。

画面にStatusStripコントロールを置いて,その中に「テキスト」と「プログレスバー」を配置しています。このBaseFormをすべての画面で継承することで,アプリケーションは統一された画面レイアウトで表示することができます。 

    public partial class BaseForm : Form
    {
        public BaseForm()
        {
            InitializeComponent();
 
            this.StartPosition = FormStartPosition.CenterScreen;
 
            this.Load += (_, __) =>
            {
                this.Text = "顧客管理システム";
                this.toolStripStatusLabel1.Text = "";
                this.toolStripProgressBar1.Visible = false;
            };
        }
 
        protected void ProgressUp()
        {
            this.toolStripStatusLabel1.Text = "保存しています...";
            this.toolStripProgressBar1.Visible = true;
            this.toolStripProgressBar1.Value++;
        }
    }

まず「this.StartPosition = FormStartPosition.CenterScreen; 」で画面の中央表示を設定しています。これでサブフォームは中央表示のコーディングは不要になります。

 その後Loadイベントでは画面タイトルを「顧客管理システム」で統一しています。(画面ごとにタイトルを変える場合は当然このコーディングは不要です。例えで書いています)

そしてステータスバーの初期化をしています。

 ProgressUp関数はステータスバーに「保存しています」と表示し,プログレスバーを表示し,プログレスバーのカウントをアップしています。プログレスバーの値を上げたい場合は,各画面よりこの関数を呼び出します。

 ProgressUpのアクセスレベルがprotectedになっていることに注目してください。

protectedは自分自身と継承先からのみアクセスができるアクセスレベルです。安易にpublicにすると,どこからアクセスされているのかわからなくなりますが,protectedにすることで,継承しているクラスのみがアクセスしていることが保障されます。継承先だけに公開する関数はprotectedを使いましょう。

継承先クラスMenuForm

メニューにはListForm呼び出しのボタンと,プログレスバーをアップするボタンのみを配置しています。BaseFormを継承することで,画面下部のステータスバーは勝手に表示されて使えるようになります。

     public partial class MenuForm : BaseForm
    {
        public MenuForm()
        {
            InitializeComponent();
 
            ListFormShowButton.Click +=
                (_, __) =>
                {
                    using (var f = new ListForm())
                    {
                        f.ShowDialog();
                    }
                };
 
            ProgressUpButton.Click +=
               (_, __) =>
               {
                   base.ProgressUp();
               };
        }
    }

 ListFormShowButton.ClickではListFormを表示しています。ProgressUpButton.ClickではBaseFormProgressUpを呼び出しています。継承元という意味で,「base」をつけています。これは省略できますが,私はあったほうがわかりやすいと思うのでつけるようにしています。一行目で「class MenuForm : BaseForm」となっていることに注目してください。これでBaseFormを継承するという意味になります。ちなみにC#では継承できるのは1つだけです。(インタフェースは複数可能) 

継承先クラスListFormの実装

この画面もBaseFormを継承して,プログレスバーをカウントアップするボタンを1つ設置しています。 

    public partial class ListForm : BaseForm
    {
        public ListForm()
        {
            InitializeComponent();
 
            ProgressUpButton.Click +=
                (_, __) =>
                {
                    base.ProgressUp();
                };
        }
    }

 内容はMenuFormと同じです。 注目すべきは「BaseForm」を継承しているだけで,BaseFormのフル機能が手に入っていることです。 足りない部分のみを各サブクラスで実装するというイメージになります。 これでMenuFormListFormのステータスバーは同じレイアウトで同じ動作をすることが保障されているのです。 

継承されたくないとき

ちなみに,継承されたくないクラスを作ることができます。

    public sealed class Class1
    {
    }

classを宣言するときに「sealed」(シール)と書きます。そうすると,このクラスは継承できないクラスになります。継承されたくないクラスを作った場合はsealedと書きましょう。 でも「継承されたくないクラス」って何でしょうか? 基本的には「すべて」だと思います。画面やコントロール以外のクラスはほとんどが継承される前提で作っていないクラスになると思います。だからクラスを作るときは必ず「sealed」をつけましょう。 かの有名な「プログラミング.NETFramework」の中でも,C#のクラスのデフォルトが「sealed」でないのは失敗だったといっています。

 プログラミング.NET Framework 第4

 以上が継承の解説となります。

本章のまとめ

継承は継承元のフル機能を手に入れることができる強力な機能

継承すると,継承元の機能を100%理解していないと実装ができないため,可読性が悪い。

継承されるためのクラスである「抽象クラス」またはフォームやボタンなどのコントロールに対してのみ継承を使い,そのほかは限定的に,必要最小限に抑えよう。

基本的にクラスにはsealedをつけよう!

おわりに

最後までお読みいただきありがとうございました。プログラミングを勉強してきて,オブジェクト指向を理解した瞬間は今でも覚えていますが,全く異なる世界に入っていく感覚でした。文法だけを覚えてプログラミングをしていた自分が,恥ずかしくてたまらなくなりました。オブジェクト指向を深く理解するには「テスト駆動開発」や「ドメイン駆動開発」「リファクタリング」「デザインパターン」を学ぶのが近道となります。

ここまで読んでいただいたあなただけに、
Udemyという学習用プラットフォームで2600円で販売している
「C#でオブジェクト指向をする方法」とい有料コースを
無料でプレゼントしています
無料クーポンを受け取る
先着100名限定で、クーポン期限は2024/8/23までです。


無料クーポンを受け取る