Заводской метод с использованием оператора Is/As

У меня есть factory, который выглядит примерно так: Foo — это класс-оболочка для Bar, и в большинстве случаев (но не во всех) существует сопоставление 1:1. Как правило, Bar не может ничего знать о Foo, но Foo принимает экземпляр Bar. Есть ли лучший/более чистый подход к этому?

public Foo Make( Bar obj )
{
    if( obj is Bar1 )
        return new Foo1( obj as Bar1 );
    if( obj is Bar2 )
        return new Foo2( obj as Bar2 );
    if( obj is Bar3 )
        return new Foo3( obj as Bar3 );
    if( obj is Bar4 )
        return new Foo3( obj as Bar4 ); // same wrapper as Bar3
    throw new ArgumentException();
}

На первый взгляд этот вопрос может показаться дубликатом (может быть, это так), но я не видел точно такого же. Вот тот, который близок, но не совсем:

Factory на основе Typeof или


person Swim    schedule 02.06.2010    source источник


Ответы (3)


Я не уверен, чего вы на самом деле хотите достичь. Я бы, наверное, попытался сделать его более универсальным.

Вы можете использовать атрибуты Foo, которые поддерживает Bar, а затем создать список на этапе инициализации. Мы делаем довольно много подобных вещей, это упрощает добавление и подключение новых классов.

private Dictionary<Type, Type> fooOfBar = new Dictionary<Type, Type>();
public initialize()
{
  // you could scan all types in the assembly of a certain base class
  // (fooType) and read the attribute

  fooOfBar.Add(attribute.BarType, fooType);
}

public Foo Make( Bar obj )
{
  return (Foo)Activator.CreateInstance(fooOfBar(obj.GetType()), obj);
}
person Stefan Steinegger    schedule 02.06.2010
comment
Я предложил что-то подобное, но без отражения. Значение словаря может быть делегатом, а не необработанным типом. - person Steven Sudit; 02.06.2010
comment
Я добавил пример кода, чтобы сделать это более понятным. Хотя я не тестировал его, я сильно подозреваю, что делегатский подход будет быстрее, чем Activator.CreateInstance. - person Steven Sudit; 02.06.2010
comment
Я только что попробовал этот подход, и он мне очень нравится. Он очень чистый и легко (тривиально) поддерживается. Пиковая производительность для этой конкретной фабрики не является ни необходимостью, ни требованием, иначе я бы, вероятно, согласился с ответом @Steven, что я и сделал изначально. - person Swim; 02.06.2010
comment
@Swim: Вам не нужно решать. Если вы используете делегат, вы можете либо явно подключить конструктор, либо создать его через Activator. - person Steven Sudit; 03.06.2010

Если это ссылочные типы, то вызов as после is — лишняя трата. Обычная идиома состоит в том, чтобы использовать as и проверять на ноль.

Сделав шаг назад от микрооптимизации, похоже, вы могли бы использовать некоторые методы из статьи, на которую вы ссылаетесь. В частности, вы можете создать словарь с типом, значением которого будет делегат, создающий экземпляр. Делегат возьмет (дочерний элемент) Bar и вернет (дочерний элемент) Foo. В идеале каждый потомок Foo должен зарегистрироваться в словаре, который может быть статическим внутри самого Foo.

Вот пример кода:

// Foo creator delegate.
public delegate Foo CreateFoo(Bar bar);

// Lookup of creators, for each type of Bar.
public static Dictionary<Type, CreateFoo> Factory = new Dictionary<Type, CreateFoo>();

// Registration.
Factory.Add(typeof(Bar1), (b => new Foo1(b)));

// Factory method.
static Foo Create(Bar bar)
{
    CreateFoo cf;
    if (!Factory.TryGetValue(bar.GetType(), out cf))
        return null;

    return cf(bar);
}
person Steven Sudit    schedule 02.06.2010
comment
Верно. Я знаю, что это просто обернутый as, но имеющий длинный список: Bar1 bar1 = obj as Bar1; if (bar1 != null) вернуть новый Foo1(bar1); просто кажется мне очень уродливым. ;-) - person Swim; 02.06.2010
comment
Я буду продолжать наблюдать за этим, потому что я не знаю лучшего способа, чем то, что вы делаете, и именно так я решил бы эту проблему... - person jcolebrand; 02.06.2010
comment
Плавать, если вам не нужна гибкость, то простой, быстрый, но некрасивый метод не так уж и страшен. Для гибкости рассмотрите метод словаря. - person Steven Sudit; 02.06.2010
comment
Уважаемый даунвотер (предположительно Стефан): для вас нормально голосовать против, но я был бы рад, если бы вы объяснили, почему. - person Steven Sudit; 02.06.2010
comment
Хм, я здесь ни за что не голосовал и никогда не минусую без комментария. Наверное, это тот же, кто меня минусовал? - person Stefan Steinegger; 03.06.2010
comment
Вернемся к теме: Делегат хорош, независимо от оптимизации. Он допускает любую подпись конструктора. Но вам нужно зарегистрировать все типы. Это дает высокую связь фабрики с этими типами. Таким образом, это зависит от ситуации, в которой он используется, если делегаты более подходят как полностью общий (отражение с автоматической регистрацией типа) способ. В конце концов, особой разницы нет. Это просто деталь реализации. - person Stefan Steinegger; 03.06.2010
comment
@Stefan: Мои извинения за поспешные выводы. - person Steven Sudit; 03.06.2010
comment
@Stefan: Что мне нравится в подходе Activator, так это то, что он хорошо подходит для указания типа в виде настраиваемой строки. Вот почему я использовал его для таких вещей, как плагины и, в более общем смысле, инверсия управления. В этих случаях объект с поздней привязкой реализовывал интерфейс. В OP у нас есть дети базового класса, поэтому они уже довольно тесно связаны. Вызов статического метода в базе для регистрации себя не кажется большим количеством дополнительной связи. (продолжение) - person Steven Sudit; 03.06.2010
comment
Сказав это, безусловно, можно было бы объединить подходы, чтобы получить лучшее из обоих миров. В этом случае он может использовать словарь, но если делегат не найден, он может вернуться к активатору. Так что, да, в конце концов, это просто деталь реализации, а не огромная. - person Steven Sudit; 03.06.2010

В вашем вопросе отображение одного набора классов в другой набор классов выглядит довольно просто. Однако часто требуется вызывать определенные конструкторы и/или задавать свойства выходных классов на основе входных классов. Иногда вы можете использовать библиотеку, например AutoMapper.

Однако в других случаях вам необходимо создавать специальные фабричные методы для каждого преобразования. В вашем случае это будут фабричные методы для создания Foo1 из Bar1, Foo2 из Bar2 и т. д.:

Foo1 CreateFoo1(Bar1 bar1) { ... }

Foo2 CreateFoo2(Bar2 bar2) { ... }

Вы можете сохранить все эти фабричные методы как делегаты в словаре, а затем использовать тип ввода, чтобы выбрать фабрику для создания типа вывода.

var inputType = input.GetType();
var factory = factories[inputType];
var output = factory(input);

Используя отражение, вы можете создать этот словарь, и, чтобы избежать дополнительных затрат на использование отражения при вызове фабрики, вы можете использовать выражения для компиляции небольших лямбда-выражений, которые будут выполнять требуемое приведение и вызов.

Эта функциональность может быть представлена ​​через базовый класс, который предполагает, что типы ввода и вывода находятся в иерархиях параллельных классов. Например, в вашем случае все классы Foo# могут иметь Foo в качестве базового класса, а все классы Bar# могут иметь Bar в качестве базового закрытия. Однако, если это не так, то все классы имеют object в качестве базового класса, поэтому этот подход все равно будет работать.

Ваш производный фабричный класс будет выглядеть примерно так:

public class FooFactory : TypeBasedFactory<Bar, Foo>
{
    private Foo1 CreateFoo1(Bar1 bar1)
    {
        return new Foo1(bar1.Id, bar1.Name, ...);
    }

    private Foo2 CreateFoo2(Bar2 bar2)
    {
        return new Foo2(bar2.Description, ...);
    }
}

Обратите внимание, что фабричные методы являются закрытыми. Они не предназначены для прямого вызова. Вместо этого TypeBasedFactory объявляет метод CreateFrom, который вызовет правильную фабрику:

var fooFactory = new TypeBasedFactory<Bar, Foo>();
var foo = fooFactory.CreateFrom(bar);

Вот код для TypeBasedFactory:

public abstract class TypeBasedFactory<TInput, TOutput>
    where TInput : class where TOutput : class
{
    private readonly Dictionary<Type, Func<TInput, TOutput>> factories;

    protected TypeBasedFactory()
    {
        factories = CreateFactories();
    }

    private Dictionary<Type, Func<TInput, TOutput>> CreateFactories()
    {
        return GetType()
            .GetMethods(
                BindingFlags.Public
                | BindingFlags.NonPublic
                | BindingFlags.Instance)
            .Where(methodInfo =>
                !methodInfo.IsAbstract
                && methodInfo.GetParameters().Length == 1
                && typeof(TOutput).IsAssignableFrom(methodInfo.ReturnType))
            .Select(methodInfo => new
            {
                MethodInfo = methodInfo,
                methodInfo.GetParameters().First().ParameterType
            })
            .Where(factory =>
                typeof(TInput).IsAssignableFrom(factory.ParameterType)
                && !factory.ParameterType.IsAbstract)
            .ToDictionary(
                factory => factory.ParameterType,
                factory => CreateFactory(factory.MethodInfo, factory.ParameterType));
    }

    private Func<TInput, TOutput> CreateFactory(MethodInfo methodInfo, Type parameterType)
    {
        // Create this Func<TInput, TOutput>: (TInput input) => Method((Parameter) input)
        var inputExpression = Expression.Parameter(typeof(TInput), "input");
        var castExpression = Expression.Convert(inputExpression, parameterType);
        var callExpression = Expression.Call(Expression.Constant(this), methodInfo, castExpression);
        var lambdaExpression = Expression.Lambda<Func<TInput, TOutput>>(callExpression, inputExpression);
        return lambdaExpression.Compile();
    }

    public TOutput CreateFrom(TInput input)
    {
        if (input == null)
            throw new ArgumentNullException(nameof(input));
        var inputType = input.GetType();
        Func<TInput, TOutput> factory;
        if (!factories.TryGetValue(inputType, out factory))
            throw new InvalidOperationException($"No factory method defined for {inputType.FullName}.");
        return factory(input);
    }
}

Метод CreateFactories использует отражение для поиска как общедоступных, так и закрытых методов, которые могут создать TOuput (возможно, производный класс) из TInput (неабстрактный производный класс).

Метод CreateFactory создает Func<TInput, TOutput>, который выполняет требуемое преобразование вниз перед вызовом фабричного метода. После того, как лямбда скомпилирована, ее вызов не требует дополнительных затрат.

Построение класса, производного от TypeBasedFactory, будет использовать отражение для построения словаря фабрик, поэтому вам следует избегать создания более одного экземпляра (т. е. фабрика должна быть одноэлементной).

person Martin Liversage    schedule 26.01.2017