Свободно устанавливая свойства C # и методы цепочки - PullRequest
7 голосов
/ 13 марта 2010

Я использую .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 ]

9 голосов
/ 13 марта 2010

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

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

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

Для вызова методов, которые возвращают 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();
  })
};
5 голосов
/ 13 марта 2010

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

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

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

0 голосов
/ 13 марта 2010

Если вы действительно хотите иметь возможность связывать настройки свойств без необходимости писать тонну кода, один из способов сделать это - использовать генерацию кода (CodeDom). Вы можете использовать Reflection, чтобы получить список изменяемых свойств, сгенерировать плавный класс построителя с финальным 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

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

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

0 голосов
/ 13 марта 2010

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...