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ではBaseFormのProgressUpを呼び出しています。継承元という意味で,「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のフル機能が手に入っていることです。 足りない部分のみを各サブクラスで実装するというイメージになります。 これでMenuFormとListFormのステータスバーは同じレイアウトで同じ動作をすることが保障されているのです。
継承されたくないとき
ちなみに,継承されたくないクラスを作ることができます。
public sealed class Class1 { }
classを宣言するときに「sealed」(シール)と書きます。そうすると,このクラスは継承できないクラスになります。継承されたくないクラスを作った場合はsealedと書きましょう。 でも「継承されたくないクラス」って何でしょうか? 基本的には「すべて」だと思います。画面やコントロール以外のクラスはほとんどが継承される前提で作っていないクラスになると思います。だからクラスを作るときは必ず「sealed」をつけましょう。 かの有名な「プログラミング.NETFramework」の中でも,C#のクラスのデフォルトが「sealed」でないのは失敗だったといっています。 プログラミング.NET Framework 第4版
以上が継承の解説となります。
まとめ
継承は継承元のフル機能を手に入れることができる強力な機能
継承すると,継承元の機能を100%理解していないと実装ができないため,可読性が悪い。
継承されるためのクラスである「抽象クラス」またはフォームやボタンなどのコントロールに対してのみ継承を使い,そのほかは限定的に,必要最小限に抑えよう。
基本的にクラスにはsealedをつけよう!