如何在运行时在已有的类中创建方法

本文关键字:创建 方法 运行时 | 更新日期: 2023-09-27 18:06:40

我正在使用流畅模式来帮助进行单元测试和结果对象构建。流畅构建器模式的最大痛点是必须为我可能想要设置的每个属性定义所有这些With____方法。

当它涉及到一个可能有30个字段的对象时,我可能想要设置,我不想写30个方法,它们几乎都做同样的事情。我宁愿写一些动态的东西,可以为我处理所有类似的逻辑。

例如(psuedo code)

for each property in this.properties 
  define method( property.name with 
    return type: this.class,
    parameter types: [property.type]){
    set property property.name, parameters[0]
    return this
  }

这是我目前在c#

中所做的
 var properties = typeof(EngineModelBuilder).GetProperties();
 foreach (var property in properties){
   // how do I create a method here with the property?
   // a property has .PropertyType and a .Name
   // and the return type is always going to be 'this'
 }

作为参考,下面是一个正常的流畅构建器方法的样子:

    public EngineModelBuilder WithDescription(string description)
    {
        _description = description;
        return this;
    }

如何在运行时在已有的类中创建方法

这就是我想出来的类。它利用c#的动态对象来使用运行时功能的"method_missing"方法。

注意method_missing来自Ruby,通常用于这种动态行为。

DynamicBuilder.cs:

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace PGenCore
{
    /// <summary>
    /// TODO: test different field retreival techniques (using fields from T, and managing in a Dictionary)
    ///  - how would we define defaults?
    ///
    ///
    /// Usage:
    ///   dynamic objectBuilder = new ObjectBuilder();
    ///   var object = objectBuilder
    ///     .WithObjectFieldName(appropriateValue)
    ///     .WithObjectField(appropriateValue2)
    ///     .Build();
    ///
    /// </summary>
    /// <typeparam name="T">Type to Build</typeparam>
    public class DynamicBuilder<T> : DynamicObject
    {
        #region List of FieldInformation
        protected FieldInfo[] FieldInfos;
        public FieldInfo[] FieldInformation
        {
            get
            {
                // don't GetFields all the time
                if (FieldInfos != null) return FieldInfos;
                FieldInfos = this.GetType().GetFields(
                    BindingFlags.Public |
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);
                return FieldInfos;
            }
        }
        #endregion
        #region Utility
        /// <summary>
        /// converts FieldName to _fieldName
        /// </summary>
        /// <param name="publicName"></param>
        /// <returns></returns>
        public static string PublicNameToPrivate(string publicName)
        {
            var propertyLowerFirstLetterName = char.ToLower(
                publicName[0]) + publicName.Substring(1);
            var privateName = "_" + propertyLowerFirstLetterName;
            return privateName;
        }
        #endregion
        #region Method is Missing? Check for With{FieldName} Pattern
        /// <summary>
        /// Inherited form DynamicObject.
        /// Ran before each method call.
        ///
        ///
        /// Note: Currently only works for setting one value
        ///  at a time.
        ///   e.g.: instance.Object = value
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="args"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public override bool TryInvokeMember(
             InvokeMemberBinder binder,
             object[] args,
             out object result)
        {
            var firstArgument = args[0];
            var methodName = binder.Name;
            var propertyRootName = methodName;
            // following the builder pattern,
            // methods that participate in building T,
            // return this so we can chain building methods.
            result = this;
            // for booleans, since the field / property should be named as
            // a question, using "With" doesn't make sense.
            // so, this logic is only needed when we are not setting a
            // boolean field.
            if (!(firstArgument is bool))
            {
                // if we are not setting a bool, and we aren't starting
                // with "With", this method is not part of the
                // fluent builder pattern.
                if (!methodName.Contains("With")) return false;
                propertyRootName = methodName.Replace("With", "");
            }
            // convert to _privateFieldName
            // TODO: think about dynamicly having fields in a Dictionary,
            //  rather than having to specify them
            var builderFieldName = PublicNameToPrivate(propertyRootName);
            // find matching field name, given the method name
            var fieldInfo = FieldInformation
                .FirstOrDefault(
                    field => field.Name == builderFieldName
                 );
            // if the field was not found, abort
            if (fieldInfo == null) return false;
            // set the field to the value in args
            fieldInfo.SetValue(this, firstArgument);
            return true;
        }
        #endregion
        /// <summary>
        /// Returns the built object
        /// </summary>
        /// <returns></returns>
        public virtual T Build()
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// for any complex associations
        /// - building lists of items
        /// - building anything else that isn't just an easy vavlue.
        /// </summary>
        public virtual void SetRelationshipDefaults()
        {
            throw new NotImplementedException();
        }
    }
}

使用的测试可以在这里看到:https://github.com/NullVoxPopuli/Procedural-Builder/blob/master/ProceduralBuilder.Test/DynamicBuilderTest.cs