こんにちは。エンジニアの今井です。
システム開発中、Factoryパターンを適用することがあると思いますが、せっかく用意したFactoryを経由せずにクラスを生成されるのを制限したいことってありますよね?
C#のAttributeの学習もかねて、上記の制限をAttributeである程度実現してみました。
ちなみに、利用している.NETのVerは.NET5です。
1. 作ったもの
今回の目的のため、用意したAttributeクラスは以下になります。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class UseFactoryAttribute : System.Attribute
{
private readonly string _factoryClassName;
public UseFactoryAttribute(string factoryClassName)
{
_factoryClassName = factoryClassName;
}
[Conditional("DEBUG")]
public static void ValidateCallClass<T>(T callObj, int stackFrameNumber = 2) where T : class
{
// ①UseFactoryAttributeによるAttributeがあるかどうか調べる
var type = callObj.GetType();
var useFactories =
(UseFactoryAttribute[])System.Attribute.GetCustomAttributes(type, typeof(UseFactoryAttribute));
if (useFactories.Length == 0) return;
// ②呼び出し対象のクラス名の取得
var callClassName = new StackFrame(stackFrameNumber).GetMethod().ReflectedType.Name;
// ③指定されたクラス名の比較
var useFactoryNames = useFactories.Select(uf => uf._factoryClassName);
foreach (var useFactory in useFactoryNames)
{
if (useFactory == callClassName) return;
}
// ④呼び出し対象のクラスがヒットしなかったらエラー
var error = callClassName + "は" + string.Join(',', useFactoryNames) + "クラスでのみコンストラクタが許可されています。";
throw new Exception(error);
}
}
上記のクラスは以下のように配置することで、Attributeで指定したクラスからのみコンストラクタを許可します。
[UseFactory("HogeFactory")]
public class hoge{
public hoge(){
// HogeFactoryからコンストラクタを呼ばれないと、例外が発生する
UseFactoryAttribute.ValidateCallClass(this);
}
}
2. 解説
UseFactoryAttributeクラス自体はValidateCallClassがなければオーソドックスなAttributeクラスですね
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class UseFactoryAttribute : System.Attribute
{
private readonly string _factoryClassName;
public UseFactoryAttribute(string factoryClassName)
{
_factoryClassName = factoryClassName;
}
}
続いてValidateCallClassメソッドについて解説します。
ValidateCallClassメソッドは以下の四段階で構成されています。
①UseFactoryAttributeの設定値の取得
②メソッド呼び出しもと取得
③UseFactoryAttributeで指定した名前との比較
④比較してヒットしなかった場合のエラー処理
②で利用しているStackFrameが見慣れないものかもしれませんが、これはメソッド等の呼び出し履歴を扱うものと考えてもらえばいいです。デフォルト引数でstackFrameNumber変数に2を与えている理由は「ValidateCallClassメソッド→コンストラクタ→Factoryクラスのメソッド」と、メソッドの呼び出し階層が2だからです。
[Conditional("DEBUG")]
public static void ValidateCallClass<T>(T callObj, int stackFrameNumber = 2) where T : class
{
// ①UseFactoryAttributeによるAttributeがあるかどうか調べる
var type = callObj.GetType();
var useFactories =
(UseFactoryAttribute[])System.Attribute.GetCustomAttributes(type, typeof(UseFactoryAttribute));
if (useFactories.Length == 0) return;
// ②呼び出し対象のクラス名の取得
var callClassName = new StackFrame(stackFrameNumber).GetMethod().ReflectedType.Name;
// ③指定されたクラス名の比較
var useFactoryNames = useFactories.Select(uf => uf._factoryClassName);
foreach (var useFactory in useFactoryNames)
{
if (useFactory == callClassName) return;
}
// ④呼び出し対象のクラスがヒットしなかったらエラー
var error = callClassName + "は" + string.Join(',', useFactoryNames) + "クラスでのみコンストラクタが許可されています。";
throw new Exception(error);
}
利用しているクラス側はあまり書くこともないので解説を割愛します。
おわりに
今回はFactoryクラスのみを対象に考えていましたが、ちょっと書き方を変えるだけで特定メソッドやクラス、namespaceの制限ができるので応用が利きそうですね。
今回は学習用だったためちゃんと作りこんでいませんでしたが、実際の開発に導入するにはStackFrameの階層の値を渡しているところや、利用者側でthisを渡しているところをなくさないといけないと思いました。
以上です。これを読んだ人のお役に立てたら幸いです。
コメント