简体   繁体   中英

How can I refactor this C# code

Basically I have a method that takes an object and sets another objects properties based on the object passed in.

eg:

    private void SetObject(MyClass object)
{
  MyClass2 object2 = new MyClass2();
  object2.Property1 = HelperClass.Convert(object.Property1);
  //....
  // Lots more code ....
  //....
}

Now the method is 53 lines long because there are alot of properties to set. The method seems too long to me but I'm struggling to work out how I can possibly break it down.

One option is to try and group up similar properties and pass the object around as a reference to different methods that set these similar properties, but that doesn't seem to sit right with me.

Or I could create a constructor for MyClass2 that accepts a MyClass1 but that doesn't seem right either.

Anyway would welcome some suggestions.

EDIT: Ok thanks for the replies I'll have to give more info, the property names arent the same and I have to call some conversion methods as well. Reflection wouldn't be good because of this and also the performance hit. Automapper I think for the same reasons.

A real example of the code:

    private ReportType GetReportFromItem(SPWeb web, SPListItem item)
            {
                ReportType reportType = new ReportType();
                reportType.ReportID = int.Parse(item["Report ID"].ToString());
                reportType.Name = item["Title"].ToString();
                reportType.SourceLocation = FieldHelpers.GetUri(item["Source Location"]);
                reportType.TargetLocation = FieldHelpers.GetUri(item["Document Library"]);
                SPFieldUserValue group1 = 
                    new SPFieldUserValue(web, FieldHelpers.GetStringFieldValue(item, "Security Group 1"));
                reportType.SecurityGroup1 = group1.LookupValue;
                SPFieldUserValue group2 =
                    new SPFieldUserValue(web, FieldHelpers.GetStringFieldValue(item, "Security Group 2"));
                reportType.SecurityGroup2 = group2.LookupValue;
                SPFieldUserValue group3 =
                    new SPFieldUserValue(web, FieldHelpers.GetStringFieldValue(item, "Security Group 3"));
                reportType.SecurityGroup3 = group3.LookupValue;
                SPFieldUserValue group4 =
                    new SPFieldUserValue(web, FieldHelpers.GetStringFieldValue(item, "Security Group 4"));
// More code
//...
//...
}

听起来像是AutoMapper的工作

use reflection to do it. probably have a method like this:

private void SetProperties<T>(List<T> objects, List<Tuple<string, object>> propsAndValues) where T:<your_class>
        {
            Type type = typeof(T);
            var propInfos = propsAndValues.ToDictionary(key => type.GetProperty(key.Item1, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.SetProperty), elem => elem.Item2);

            objects.AsParallel().ForAll(obj =>
                {
                    obj.SetProps(propInfos);                                  
                });

        }

public static void SetProps<T>(this T obj, Dictionary<PropertyInfo, object> propInfos) where T : <your_class>
        {
            foreach (var propInfo in propInfos)
            {
                propInfo.Key.SetValue(obj, propInfo.Value, null);
            }            
        }

There are a few strategies that came to my mind for doing this, all with their own advantages and disadvantages. Also, I was not familiar with it, but the AutoMapper tool linked to in a different answer to your question sounds like it also could be a good solution. (Also, if there were any way of deriving your classes all from the same class, or storing the properties themselves in a struct instead of directly in the classes, those seem like things to consider as well.)

Reflection

This was mentioned in this answer . However, I am not sure I totally understand the intended use of the functions in that answer, since I don't see a GetValue call, nor any mapping between two types. Also, I can see times where you might want to create something to allow for two different names to map to one another, or for conversion between two types. For a rather generic solution, I would probably go with an approach like this:

  • Create an extension class with one or more methods that will use reflection to copy based on identical property names and/or pre-defined configuration objects.
  • For each pair of types that has properties that won't have identical names, create a configuration object mapping the names to each other.
  • For properties that you won't want to be copied, create a configuration object that holds a list of names to ignore.
  • I don't actually see anything objectionable about passing one class to another in the constructor if the intent is to copy the properties, seems more of a matter of style than anything hard and fast.

Example Code

classes to be copied to:

public class MyClass2
{
    public int Property1 { get; set; }
    public int Property2 { get; set; }
    public string Property3WithOtherName { get; set; }
    public double Property4 { get; set; }
    public string Property5WithDifferentName { get; set; }

    public string TestIntToString { get; set; }
    public int TestStringToInt { get; set; }
}

public class MyClass3
{
    public int Prop1 { get; set; }
    public int Prop2 { get; set; }
    public string Prop3OtherName { get; set; }
    public double Prop4 { get; set; }
    public string Prop5DiffName { get; set; }
    public string PropOnlyClass3 { get; set; }
    public string[] StringArray { get; set; }
}

class to be copied, w/ mapping info to the other objects:

public class MyClass
{
    public int Property1 { get; set; }
    public int Property2 { get; set; }
    public string Property3 { get; set; }
    public double Property4 { get; set; }
    public string Property5 { get; set; }

    public double PropertyDontCopy { get; set; }
    public string PropertyOnlyClass3 { get; set; }
    public int[] PropertyIgnoreMe { get; set; }

    public string[] StringArray { get; set; }

    public int TestIntToString { get; set; }
    public string TestStringToInt { get; set; }

    # region Static Property Mapping Information
        // this is one possibility for creating and storing the mapping
       // information: the class uses two dictionaries, one that links 
       // the other type with a dictionary of mapped properties, and 
       // one that links the other type with a list of excluded ones.
        public static Dictionary<Type, Dictionary<string, string>>
            PropertyMappings =
                new Dictionary<Type, Dictionary<string, string>>
                {
                    {
                        typeof(MyClass2),
                        new Dictionary<string, string>
                        {
                            { "Property3", "Property3WithOtherName" },
                            { "Property5", "Property5WithDifferentName" },
                        }
                    },
                    {
                        typeof(MyClass3),
                        new Dictionary<string, string>
                        {
                            { "Property1", "Prop1" },
                            { "Property2", "Prop2" },
                            { "Property3", "Prop3OtherName" },
                            { "Property4", "Prop4" },
                            { "Property5", "Prop5DiffName" },
                            { "PropertyOnlyClass3", "PropOnlyClass3" },
                        }
                    },
                };

        public static Dictionary<Type, List<string>>
            UnmappedProperties =
                new Dictionary<Type, List<string>>
                {
                    {
                        typeof(MyClass2),
                        new List<string> 
                            {
                                "PropertyDontCopy",
                                "PropertyOnlyClass3",
                                "PropertyIgnoreMe"
                            }
                    },
                    {
                        typeof(MyClass3),
                        new List<string> 
                            {
                                "PropertyDontCopy", 
                                "PropertyIgnoreMe"
                            }
                    }
                };

        // this function pulls together an individual property mapping
        public static Tuple<Dictionary<string, string>, List<string>>
            MapInfo<TOtherType>()
            {
                return 
                    new Tuple<Dictionary<string,string>,List<string>>
                    (
                        PropertyMappings[typeof(TOtherType)],
                        UnmappedProperties[typeof(TOtherType)]
                    );
            }

    #endregion
}

Mapping Extension Class:

public static class MappingExtensions
{
    // this is one possibility for setting up object mappings
    #region Type Map Definition Section
        // * set up the MapInfo<TOther>() call in each object to map
        // * setup as follows to map two types to an actual map
        public static Tuple<Type, Type> MapFromTo<TFromType, TToType>()
        {
            return Tuple.Create<Type,Type>(typeof(TFromType), typeof(TToType));
        }

        static Dictionary<
            Tuple<Type, Type>,
            Tuple<Dictionary<string, string>, List<string>>
        >
        MappingDefinitions =
            new Dictionary <
                Tuple<Type,Type>,
                Tuple<Dictionary<string,string>,List<string>>
            > 
            {
                { MapFromTo<MyClass,MyClass2>(), MyClass.MapInfo<MyClass2>() },
                { MapFromTo<MyClass,MyClass3>(), MyClass.MapInfo<MyClass3>() },
            };
    #endregion

    // method using Reflection.GetPropertyInfo and mapping info to do copying
    // * for fields you will need to reflect using GetFieldInfo() instead
    // * for both you will need to reflect using GetMemberInfo() instead
    public static void CopyFrom<TFromType, TToType>(
            this TToType parThis,
            TFromType parObjectToCopy
        )
    {
        var Map = MappingDefinitions[MapFromTo<TFromType, TToType>()];

        Dictionary<string,string> MappedNames = Map.Item1;
        List<string> ExcludedNames = Map.Item2;

        Type FromType = typeof(TFromType);  Type ToType = typeof(TToType);

        // ------------------------------------------------------------------------
        // Step 1: Collect PIs for TToType and TFromType for Copying

        // ------------------------------------------------------------------------
        // Get PropertyInfos for TToType

        // the desired property types to reflect for ToType
        var ToBindings =
            BindingFlags.Public | BindingFlags.NonPublic  // property visibility 
            | BindingFlags.Instance                       // instance properties
            | BindingFlags.SetProperty;                   // sets for ToType

        // reflect an array of all properties for this type
        var ToPIs = ToType.GetProperties(ToBindings);

        // checks for mapped properties or exclusions not defined for the class
        #if DEBUG
            var MapErrors =
                from name in 
                    MappedNames.Values
                where !ToPIs.Any(pi => pi.Name == name)
                select string.Format(
                    "CopyFrom<{0},{1}>: mapped property '{2}' not defined for {1}",
                    FromType.Name, ToType.Name, name
                );
        #endif

        // ------------------------------------------------------------------------
        // Get PropertyInfos for TFromType

        // the desired property types to reflect; if you want to use fields, too, 
        //   you can do GetMemberInfo instead of GetPropertyInfo below
        var FromBindings =
            BindingFlags.Public | BindingFlags.NonPublic  // property visibility 
            | BindingFlags.Instance                       // instance/static
            | BindingFlags.GetProperty;                   // gets for FromType

        // reflect all properties from the FromType
        var FromPIs = FromType.GetProperties(FromBindings);

        // checks for mapped properties or exclusions not defined for the class
        #if DEBUG
            MapErrors = MapErrors.Concat(
                from mn in MappedNames.Keys.Concat(
                    ExcludedNames)
                where !FromPIs.Any(pi => pi.Name == mn)
                select string.Format(
                    "CopyFrom<{0},{1}>: mapped property '{2}' not defined for {1}",
                    FromType.Name, ToType.Name, mn
                )
            );

            // if there were any errors, aggregate and throw 
            if (MapErrors.Count() > 0)
                throw new Exception(
                    MapErrors.Aggregate(
                        "", (a,b)=>string.Format("{0}{1}{2}",a,Environment.NewLine,b)
                ));
        #endif

        // exclude anything in the exclusions or not in the ToPIs
        FromPIs = FromPIs.Where(
            fromPI => 
                !ExcludedNames.Contains(fromPI.Name)
                && ToPIs.Select(toPI => toPI.Name).Concat(MappedNames.Keys)
                    .Contains(fromPI.Name)
        )
        .ToArray();

        // Step 1 Complete 
        // ------------------------------------------------------------------------

        // ------------------------------------------------------------------------
        // Step 2: Copy Property Values from Source to Destination 

        #if DEBUG
            Console.WriteLine("Copying " + FromType.Name + " to " + ToType.Name);
        #endif

        // we're using FromPIs to drive the loop because we've already elimiated 
        // all items that don't have a matching value in ToPIs
        foreach (PropertyInfo FromPI in FromPIs)
        {
            PropertyInfo ToPI;

            // if the 'from' property name exists in the mapping, use the mapped 
            //   name to find ToPI, otherwise use ToPI matching the 'from' name
            if (MappedNames.Keys.Contains(FromPI.Name))
                ToPI = ToPIs.First(pi => pi.Name == MappedNames[FromPI.Name]);
            else
                ToPI = ToPIs.First(pi => pi.Name == FromPI.Name);

            Type FromPropertyType = FromPI.PropertyType;
            Type ToPropertyType = ToPI.PropertyType;

            // retrieve the property value from the object we're copying from; keep
            // in mind if this copies by-reference for arrays and other ref types,
            // so you will need to deal with it if you want other behavior
            object PropertyValue = FromPI.GetValue(parObjectToCopy, null);

            // only need this if there are properties with incompatible types
            // * implement IConvertible for user-defined types to allow conversion
            // * you can try/catch if you want to ignore items which don't convert
            if (!ToPropertyType.IsAssignableFrom(FromPropertyType))
                PropertyValue = Convert.ChangeType(PropertyValue, ToPropertyType);

            // set the property value on the object we're copying to
            ToPI.SetValue(parThis, PropertyValue, null);

            #if DEBUG
                Console.WriteLine(
                    "\t"
                    + "(" + ToPI.PropertyType.Name + ")" + ToPI.Name
                    + " := "
                    + "(" + FromPI.PropertyType.Name + ")" + FromPI.Name 
                    + " == " 
                    + ((ToPI.PropertyType.Name == "String") ? "'" : "")
                    + PropertyValue.ToString()
                    + ((ToPI.PropertyType.Name == "String") ? "'" : "")
                );
            #endif
        }

        // Step 2 Complete
        // ------------------------------------------------------------------------
    }
}

Test Method: public void RunTest() { MyClass Test1 = new MyClass();

        Test1.Property1 = 1;
        Test1.Property2 = 2;
        Test1.Property3 = "Property3String";
        Test1.Property4 = 4.0;
        Test1.Property5 = "Property5String";

        Test1.PropertyDontCopy = 100.0;
        Test1.PropertyIgnoreMe = new int[] { 0, 1, 2, 3 };
        Test1.PropertyOnlyClass3 = "Class3OnlyString";
        Test1.StringArray = new string[] { "String0", "String1", "String2" };

        Test1.TestIntToString = 123456;
        Test1.TestStringToInt = "654321";

        Console.WriteLine("-------------------------------------");
        Console.WriteLine("Copying: Test1 to Test2");
        Console.WriteLine("-------------------------------------");

        MyClass2 Test2 = new MyClass2();
        Test2.CopyFrom(Test1);

        Console.WriteLine("-------------------------------------");
        Console.WriteLine("Copying: Test1 to Test3");
        Console.WriteLine("-------------------------------------");

        MyClass3 Test3 = new MyClass3();
        Test3.CopyFrom(Test1);

        Console.WriteLine("-------------------------------------");
        Console.WriteLine("Done");
        Console.WriteLine("-------------------------------------");
    }
}

Output:

-------------------------------------
Copying: Test1 to Test2
-------------------------------------
Copying MyClass to MyClass2
    (Int32)Property1 := (Int32)Property1 == 1
    (Int32)Property2 := (Int32)Property2 == 2
    (String)Property3WithOtherName := (String)Property3 == 'Property3String'
    (Double)Property4 := (Double)Property4 == 4
    (String)Property5WithDifferentName := (String)Property5 == 'Property5String'
    (String)TestIntToString := (Int32)TestIntToString == '123456'
    (Int32)TestStringToInt := (String)TestStringToInt == 654321
-------------------------------------
Copying: Test1 to Test3
-------------------------------------
Copying MyClass to MyClass3
    (Int32)Prop1 := (Int32)Property1 == 1
    (Int32)Prop2 := (Int32)Property2 == 2
    (String)Prop3OtherName := (String)Property3 == 'Property3String'
    (Double)Prop4 := (Double)Property4 == 4
    (String)Prop5DiffName := (String)Property5 == 'Property5String'
    (String)PropOnlyClass3 := (String)PropertyOnlyClass3 == 'Class3OnlyString'
    (String[])StringArray := (String[])StringArray == System.String[]
-------------------------------------
Done
-------------------------------------

NOTE: if you have use for copying back the other direction, or would like, for instance, to create your mappings with a code generator, you may want to go with having your mappings in a separate static variable somewhere else, mapped by both types, rather than storing your mappings directly within a class. If you want to reverse direction, you could probably just add a flag to the existing code that lets you invert the meanings of the tables and of the passed types, although I would recommend changing references to TToType and TFromType to TType1 and TType2.

For having a code generator generate the mapping it's probably a good bit easier to separate it into the separate class with two type parameters for the generic, so that you don't necessarily have to worry about putting those definitions directly within your classes; I was torn about how to do it when I wrote the code, but I do think that would probably have been a more flexible option. (On the other hand, it probably means needing larger overall structure, which is why I broke it out like I did in the first place.) Another advantage to this way is that you don't necessarily need to create everything all at once, you could conceivable change to member variables that let you create them on-the-fly as needed, possibly through a function param or through an additional interface on your object.

Code Generation

This can be used by itself, it can also be used as a tool in conjunction with either of the next two methods. But the idea is that you can create a CSV or use some other method to hold your mapping data, and then you can create class templates (in separate files or in code) with placeholders in them (eg, ${PROPERTIES_LIST} ) that you can use to do .Replace() operations on, or you can create templates of the .xsd files used for typed dataset generation in the next section, generate the individual lines of the .xsd from the list, or, finally you could create the structures that hold the mapping information for something like the solution I give in the reflection section.

One thing I've done in the past which was very handy was to just whip up a typed dataset with a table structure that can hold all the information I need to do my code generation, and use the old .NET 2.0 version of the GridView to allow me to enter the data in. But even with just a simple XmlDocument or CSV file(s), it should be very easy to enter your properties mappings and read them back in, and the none of the generation for any of the scenarios here is likely to be terribly much effort, usually even compared to having to type it all in by hand just once or twice, plus if it's something that gets updated at all often, you will eventually save yourself hand-coding errors and debug time as well.

Typed Data Sets

Although it's sort of beyond the intended use of the data set, if you don't need super-high performance, it can come in handy. After defining a table in the dataset and building, you have a set of strongly-typed objects that can be used, for instance, to hold objects representing the tables and columns or rows. So, you could easily create a typed dataset from scratch using column names and types matching your objects, and then loop through the column names to get the names of your properties. While not as powerful as reflection, it could be a very quick way of getting yourself set up to copy two objects with the same property names. Of course, it's a much more limited usefulness because it's only going to be useful if you have control over your types, and if you don't have any particularly complicated needs for copying the data.

But it also can let you do things like:

MyClassDataRow Object1 = MyDataSet.MyClassTable.NewRow();

Object1.Prop1 = 123;
Object2.Prop2 = "Hello Dataset";
// etc...

MyClass2DataRow Object2 = MyDataSet.MyClass2Table.NewRow();

foreach (DataColumn ColumnName in MyClassTable.Columns) 
    Object2[ColumnName] = Object1[ColumnName];

with little additional effort. And, if you're in a situation where you want to be able save your objects out to disk, just add the rows to the table you've got a great way of serializing the data with the DataSet.ReadXml and DataSet.WriteXml calls.

Also, for the reflection solution, as long as your requirements weren't too complicated, you could create your mapping definitions by creating a typed dataset with a couple of tables with column names matching those to map, and then using the information in the DataColumn s of the tables to do your mapping definition instead of Dictionaries , Lists , and Tuples . Using some sort of predefined names prefixes or otherwise unused types in your table, you ought to be able to duplicate most of the functionality in the sample code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM