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