Свойства C# установки Fluently и методы объединения в цепочку

Я использую.NET 3.5. У нас есть некоторые сложные сторонние классы, которые автоматически сгенерированы и из моего управления, но с которыми мы должны работать для тестирования. Я вижу, что моя команда делает большое глубоко вложенное свойство, getting/setting в нашем тестовом коде, и это становится довольно громоздким.

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

Моя начальная буква думала, должен был просто использовать объектные инициализаторы. Red, Blue, и Green свойства, и Mix() метод, который устанавливает четвертое свойство Color к самому близкому безопасному цвету RGB с тем смешанным цветом. Краски должны быть гомогенизированы с Stir() прежде чем они смогут использоваться.

Bucket b = new Bucket() {
  Paint = new Paint() {
    Red = 0.4;
    Blue = 0.2;
    Green = 0.1;
  }
};

Это работает для инициализации Paint, но я должен объединить в цепочку Mix() и другие методы к нему. Следующая попытка:

Create<Bucket>(Create<Paint>()
  .SetRed(0.4)
  .SetBlue(0.2)
  .SetGreen(0.1)
  .Mix().Stir()
)

Но это не масштабируется хорошо, потому что я должен был бы определить метод для каждого свойства, которое я хочу установить, и во всех классах существуют сотни различных свойств. Кроме того, C# не имеет способа динамично определить методы до C# 4, таким образом, я не думаю, что могу сцепиться в вещи сделать это автоматически в некотором роде.

Третья попытка:

Create<Bucket>(Create<Paint>().Set(p => {
    p.Red = 0.4;
    p.Blue = 0.2;
    p.Green = 0.1;
  }).Mix().Stir()
)

Это не выглядит слишком плохо и кажется, что было бы выполнимо. Действительно ли это - желательный подход? Действительно ли возможно записать a Set метод, который прокладывает себе путь? Или я должен преследовать альтернативную стратегию?

7
задан John Feminella 13 March 2010 в 15:40
поделиться

4 ответа

Это работает?

Bucket b = new Bucket() {
  Paint = new Paint() {
    Red = 0.4;
    Blue = 0.2;
    Green = 0.1;
  }.Mix().Stir()
};

Предполагая, что Mix () и Stir () определены для возврата Раскрасьте объект .

Для вызова методов, возвращающих void , вы можете использовать метод расширения, который позволит вам выполнить дополнительную инициализацию переданного вами объекта:

public static T Init<T>(this T @this, Action<T> initAction) {
    if (initAction != null)
        initAction(@this);
    return @this;
}

Который может использоваться аналогично Set () как описано:

Bucket b = new Bucket() {
  Paint = new Paint() {
    Red = 0.4;
    Blue = 0.2;
    Green = 0.1;
  }.Init(p => {
    p.Mix().Stir();
  })
};
9
ответ дан 6 December 2019 в 15:20
поделиться

Я бы использовал метод расширения Init, потому что U всегда может играть с делегатом. Черт, вы всегда можете объявить методы расширения, которые принимают выражения и даже подыгрывают выражениям (сохраните их на потом, измените, что угодно) Таким образом Вы можете легко сохранить группы по умолчанию, например:

Create<Paint>(() => new Paint{p.Red = 0.3, p.Blue = 0.2, p.Green = 0.1}).
Init(p => p.Mix().Stir())

Таким образом Вы можете использовать все действия (или функции) и стандартные инициализаторы кеширования в качестве цепочек выражений на будущее?

0
ответ дан 6 December 2019 в 15:20
поделиться

Если вы действительно хотите иметь возможность цепочки настроек свойств без необходимости писать тонну кода, одним из способов сделать это может быть использование генерации кода (CodeDom). Вы можете использовать Reflection для получения списка изменяемых свойств, а затем сгенерировать класс fluent builder с конечным методом Build(), который возвращает класс, который вы пытаетесь создать.

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

public static class PropertyBuilderGenerator
{
    public static CodeTypeDeclaration GenerateBuilder(Type destType)
    {
        if (destType == null)
            throw new ArgumentNullException("destType");
        CodeTypeDeclaration builderType = new
            CodeTypeDeclaration(destType.Name + "Builder");
        builderType.TypeAttributes = TypeAttributes.Public;
        CodeTypeReference destTypeRef = new CodeTypeReference(destType);
        CodeExpression resultExpr = AddResultField(builderType, destTypeRef);
        PropertyInfo[] builderProps = destType.GetProperties(
            BindingFlags.Instance | BindingFlags.Public);
        foreach (PropertyInfo prop in builderProps)
        {
            AddPropertyBuilder(builderType, resultExpr, prop);
        }
        AddBuildMethod(builderType, resultExpr, destTypeRef);
        return builderType;
    }

    private static void AddBuildMethod(CodeTypeDeclaration builderType,
        CodeExpression resultExpr, CodeTypeReference destTypeRef)
    {
        CodeMemberMethod method = new CodeMemberMethod();
        method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
        method.Name = "Build";
        method.ReturnType = destTypeRef;
        method.Statements.Add(new MethodReturnStatement(resultExpr));
        builderType.Members.Add(method);
    }

    private static void AddPropertyBuilder(CodeTypeDeclaration builderType,
        CodeExpression resultExpr, PropertyInfo prop)
    {
        CodeMemberMethod method = new CodeMemberMethod();
        method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
        method.Name = prop.Name;
        method.ReturnType = new CodeTypeReference(builderType.Name);
        method.Parameters.Add(new CodeParameterDeclarationExpression(prop.Type,
            "value"));
        method.Statements.Add(new CodeAssignStatement(
            new CodePropertyReferenceExpression(resultExpr, prop.Name),
            new CodeArgumentReferenceExpression("value")));
        method.Statements.Add(new MethodReturnStatement(
            new CodeThisExpression()));
        builderType.Members.Add(method);
    }

    private static CodeFieldReferenceExpression AddResultField(
        CodeTypeDeclaration builderType, CodeTypeReference destTypeRef)
    {
        const string fieldName = "_result";
        CodeMemberField resultField = new CodeMemberField(destTypeRef, fieldName);
        resultField.Attributes = MemberAttributes.Private;
        builderType.Members.Add(resultField);
        return new CodeFieldReferenceExpression(
            new CodeThisReferenceExpression(), fieldName);
    }
}

Я думаю, что это должно почти сделать это - очевидно, что это не проверено, но вы пойдете отсюда так: вы создаете codegen (наследующий от BaseCodeGeneratorWithSite), который компилирует CodeCompileUnit, заполненный списком типов. Этот список берется из типа файла, который вы регистрируете в инструменте - в данном случае я бы, вероятно, просто сделал его текстовым файлом с разделенным на строки списком типов, для которых вы хотите сгенерировать код сборщика. Пусть инструмент просканирует его, загрузит типы (возможно, сначала придется загрузить сборки) и сгенерирует байткод.

Это сложно, но не так сложно, как кажется, и когда вы закончите, вы сможете написать код, подобный этому:

Paint p = new PaintBuilder().Red(0.4).Blue(0.2).Green(0.1).Build().Mix.Stir();

Что, как я полагаю, почти точно то, что вы хотите. Все, что вам нужно сделать, чтобы вызвать генерацию кода, это зарегистрировать инструмент с пользовательским расширением (скажем, .buildertypes), поместить файл с этим расширением в ваш проект и поместить в него список типов:

MyCompany.MyProject.Paint
MyCompany.MyProject.Foo
MyCompany.MyLibrary.Bar

И так далее. Когда вы сохраните, он автоматически сгенерирует нужный вам файл кода, поддерживающий написание утверждений, подобных приведенному выше.

Я уже использовал этот подход для очень запутанной системы обмена сообщениями с несколькими сотнями различных типов сообщений. Это занимало слишком много времени, чтобы постоянно конструировать сообщение, устанавливать кучу свойств, отправлять его по каналу, получать от канала, сериализовать ответ и т.д. Использование codegen значительно упростило работу, поскольку позволило мне создать один класс обмена сообщениями, который принимал все отдельные свойства в качестве аргументов и выдавал ответ нужного типа. Это не то, что я бы рекомендовал всем, но когда вы имеете дело с очень большими проектами, иногда нужно начинать изобретать свой собственный синтаксис!

0
ответ дан 6 December 2019 в 15:20
поделиться

Я бы подумал об этом так:

По сути, вы хотите, чтобы ваш последний метод в цепочке возвращал Bucket. В вашем случае, я думаю, вы хотите, чтобы этот метод был Mix (), так как после этого вы можете Stir () ведро

public class BucketBuilder
{
    private int _red = 0;
    private int _green = 0;
    private int _blue = 0;

    public Bucket Mix()
    {
        Bucket bucket = new Bucket(_paint);
        bucket.Mix();
        return bucket;
    }
}

Таким образом, вам нужно установить хотя бы один цвет перед вызовом Mix (). Давайте сделаем это с помощью некоторых синтаксических интерфейсов.

public interface IStillNeedsMixing : ICanAddColours
{
     Bucket Mix();
}

public interface ICanAddColours
{
     IStillNeedsMixing Red(int red);
     IStillNeedsMixing Green(int green);
     IStillNeedsMixing Blue(int blue);
}

И давайте применим их к BucketBuilder.

public class BucketBuilder : IStillNeedsMixing, ICanAddColours
{
    private int _red = 0;
    private int _green = 0;
    private int _blue = 0;

    public IStillNeedsMixing Red(int red)
    {
         _red += red;
         return this;
    }

    public IStillNeedsMixing Green(int green)
    {
         _green += green;
         return this;
    }

    public IStillNeedsMixing Blue(int blue)
    {
         _blue += blue;
         return this;
    }

    public Bucket Mix()
    {
        Bucket bucket = new Bucket(new Paint(_red, _green, _blue));
        bucket.Mix();
        return bucket;
    }
}

Теперь вам нужно начальное статическое свойство для запуска цепочки

public static class CreateBucket
{
    public static ICanAddColours UsingPaint
    {
        return new BucketBuilder();
    }
}

И это почти все, теперь у вас есть свободный интерфейс с дополнительными параметрами RGB (если вы вводите хотя бы один) в качестве бонуса.

CreateBucket.UsingPaint.Red(0.4).Green(0.2).Mix().Stir();

Проблема с Fluent Interfaces в том, что их не так просто собрать, но разработчику легко кодировать их, и они очень расширяемы. Если вы хотите добавить к этому флаг Matt / Gloss, не меняя весь код вызова, это легко сделать.

Кроме того, если провайдер вашего API изменяет все, что находится под вами, вам нужно только переписать этот единственный фрагмент кода; весь код вызова может оставаться прежним.

5
ответ дан 6 December 2019 в 15:20
поделиться
Другие вопросы по тегам:

Похожие вопросы: