C#

【C#】Adapter Pattern(アダプターパターン)で書く3つのサンプルプログラム

アイキャッチ(c_sharp)

こんにちは!
なかむぅです。

ずっとコピペプログラマーだった私は

オブジェクト指向?なにそれ美味しいの?

って感じだったので、「オブジェクト指向」を学ぶために「Java言語で学ぶデザインパターン入門第3版」を購入しました。

そのデザインパターンの一つ「Adapter Pattern(アダプターパターン)」をC#で書いていこうと思います。


我が社で一緒に働きませんか?

私が働く会社では、一緒に働いてくれるエンジニアを募集しています♪

我が社のココがアツイ!!
  • 未経験、第二新卒歓迎
  • 学歴・転職回数・離職期間不問
  • 残業は月15時間以下で残業代は全額支給
  • 経験者は月給23万円以上
  • 年収100万以上UP↑↑↑も可能
  • 長く働いていても年収が全然上がらないから、一気に年収を上げたい
  • 残業をしたくてしているわけではないので、残業代はしっかりと出して欲しい
  • やりたいことに挑戦させてもらえないから、自分がしてみたいことに挑戦したい
  • 通勤時間が長すぎるから、できるだけ短くしたい
  • もっと勉強したいから、書籍代など負担してくれるところに入りたい
  • 現場に駆り出されてからは放置プレイで、相談できる人も先輩も居ないので、自分の状況をちゃんと理解してくれるところで働きたい

上記内容に1つでも当てはまる場合は、ぜひご検討ください。

詳細ページ


Adapter Pattern(アダプターパターン)

新しいPCを買いました。

そしてマウスを接続しようと思ったら、USB TypeCの差し込み口しかありません。

マウスのUSBはTypeAです。

仕方なくUSB TypeAからUSB TypeCに変換するアダプターを購入しました。

こうゆうやつです。

この変換アダプターがあれば、新しく購入したPCでもマウスが使えるようになります。
他にも

  • DisplayportからHDMIに変換するアダプター
  • microSDカードから通常のSDカードに変換するアダプター

などありますね。

このように、もとからあるものを変換して使えるようにするのが「Adapter Pattern」です。

元あるソースをラッピングして作るという意味で「Wrapperパターン」とも呼ばれています。

Adapter Patternはどんなときに使うのか

たとえば、新しく追加したい機能と似たような機能を持つ既存クラスがあるとします。

用途としては同じなのですが一部足りない機能があり、その足りない部分だけを追加すれば完成しそうです。

そんなときは既存のクラスをいじらずに、Adapter Patternを使って実装をします。

他にも

  • 新しいシステムでも古いシステムの一部を使いたいとき
  • 公開されているライブラリーに手を加えたいとき

などにAdapter Patternを使用して実装します。

Adapter Patternは2種類

Adapter Patternは

  • 既存クラスを継承する方法(Class Adapter)
  • 既存クラスを委譲する方法(Object Adapter)

の2種類があります。

以降のサンプルプログラムでは、継承と委譲の2パターンを紹介していきたいと思います。

指定の文字列を()または*で挟んで出力するプログラム

まずは要件を満たすために一部の機能を追加したい場合に実装するAdapter Patternです。

Bannerという既存クラスがあるとします。

Bannerクラスに要件を満たすための実装を追加したいです。

そこでBannerクラスのAdapterとしてPrintBannerクラスを追加します。

それに伴い、継承で使用する場合はIPrintインタフェース、委譲で使用する場合はPrintクラスがそれぞれ登場します。

Banner 「()」か「*」で文字列を挟んで出力する既存クラス
IPrint PrintBannerを継承で使うときのインタフェース
Print PrintBannerを委譲で使うときの抽象クラス
PrintBanner 新しく追加するクラス

Bannerクラス

Bannerクラスは指定の文字列を「()」または「*」で挟んで出力する既存クラスです。

ShowWithParenメソッドが「()」、ShowWithAsterメソッドが「*」で挟んで出力します。

Banner.cs

namespace AdapterPattern
{
	public class Banner
	{
		private string _str;

		public Banner(string str)
		{
			_str = str;
		}

		public void ShowWithParen()
		{
			Console.WriteLine("(" + _str + ")");
		}

		public void ShowWithAster()
		{
			Console.WriteLine("*" + _str + "*");
		}
	}
}

継承パターン

継承のパターンではBannerクラスを継承したPrintBannerクラスと、PrintBannerクラス用のインタフェースIPrintを用意します。

PrintBannerクラスはAdapterの役割を果たすクラスです。

IPrintインタフェースを実装し、指定の文字列を「()」で挟む場合は、文字列の字数分の「-」を上下に出力します。

指定の文字列を「*」で挟む場合は、文字列の字数分の「*」を上下に出力します。

これが今回追加したい機能です。

IPrintクラス

IPrint.cs

namespace AdapterPattern
{
	public interface IPrint
	{
		public void PrintWeak();
		public void PrintStrong();
	}
}

PrintBannerクラス

PrintBanner.cs

namespace AdapterPattern
{
	public class PrintBanner : Banner, IPrint
	{
		private int _length;
		public PrintBanner(string str) : base(str)
		{
			_length = str.Length;
		}

		public void PrintWeak()
		{
			LineUp("-");
			ShowWithParen();
			LineUp("-");
		}

		public void PrintStrong()
		{
			LineUp("*");
			ShowWithAster();
			LineUp("*");
		}

		private void LineUp(string chara)
		{
			var line = chara;
			for (int i = 0; i <= _length; i++)
			{
				line += chara;
			}
			Console.WriteLine(line);
		}
	}
}

委譲パターン

委譲のパターンではPrintBannerクラス用の抽象クラスPrintを用意し、Bannerクラスのインスタンスを生成して使用します。

役割と要件は継承パターンと同じです。

Printクラス

Print.cs

namespace AdapterPattern
{
	public abstract class Print
	{
		public abstract void PrintWeak();
		public abstract void PrintStrong();
	}
}

PrintBannerクラス

PrintBanner.cs

namespace AdapterPattern
{
	public class PrintBanner : Print
	{
		private int _length;
		private Banner _banner;

		public PrintBanner(string str)
		{
			_banner = new Banner(str);
			_length = str.Length;
		}

		public override void PrintWeak()
		{
			LineUp("-");
			_banner.ShowWithParen();
			LineUp("-");
		}

		public override void PrintStrong()
		{
			LineUp("*");
			_banner.ShowWithAster();
			LineUp("*");
		}

		private void LineUp(string chara)
		{
			var line = chara;
			for (int i = 0; i <= _length; i++)
			{
				line += chara;
			}
			Console.WriteLine(line);
		}
	}
}

使い方

インスタンス生成時に出力したい文字列を指定して渡しておきます。

インスタンスを生成したら、それぞれのメソッドを呼び出しましょう。

Program.cs

namespace AdapterPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine("Bannerクラス(既存クラス)");
			var banner = new Banner("Hello");
			banner.ShowWithParen();
			banner.ShowWithAster();
			Console.WriteLine();

			Console.WriteLine("PrintBannerクラス(新規クラス)");
			var p = new PrintBanner("Hello");
			p.PrintWeak();
			p.PrintStrong();
		}
	}
}

コードを実行するとBannerクラスは「()」と「*」のみ、PrintBannerクラスはBannerクラスで出力した結果のそれぞれの上下に「-」と「*」が並べられています。

結果

Bannerクラス(既存クラス)
(Hello)
*Hello*

PrintBannerクラス(新規クラス)
-------
(Hello)
-------
*******
*Hello*
*******

パソコン周辺機器を接続するプログラム

新しいシステムでも古いシステムの一部を使いたいときのAdapter Patternです。

まずUSBの接続部分がTypeAとTypeCのインタフェースがあります。

IUsbTypeA 接続部分がUSB TypeAのインタフェース
IUsbTypeC 接続部分がUSB TypeCのインタフェース

そして、上記2つのインタフェースはパソコン周辺機器のクラスが実装します。

1つは接続部分がTypeAのマウス。

その他は接続部分がTypeCのパソコン周辺機器です。

Mouse マウスを表したクラス
Keyboard キーボードを表したクラス
Mic マイクを表したクラス
MouseToTypeC TypeCに変換したマウスを表したクラス

最後に新しく購入したPC、NewPCクラスがあります。

USBの差し込み口はTypeCしかありません。

NewPC 購入した新しいPCを表したクラス

IUsbTypeAインタフェース

IUsbTypeAインタフェースは接続部分がUSB TypeAのパソコン周辺機器が実装するインタフェースです。

IUsbTypeA.cs

namespace AdapterPattern
{
	public interface IUsbTypeA
	{
		public void PlugIn();
	}
}

Mouseクラス

MouseクラスはIUsbTypeAインタフェースを実装したクラスです。

接続部分がUSB TypeAのマウスを表しています。

マウスが接続されたことを出力するPlugInメソッドを実装しています。

Mouse.cs

namespace AdapterPattern
{
	public class Mouse : IUsbTypeA
	{
		public Mouse()
		{
		}

		public void PlugIn()
		{
			Console.WriteLine("マウスを接続しました");
		}
	}
}

IUsbTypeCインタフェース

IUsbTypeCインタフェースは接続部分がUSB TypeCのパソコン周辺機器が実装するインタフェースです。

IUsbTypeC.cs

namespace AdapterPattern
{
	public interface IUsbTypeC
	{
		public void Connection();
	}
}

Keyboardクラス

KeyboardクラスはIUsbTypeCインタフェースを実装したクラスです。

接続部分がUSB TypeCのキーボードを表しています。

キーボードが接続されたことを出力するConnectionメソッドを実装しています。

Keyboard.cs

namespace AdapterPattern
{
	public class Keyboard : IUsbTypeC
	{
		public Keyboard()
		{
		}

		public void Connection()
		{
			Console.WriteLine("キーボードを接続しました");
		}
	}
}

Micクラス

MicクラスはIUsbTypeCインタフェースを実装したクラスです。

接続部分がUSB TypeCのマイクを表しています。

キーボードと同様にマイクが接続されたことを出力するConnectionメソッドを実装しています。

Mic.cs

namespace AdapterPattern
{
	public class Mic : IUsbTypeC
	{
		public Mic()
		{
		}

		public void Connection()
		{
			Console.WriteLine("キーボードを接続しました");
		}
	}
}

MouseToTypeCクラス

MouseToTypeCクラスはMouseクラスのアダプターであり、IUsbTypeCインタフェースを実装したクラスです。

接続部分がUSB TypeAのマウスをTypeCに変換したクラスを表しています。

マウスが接続されるとConnectionメソッドが呼び出され、MouseクラスのPlugInメソッドが呼ばれます。

継承パターン

MouseToTypeC.cs

namespace AdapterPattern
{
	public class MouseToTypeC : Mouse, IUsbTypeC
	{
		public MouseToTypeC()
		{
		}

		public void Connection()
		{
			PlugIn();
		}
	}
}

委譲パターン

MouseToTypeC.cs

namespace AdapterPattern
{
	public class MouseToTypeC : IUsbTypeC
	{
		private Mouse _mouse;
		public MouseToTypeC()
		{
			_mouse = new Mouse();
		}

		public void Connection()
		{
			_mouse.PlugIn();
		}
	}
}

NewPCクラス

NewPCクラスは新しく購入したPCのクラスです。

USBの差し込み口がTypeCしかないため、IUsbTypeCインタフェースを実装したクラスしか接続することができません。

NewPCクラスにパソコン周辺機器が接続されたら、接続された周辺機器を出力するConnectionメソッドを呼び出します。

NewPC.cs

namespace AdapterPattern
{
	public class NewPC
	{
		public NewPC()
		{
		}

		public void Into(IUsbTypeC typeC)
		{
			typeC.Connection();
		}
	}
}

使い方

NewPCクラスのインスタンスを生成し、NewPCクラスの差し込み口Intoメソッドに、それぞれのパソコン周辺機器を接続します。

NewPCクラスのIntoメソッドはIUsbTypeCインタフェースしか受け付けることがきないので、IUsbTypeAのMouseクラスを接続することができません。

そこで、MouseクラスをAdapter Patternで実装したMouseToTypeCを利用することでNewPCクラスの差し込み口にも接続することができます。

Program.cs

namespace AdapterPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			var pc = new NewPC();
			pc.Into(new Keyboard());
			pc.Into(new Mic());
			// pc.Into(new Mouse()); // 実装しているインタフェースが違うためエラー
			pc.Into(new MouseToTypeC());
		}
	}
}
結果

キーボードを接続しました
マイクを接続しました
マウスを接続しました

パソコン周辺機器が全てTypeAだったら

前回のサンプルコードではマウスのみUSB TypeAでしたが、現実世界では大体がUSB TypeAの差し込み口です。

キーボードもマイクも全てUSB TypeAだった場合は、USB TypeAのインタフェースを委譲したAdapterクラスを作りましょう。

登場するクラスは前回紹介したサンプルコードのMouseToTypeCクラス以外は同じです。

その他の違いはKeyboardクラスとMicクラスがIUsbTypeAインタフェースを実装しているところです。

Keyboard.cs

public class Keyboard : IUsbTypeA // 実装するインタフェースをIUsbTypeAに変更
Mic.cs

public class Mic : IUsbTypeA // 実装するインタフェースをIUsbTypeAに変更
IUsbTypeA 接続部分がUSB TypeAのインタフェース
IUsbTypeC 接続部分がUSB TypeCのインタフェース
Mouse マウスを表したクラス
Keyboard キーボードを表したクラス
Mic マイクを表したクラス
ToTypeC TypeCに変換するクラス
NewPC 購入した新しいPCを表したクラス

ToTypeCクラス

ToTypeCクラスはIUsbTypeCインタフェースを実装したAdapterクラスです。

IUsbTypeAを委譲して、コンストラクタで渡ってきたクラスのPlugInメソッドを呼び出します。

ToTypeC.cs

namespace AdapterPattern
{
	public class ToTypeC : IUsbTypeC
	{
		private IUsbTypeA _usbTypeA;
		public ToTypeC(IUsbTypeA usbTypeA)
		{
			_usbTypeA = usbTypeA;
		}

		public void Connection()
		{
			_usbTypeA.PlugIn();
		}
	}
}

使い方

AdapterクラスであるToTypeCクラスに各IUsbTypeAを実装したクラスのインスタンスを渡し、NewPCクラスに渡すだけです。

共通のUSB TypeCに変換するアダプターを経由することで、USB TypeCの差し込み口しかないPCにもUSB TypeAのパソコン周辺機器を差し込めるようになります。

結果は一緒なので割愛します。

Program.cs

namespace AdapterPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			var pc = new NewPC();
			pc.Into(new ToTypeC(new Keyboard()));
			pc.Into(new ToTypeC(new Mic()));
			pc.Into(new ToTypeC(new Mouse()));
		}
	}
}

当記事の他にも

旧システム 二次元配列でデータを扱っている
新システム Entityでデータを扱っている

といったシステムで、旧システムから新システムを利用するサンプルプログラムも紹介しているサイトもあるので、参考にしてみてください。

なぜAdapter Patternを使うのか

もし仮に、すでに十分テストされている既存のクラスを修正するとします。

すると、修正した部品を使用している影響箇所を全て再テストしないといけなくなります。

そうなるとめちゃくちゃめんどくさいですよね。

そこで、Adapter Patternを利用すれば、新しいクラスで実装した部分のみテストすれば品質が保証されますし、バグが出ても実装した部分を調べればいいので見つけやすくなるのです。

継承と委譲、どっちがいいの?

基本的には委譲を使用する方がよいでしょう。

理由としては2つあります。

  • 二重継承ができない
  • 責務を気にしなくても良い

二重継承ができない

インタフェースではなく、抽象クラスで実装したい場合、抽象クラスと既存のクラスを二重で継承することができません。

そのため、抽象クラスを使いたい場合は委譲で実装する必要があります。

責務を気にしなくても良い

継承だとスーパークラスの内部的な振る舞いを理解していないと、効果的に使うのが難しくなってしまいます。

新しく作りたいクラスには必要のない機能が使えてしまうようになることで、必要のない責務が発生したりしてしまい、他者に自分の思惑とは違う使い方をされてしまうことにもなりかねません。

そういったことが起こらないように委譲という方法で相手に任せてしまうことで、新しいクラスが自分の責務に集中することができます。

まとめ

変換アダプターといえばUSBがパッと思いついたので、「Java言語で学ぶデザインパターン入門第3版」で紹介されているコード以外にも調子に乗って書いてみました。
(たぶん合ってるはず)

今後、使えそうな場面が出てきたら使っていきたいと思います♪

いやここは違うだろ

と思った方はコメントいただけると嬉しいです。

最後までお読みいただきありがとうございました!


我が社で一緒に働きませんか?

私が働く会社では、一緒に働いてくれるエンジニアを募集しています♪

我が社のココがアツイ!!
  • 未経験、第二新卒歓迎
  • 学歴・転職回数・離職期間不問
  • 残業は月15時間以下で残業代は全額支給
  • 経験者は月給23万円以上
  • 年収100万以上UP↑↑↑も可能
  • 長く働いていても年収が全然上がらないから、一気に年収を上げたい
  • 残業をしたくてしているわけではないので、残業代はしっかりと出して欲しい
  • やりたいことに挑戦させてもらえないから、自分がしてみたいことに挑戦したい
  • 通勤時間が長すぎるから、できるだけ短くしたい
  • もっと勉強したいから、書籍代など負担してくれるところに入りたい
  • 現場に駆り出されてからは放置プレイで、相談できる人も先輩も居ないので、自分の状況をちゃんと理解してくれるところで働きたい

上記内容に1つでも当てはまる場合は、ぜひご検討ください。

詳細ページ


COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です