Плавная настройка свойств C# и методов цепочки

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

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

моей первой мыслью было просто использовать инициализаторы объектов. 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()
)

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

4 ответов


это работает?

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

предполагая, что Mix() и Stir() определены для возврата


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

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

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 интерфейсами заключается в том, что их не так легко собрать, но они легко кодируются разработчиком, и они очень расширяемы. Если вы хотите добавить флаг Matt / Gloss к этому без изменения всего вашего вызывающего кода, это легко делать.

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


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

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

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


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

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

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

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

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