ITの世界では,ときたま「ラッパー」とか「ラップする」とかいう言い方をすることがあります。新人さんに質問されたことがあるのでちょっと説明を書いてみます。1
概説
例えていうならば 「正露●糖衣A」 です。
お腹が痛くて薬が必要。でも苦いのはイヤ。そんなあなたのために優しく包んでくれているお砂糖の部分。それが 「ラッパー」 です。
ラッパーとは “包む” とか “くるむ” という意味の 「ラップ (Wrap)」 をする人ということです。
直接扱えないモジュールに対して I/F を変換して扱えるようにしたり,直接扱うよりも利便性を向上させたりするときに用います。別の要素を付与したり,必要な要素を省略したりすることもあります。
例えば,プログラム(C# や Java)から RDBMS にアクセスしようと思ったら ODBC や JDBC などのドライバが必要です。さらに,それらを扱いやすくするためのアダプター用のライブラリを利用するのが一般的でしょう。このとき,プログラム言語の世界から見るとDBの世界(への道のり)が “ラップ” されているとみることができます。
Java でいう「ラッパークラス」は,プリミティブ型(値型)を扱いやすくするためにクラス型(参照型)として “ラップ” しているものです。ジェネリックなどで Object型 由来のオブジェクトを要求される場合,プリミティブ型は直接扱えないため,ラッパークラス(例えば Integer
型)を使う必要があります。
デザインパターンでいうと,それぞれ目的や観点は違いますが,アダプター(Adapter),デコレーター(Decorator),メディエーター(Mediator)あたりがラッパーっぽいです。
モバイルで,ブラウザで表示できる Web ページをアプリ化するために,Web View で表示するだけのアプリにすることを “ラップする” といったりもします。2
わりと幅広く使われる言葉ではありますが
“中身は同じだけど,外見を変えたい”
ときに使うものだということです。
サンプルケース
ここからは,プログラミングにおける具体的なケースをいくつかご紹介します。
アクセスの集約
外部の(とくにサードパーティ製の)ライブラリを使う場合,該当オブジェクト(クラスや関数)へのアクセスを利用者がそれぞれで行ってしまうと,ライブラリの変更に弱く(変更箇所が多く)なってしまいます。
そこで,ライブラリをラップし,利用者はラッパーを経由してライブラリにアクセスするようにします。こうすることで,ライブラリを差し替えたり,ライブラリの I/F が変更された場合であっても,ラッパーの I/F を変更せずに済めば,変更箇所はラッパー内部だけに留めることができます。
この図でいうと,ライブラリの差し替えによる影響は★の部分になります。上の状態から下の状態に変えると影響範囲が限定できているのが分かります。
// 任意のライブラリ
namespace Sample.AnyLibrary
{
public class Index
{
// 任意の処理(これをラップしたい)
public OutParam Execute(InParam param) { /* ... */ }
}
public class InParam { /* ... */ }
public class OutParam { /* ... */ }
}
// ライブラリのアダプター(ラッパー)
namespace AnyLibraryAdapter
{
public class IndexAdapter
{
public Index Index { get; }
public Converter Converter { get; }
public IndexAdapter(Index index, Converter converter)
{
Index = index;
Converter = converter;
}
// 任意の処理のラッパー処理
public OutParam Execute(InParam param)
{
// パラメーターもラップする
var inParam = Converter.ConvertToInnerInParam(param);
// 対象の処理を実行する
var result = Index.Execute(inParam);
// 戻り値もラップする
return Converter.ConvertToOuterOutParam(result);
}
}
public class Converter
{
// ラッパー用の InParam から ライブラリ用の InParam へ変換する
public AnyLibrary.InParam ConvertToInnerInParam(InParam param) { /* ... */ }
// ライブラリ用の OutParam からラッパー用の OutParam へ変換する
public OutParam ConvertToOuterOutParam(AnyLibrary.OutParam param) { /* ... */ }
}
public class InParam { /* ... */ }
public class OutParam { /* ... */ }
}
注意する点としては,ライブラリがオリジナルの構造体を要求する場合,構造体もラップする必要があるということです。利用者がライブラリの構造体を直接使ってしまうと,それも変更対象となってしまいます。上記の例では,Converter
で内外の構造体を変換させています。
レイヤー制御のための委譲
循環参照の防止や責務の明確化のためにレイヤーを分けることがあります。モジュールの境界をまたぐとき,外部に公開してる I/F を配置するレイヤーは限定したいところです。また,同一モジュール内においても,レイヤーの飛越を禁止したい場合は,直下のレイヤーに委譲用の I/F を用意して,バケツリレーします。
この図では,Service 層の関数や Business Process 層の上部の関数が,下位層へのラッパーの役割を果たしています。
オブジェクトのカリー化
「多機能で煩雑な I/F をシンプルにしたい」
ある処理は引数がたくさんあって,それらによって振る舞いを色々と変えることができます。しかし,いつも使う値が決まっていて,いちいち毎回渡さずに済ませたいことがあります。いくつかの引数を省略して呼び出しできる別の I/F があると便利です。
Order
関数には(謎の)パラメーターがたくさんあります。
- リソースID,アクションID,タイムアウト,リトライ回数,並列化する数,割り込み可かどうか,ソースをロックするかどうか,通信種別,暗号化方式,暗号キー,etc...
実際に使うものもありますが,本来利用者が知る必要はないものも含まれているかもしれません。処理Aさんは,律儀にパラメーターを渡そうとして大変な目にあっています。そこで,本当に必要なパラメーター以外は省略してくれるラッパーさんの登場です。アプリケーション設定などから必要な情報を集め,それらを付与して Order
関数を呼び出してくれます。処理Bさんたちは,このラッパーを使うことで不要な情報の取得処理から解放され,本来の責務に集中することができます。
処理のトランザクション
ある処理の前後に任意の処理を差し込みたい場合があります。利用する側はラッパーに処理を投げ込み,ラッパー経由で処理を呼び出します。そうすることで,常に決まった処理を前後に挟むことができます。また,処理のパラメーターとして決まった値を渡すこともできます。
// コンテナー(ラッパー相当)
public class Container
{
// 対象処理をラップする処理
public U Execute<T, U>(Func<TransactionInfo, T, U> proc, T param)
{
// トランザクション情報を取得する
var info = GetTransactionInfo();
// トランザクションを開始する
StartTransaction(info);
try
{
// 対象処理を実行する
var result = proc(info, param);
// トランザクションをコミットする
CommitTransaction(info);
// 対象処理の戻り値を返す
return result;
}
catch (Exception)
{
// 例外が発生した場合
// トランザクションをロールバックする
RollbackTransaction(info);
// 発生した例外はそのままスローする
throw;
}
}
// Overload
public T Execute<T>(Func<TransactionInfo, T> proc) { /* ... */ }
public void Execute<T>(Action<TransactionInfo, T> proc, T param) { /* ... */ }
public void Execute<T>(Action<TransactionInfo> proc) { /* ... */ }
// Private Method
private TransactionInfo GetTransactionInfo() { /* ... */ }
private void StartTransaction(TransactionInfo info) { /* ... */ }
private void CommitTransaction(TransactionInfo info) { /* ... */ }
private void RollbackTransaction(TransactionInfo info) { /* ... */ }
}
// 何かしらトランザクションに関係する情報
public class TransactionInfo { /* ... */ }
// ラッパーを使う人
public class Client
{
public Container Container { get; }
public Client(Container container)
{
Container = container;
}
// メイン処理
public void MainProcess()
{
// 対象処理に必要なパラメーターを用意する
var param = new Param();
// ラッパー経由で処理を実行する
var result = Container.Execute(TargetProcess, param);
// 受け取った結果を使って何かする
// ...
}
// 対象処理 → ※ これをラップしたい
private Result TargetProcess(TransactionInfo info, Param param) { /* ... */ }
// 対象処理の引数と戻り値
private class Param { /* ... */ }
private class Result { /* ... */ }
}
処理を Container.Execute
経由で実行させることによって,トランザクション処理をラップしています。処理の実行前に StartTransaction
,処理の実行後に CommitTransaction
を呼び出します。処理が失敗(例外が発生)した場合は RollbackTransaction
を実行し,例外をスローします。処理に渡されるパラメーターには,TransactionInfo
が付与されます。
処理自身の引数や戻り値はジェネリックになっているので,任意の型を指定できます。また,オーバーロードすることで,引数あり/なし,戻り値あり/なし,にも対応できます。
ここまでくると,ちょっと純粋なラッパーとは違う気もしますが、、
ご参考になれば幸いです。