C#の設計パターンとは?初心者向けに基本概念と種類を徹底解説
生徒
「C#の勉強を始めたのですが、設計パターンとかデザインパターンという言葉を耳にしました。これって一体何ですか」
先生
「設計パターンとは、プログラムを作るときによく遭遇する問題と、それを解決するための定番の仕組みのことですよ。これを知っておくと、きれいで修正しやすいプログラムが書けるようになります。」
生徒
「パソコンもあまり触ったことがないので難しそうですが、私でも理解できますか」
先生
「大丈夫です。身近な例えを使いながら、基本的なことから順番に説明していきますね。まずは全体像から一緒に見ていきましょう!」
1. 設計パターンとは何か
プログラミングの世界、特にC#(シーシャープ)という言語の開発現場では、設計パターンやデザインパターンという言葉がよく使われます。これは、家を建てるときの設計図のテンプレートのようなものです。
例えば、世界中の大工さんがそれぞれ独自のやり方で家を建てると、後から別の人が修理するときに、どこに何の柱があるのか分からなくて困ってしまいます。そこで、階段の配置やドアの付け方などに共通の決まり、つまり型を作っておくことで、誰が見ても分かりやすく、頑丈な家を建てることができるようになります。
プログラムも全く同じです。多くの先輩プログラマーたちが過去に経験した、プログラムが複雑になって動かなくなる、修正が大変になるといった失敗を繰り返さないために、こういう構造で作ると上手くいくよという解決策をまとめた知恵の結晶が設計パターンです。
初心者の方は、まずはプログラムを整理整頓するための片付けのルールのようなものだと考えてください。このルールを守ることで、パソコンの操作に不慣れな方でも、読みやすくバグの少ない優れたシステムを作ることができるようになります。
2. なぜ設計パターンが必要なのか
プログラムは、一度作ったら終わりではありません。スマートフォンのアプリや日常で使うシステムが何度もアップデートされるように、プログラムは常に作り替えられたり、新しい機能が追加されたりします。
もし、設計パターンを使わずに思いつきだけでプログラムを書き進めてしまうと、最初は動いていても、後から一部分を変更しただけで全体が動かなくなってしまうという問題が発生します。これをプログラムの専門用語でスパゲティコードと呼びます。スパゲティの麺のように、色々な処理が複雑に絡み合って、どこを解きほぐせばいいのか分からなくなってしまった状態のことです。
設計パターンを導入する最大の理由は、プログラムの変更しやすさ(保守性)を高めることにあります。それぞれの役割をしっかりと分けて独立させておくことで、一部を修正しても他の部分に悪い影響が出ないようになります。また、複数人で一緒に開発をするときにも、共通の設計パターンを使っていれば、他の人が書いたプログラムの意図をすぐに理解できるようになるという大きなメリットがあります。
3. オブジェクト指向の基本
C#の設計パターンを理解するためには、まずオブジェクト指向という考え方を知る必要があります。プログラミング未経験の方にとって、これが最初の壁に感じられるかもしれませんが、現実の世界に置き換えると非常に単純です。
オブジェクト指向とは、プログラムを役割ごとに分かれたモノ(オブジェクト)の集まりとして組み立てていく手法です。例えば、自動車を考えてみましょう。自動車は、エンジン、タイヤ、ハンドル、ブレーキといった様々な部品が組み合わさって動いています。
もし、これらが一体化していて分解できない構造だったら、タイヤがパンクしただけで車ごと買い替えなければいけなくなります。しかし、部品ごとに独立していれば、パンクしたタイヤだけを交換すれば済みます。プログラムもこれと同じで、機能ごとに部品化して作っていきます。この部品の設計図のことをC#ではクラスと呼び、その設計図から実体化されたモノのことをインスタンスと呼びます。設計パターンは、このクラスという部品をどのように組み合わせて配置すれば、一番効率よく綺麗なシステムになるかを教えてくれるガイドラインなのです。
4. 設計パターンの三大分類
設計パターンには、大きく分けて三つの種類があります。これらは世界的によく知られている二十三個のパターン(別名:GoFのデザインパターン)を、その目的ごとに分類したものです。
| 分類名 | 主な目的 | 分かりやすい例え |
|---|---|---|
| 生成に関するパターン | モノ(インスタンス)を作る仕組みを整える | 工場の製造ラインの自動化 |
| 構造に関するパターン | クラス同士の組み合わせや関係性を綺麗にする | コンセントの変換アダプター |
| 振る舞いに関するパターン | モノとモノとのやり取りや動きを整理する | 会社の業務命令や伝言ゲームのルール |
このように、目的によって使い分けることで、あらゆるプログラムの課題に対応できるようになっています。今回はこの中から、初心者の方でもイメージしやすい代表的な四つのパターンを、具体的なC#のプログラムコードを交えて詳しく解説していきます。
5. 生成に関するパターンの代表例
まずは、モノを作る仕組みを管理する生成に関するパターンから、最も有名でよく使われるシングルトンパターン(Singleton)をご紹介します。これは、あるクラスの部品(インスタンス)が、プログラム全体の中で絶対に一つしか存在しないことを保証する仕組みです。
例えば、パソコンのゲームの設定画面を思い浮かべてください。音量の設定や画面の明るさを管理する処理は、ゲームの中にいくつも存在しては困ります。あっちの設定画面では音量が大、こっちの画面では音量が小となってしまっては、ゲームが正しく動きません。常に一つの設定データを全員で共有する必要があります。このようなときに使われるのがシングルトンパターンです。
実際のC#のコードを見てみましょう。ここではゲームの構成データを管理する仕組みを再現しています。
using System;
public class GameConfig
{
private static GameConfig _instance;
private GameConfig()
{
Console.WriteLine("設定を読み込みました。これは一度しか実行されません。");
}
public static GameConfig GetInstance()
{
if (_instance == null)
{
_instance = new GameConfig();
}
return _instance;
}
public void ShowMessage()
{
Console.WriteLine("現在の音量設定は10です。");
}
}
class Program
{
static void Main()
{
GameConfig config1 = GameConfig.GetInstance();
config1.ShowMessage();
GameConfig config2 = GameConfig.GetInstance();
config2.ShowMessage();
}
}
このプログラムを実行すると、画面には次のように出力されます。
設定を読み込みました。これは一度しか実行されません。
現在の音量設定は10です。
現在の音量設定は10です。
二回データを呼び出していますが、最初の文言は一度しか表示されていません。これは、二回目に呼び出されたとき、すでに作られていた一つの部品を再利用しているためです。これにより、パソコンのメモリという記憶領域を無駄遣いせず、データの不整合を防ぐことができます。
6. 構造に関するパターンの代表例
次に、クラス同士を繋ぐ構造に関するパターンから、アダプターパターン(Adapter)を解説します。アダプターとは、旅行のときに海外のコンセントの形を日本のプラグに合わせるための変換器のことです。これと同じことをプログラミングで行います。
すでに完成している古いプログラムや、他の人が作ったプログラムを改造することなく、新しいシステムに繋ぎ込んで使いたいときにこのパターンが活躍します。古いプログラムを直接書き換えてしまうと、予期せぬ不具合が起きるリスクがありますが、間に変換用のクラス(アダプター)を挟むことで、安全に古い機能を再利用できます。
以下のサンプルコードでは、古いプリンターのシステムを、新しい印刷システムに適応させるためのアダプター構造を作っています。
using System;
public class OldPrinter
{
public void ExtPrint(string text)
{
Console.WriteLine("古いプリンターでの印刷: " + text);
}
}
public interface ITarget
{
void Print(string text);
}
public class PrinterAdapter : ITarget
{
private OldPrinter _oldPrinter = new OldPrinter();
public void Print(string text)
{
_oldPrinter.ExtPrint(text);
}
}
class Program
{
static void Main()
{
ITarget printer = new PrinterAdapter();
printer.Print("こんにちは、設計パターン。");
}
}
このコードの実行結果は以下の通りです。
古いプリンターでの印刷: こんにちは、設計パターン。
新しいシステムは、プリンターの中身が古いものであることを意識せずに、共通の手順で印刷命令を出すことができます。このように、異なる仕組みの仲介役として機能するのがアダプターパターンです。
7. 振る舞いに関するパターンの代表例その一
三つ目の分類である振る舞いに関するパターンから、まずはストラテジーパターン(Strategy)を紹介します。ストラテジーとは戦略という意味です。これは、状況に応じて処理のやり方(アルゴリズム)を、衣服を着替えるように簡単に切り替えられるようにするパターンです。
例えば、ネットショッピングの決済画面を考えてみましょう。支払い方法には、クレジットカード決済、コンビニ払い、スマートフォン決済など複数の種類があります。これらを一つのプログラムの中に詰め込んでしまうと、新しい支払い方法が増えるたびに、全体の大きなプログラムを書き直さなければならず、大変危険です。
そこで、決済方法という戦略を個別の部品として独立させ、選択されたものに差し替える仕組みを作ります。以下がその具体例です。
using System;
public interface IPaymentStrategy
{
void Pay(int amount);
}
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(int amount)
{
Console.WriteLine("クレジットカードで" + amount + "円支払いました。");
}
}
public class CashPayment : IPaymentStrategy
{
public void Pay(int amount)
{
Console.WriteLine("現金で" + amount + "円支払いました。");
}
}
public class ShoppingCart
{
private IPaymentStrategy _paymentStrategy;
public void SetPaymentMethod(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
public void Checkout(int amount)
{
_paymentStrategy.Pay(amount);
}
}
class Program
{
static void Main()
{
ShoppingCart cart = new ShoppingCart();
cart.SetPaymentMethod(new CreditCardPayment());
cart.Checkout(3000);
cart.SetPaymentMethod(new CashPayment());
cart.Checkout(1500);
}
}
このプログラムを実行すると、次のような結果になります。
クレジットカードで3000円支払いました。
現金で1500円支払いました。
買い物かごの仕組み自体は何も変えていないのに、セットする戦略を切り替えるだけで、支払い処理の挙動を自由に変えることができています。これで将来、新しい決済方法が追加されても、他のプログラムを汚すことなく安全に拡張が可能です。
8. 振る舞いに関するパターンの代表例その二
最後にもう一つ、振る舞いに関するパターンからオブザーバーパターン(Observer)をご紹介します。オブザーバーとは観察者という意味です。あるモノの状態が変わったときに、それを観察している他のモノたちへ自動的に通知を送る仕組みです。
身近な例でいうと、ユーチューブのチャンネル登録や、ニュースアプリの通知機能です。配信者が新しい動画を投稿すると、登録しているファン全員に一斉に通知が届きます。ファンが毎日、動画が上がっていないかと配信者のページを何度も確認しに行かなくても、配信者側から自動で知らせてくれるので効率的です。
プログラムでも、データの値が変わった瞬間に、画面の表示を最新の情報に自動で書き換えたいときなどにこの仕組みがよく使われます。以下のコードでその動きを確かめてみましょう。
using System;
using System.Collections.Generic;
public class NewsChannel
{
private List<Action<string>> _observers = new List<Action<string>>();
public void Register(Action<string> observer)
{
_observers.Add(observer);
}
public void Notify(string news)
{
foreach (var observer in _observers)
{
observer(news);
}
}
}
class Program
{
static void Main()
{
NewsChannel channel = new NewsChannel();
channel.Register(message => Console.WriteLine("ユーザーAが受信: " + message));
channel.Register(message => Console.WriteLine("ユーザーBが受信: " + message));
channel.Notify("緊急ニュースが配信されました!");
}
}
このプログラムを実行すると、以下の出力が得られます。
ユーザーAが受信: 緊急ニュースが配信されました!
ユーザーBが受信: 緊急ニュースが配信されました!
ニュースを発信しただけで、登録されている読者全員に漏れなくメッセージが届いていることが分かります。このように、部品同士が直接深く依存し合うことなく、情報の連携ができるようになるのがオブザーバーパターンの強みです。
9. 初心者が設計パターンを学ぶための心構え
ここまで代表的な設計パターンを見てきましたが、これらを一度にすべて暗記する必要はまったくありません。プログラミングを始めたばかりの段階で無理に設計パターンを使おうとすると、かえってプログラムが複雑になり、難しくなってしまうことがあります。これを過剰設計と呼びます。
大切なのは、まずは普通にプログラムを書いてみて、読みにくいな、修正が面倒だなと感じたときに、そういえばこれを解決するパターンがあったなと思い出すことです。実際に手がけたプログラムの壁にぶつかったときこそ、設計パターンの本当の便利さが実感できる絶好のタイミングです。
最初はコードを真似して書くだけでも十分に勉強になります。パソコンの操作やC#の基本文法に慣れていくのと並行して、パズルのピースを組み合わせるような感覚で、少しずつ設計パターンの楽しさに触れていってください。引き出しを多く持っておくことが、将来素晴らしいエンジニアになるための第一歩となります。