6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[C#の基礎] インスタンスの動的生成

Posted at

準備

生成対象のクラス定義(namespace は都合により ConsoleApp2 )

namespace ConsoleApp2
{
    public interface IFoo
    {
        int Id { get; set; }
        string Name { get; set; }

        void DoSomething();
    }
    public class Foo : IFoo
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public void DoSomething()
        {
            Console.WriteLine($"{nameof(Foo)} do something" );
        }
    }
}

パターン1 リフレクション

LINQ 登場以前に一般的だった手法。LINQ(というか Linq.Expression)を自分のものにするのはそこそこ手間がかかるので未だに現役だったりする。

基本的には 対象の型をロードして、コンストラクタ探して作る。
System.Activator は型が分かっていればコンストラクタ呼出しを代行する。

        static void Pattern1()
        {
            var type = Type.GetType("ConsoleApp2.Foo");
            var instance = Activator.CreateInstance(type) as IFoo;
            instance.DoSomething();
        }

受け取ったインスタンスを IFoo にキャストしている。これにより欲しい処理が呼び出せる。

Type.GetType() 呼出しで文字列を使っているので、同じインターフェースを実装した別のクラスに置き換えてもそれなりの動作をする。

パターン2 Linq.Expression

沼にようこそ。

リフレクションのパターンと同様に型の取得やコンストラクタの取得を実行するのは同じだが、決定的に違うのは最終的に元のコンストラクタ呼出しと同じものが出来上がる事。

        static void Pattern2()
        {
            var type = Type.GetType("ConsoleApp2.Foo");
            var constructor = type.GetConstructor(Type.EmptyTypes);
            var expression = Expression.Lambda<Func<IFoo>>(Expression.New(constructor)).Compile();
            var instance = expression();
            instance.DoSomething();
        }

expression() の部分がコンストラクタ呼出し。この呼び出しは事実上以下と等しい。

var instance = new ConsoleApp2.Foo();

あるいは

            Func<IFoo> expression = () => new ConsoleApp2.Foo();
            var instance = expression();

この呼び出しが1回だけならリフレクションパターンと大して差はないが、SQLクエリーからの結果行オブジェクト生成等だと圧倒的性能差が付く。

「パラメータがあると使えない」とか言う人はいると思うけど、Expression.New() のオーバーロード調べてみると良い。当然それなりに複雑になるけれど。

Pattern1 と同様、生成するクラスの名前を別の型にすることが可能。

パターン3 Factory

インスタンス生成を外部のクラスに隠蔽する。
コンストラクタ呼出しでもよいし、上二つのやり方を使っても良いし、他いろいろ使っても良い。
とにかくクライアントコードの中にインスタンス生成のための知識を持ち込まないことが重要。

factory クラスの実装

    public class FooFactory
    {
        public IFoo GetFoo()
        {
            return new Foo();
        }
    }

クライアントコード

        static void Pattern3()
        {
            var factory = new FooFactory();
            var instance = factory.GetFoo();
        }

クライアントコードでは Factory についての知識と、 IFoo の知識があれば良い。
Factory が何かパラメータを使って IFoo を実装した別のインスタンスを返すようにしても良い。

Pattern1、Pattern2 とは違い、呼び出す生成メソッドが実装クラスのコンストラクタを知っていなければならない
(もちろん Pattern1,Pattern2 と組み合わせて動的に生成しても良い.... あ、動的生成って言ってたけどこのパターンは基本的には静的な生成だった)

パターン4 Dependency Injection

各種 DI Container を使うパターン。コンテナによってはロードするアセンブリ自体を動的にロードして、読み込まれる型をごっそり置き換え…などという事も可能になる。

ここでは Autofac 4.6.2 (だいぶ古いけど)を使ってみる

まずはコンテナの構成

using Autofac;
        static IContainer GetContainer()
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<Foo>()
                .AsImplementedInterfaces()
                .InstancePerLifetimeScope()
                ;
            return builder.Build();
        }

コンテナに登録されたインスタンスの取得

        static void Pattern4()
        {
            var container = GetContainer();
            using(var scope = container.BeginLifetimeScope())
            {
                var instance = scope.Resolve<IFoo>();
                instance.DoSomething();

            }
        }

ここではサンプルコードの単純化のためにコンテナに登録する型を固定で書いているけれど、特定属性の付けられたクラスや特定 namespace のクラスをまとめて登録する、なんてこともできる。

その他

実行時のちょっとしたロジックの切り分け(一般ユーザーと特別会員の切り分けとか)だと Factory が活躍できる。
構成自体でロジックを切り替えたい場合(顧客ごとのカスタマイズコードの適用とか)なら DI がおすすめ。
(もちろんリフレクションを駆使しても良い。ただしそういう苦労をいろいろ肩代わりしてくれるのが DI)

6
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?