关于使用工厂模式将模型对象集合转换为 DTO 之一的建议,反之亦然

Advice on using the Factory Pattern for converting a collection of model objects to one of DTOs and vice versa

我正在尝试将共享一个公共父对象的模型对象集合转换为一个 DTO。同样,我想反转该过程 - 将具有共同父对象的一组 DTO 放入一个模型对象中。

根据我的阅读,工厂模式似乎正是我正在寻找的。我还有一个 Producer class,它通过调用相关的工厂方法来处理对象模型和 DTO 之间的转换。

有一些限制:

  1. 这是一个开源库,我不想向现有的 class 添加方法。否则访问者模式就会起作用。如有错误请指正
  2. 同样,我不想向这个项目添加任何额外的包。据我了解,AutoMapper 可能是解决此问题的方法之一。
  3. 我是 C# 和设计模式的新手,所以如果我做的事情没有意义,我深表歉意。

这里是一些示例代码,代表了我到目前为止所做的尝试。我使用了网上的一些参考来获得一个想法,但它似乎有些不对。这里提到了另一种方式:Is a switch statement applicable in a factory method? c#,但我不确定这是否适用于这种情况。

欢迎任何批评或建议。

用法示例

Animal pet1 = new Pigeon("Pidgey", 100, false);
Animal pet2 = new Rattlesnake("Ekans", 20.0, true);

IList<Animal> myPets = new List<Animal>() { pet1, pet2 };

AnimalDTOProducer dtoProducer = new AnimalDTOProducer(new AnimalDTOFactory());
IList<AnimalDTO> myDTOs = new List<AnimalDTO>();

myDTOs = dtoProducer.ConvertAnimalCollection(myPets);

型号

public abstract class Animal
{
    public Animal(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
    // business logic
}

public abstract class Bird : Animal
{
    public Bird(string name, int maxAltitude, bool isReal)
        : base(name)
    {
        Name = name;
        MaxAltitude = maxAltitude;
        IsReal = isReal;
    }

    public int MaxAltitude { get; set; }
    public bool IsReal { get; set; }
    // business logic
}

public class Pigeon : Bird
{
    public Pigeon(string name, int maxAltitude, bool isReal)
        : base(name, maxAltitude, isReal)
    {
    }
    // business logic
}

public abstract class Snake : Animal
{
    public Snake(string name, double length, bool isPoisonous)
        : base(name)
    {
        Name = name;
        Length = length;
        IsPoisonous = isPoisonous;
    }

    public double Length { get; set; }
    public bool IsPoisonous { get; set; }
    // business logic
}

public class Rattlesnake : Snake
{
    public Rattlesnake(string name, double length, bool isPoisonous)
        : base(name, length, isPoisonous)
    {
    }
    // business logic
}

DTOs

public abstract class AnimalDTO { }

public class PigeonDTO : AnimalDTO
{
    public string Name { get; set; }
    public int MaxAltitude { get; set; }
    public bool IsReal { get; set; }
}

public class RattlesnakeDTO : AnimalDTO
{
    public string Name { get; set; }
    public double Length { get; set; }
    public bool IsPoisonous { get; set; }
}

工厂

public interface IFactory { }

public interface IAnimalFactory : IFactory
{
    Animal CreateAnimal(AnimalDTO DTO);
}

public interface IAnimalDTOFactory : IFactory
{
    AnimalDTO CreateAnimalDTO(Animal animal);
}

public class AnimalFactory : IAnimalFactory
{
    public Animal CreateAnimal(AnimalDTO DTO)
    {
        switch (DTO)
        {
            case PigeonDTO _:
                var pigeonDTO = (PigeonDTO)DTO;
                return new Pigeon(pigeonDTO.Name, pigeonDTO.MaxAltitude, pigeonDTO.IsReal);
            case RattlesnakeDTO _:
                var rattlesnakeDTO = (RattlesnakeDTO)DTO;
                return new Rattlesnake(rattlesnakeDTO.Name, rattlesnakeDTO.Length, rattlesnakeDTO.IsPoisonous);
            // And many more ...
            default:
                return null;
        }
    }
}

public class AnimalDTOFactory : IAnimalDTOFactory
{
    public AnimalDTO CreateAnimalDTO(Animal animal)
    {
        switch (animal)
        {
            case Pigeon _:
                var _pigeon = (Pigeon)animal;
                return new PigeonDTO()
                {
                    Name = _pigeon.Name,
                    MaxAltitude = _pigeon.MaxAltitude,
                    IsReal = _pigeon.IsReal
                };
            case Rattlesnake _:
                var _rattlesnake = (Rattlesnake)animal;
                return new RattlesnakeDTO()
                {
                    Name = _rattlesnake.Name,
                    Length = _rattlesnake.Length,
                    IsPoisonous = _rattlesnake.IsPoisonous
                };
            // And many more ...
            default:
                return null;
        }
    }
}

生产者

public interface IProducer { }

public interface IAnimalProducer : IProducer
{
    Animal ProduceAnimalFromDTO(AnimalDTO DTO);
}

public interface IAnimalDTOProducer : IProducer
{
    AnimalDTO ProduceAnimalDTOFromAnimal(Animal animal);
}

public class AnimalProducer : IAnimalProducer
{
    private IAnimalFactory factory;

    public AnimalProducer(IAnimalFactory factory)
    {
        this.factory = factory;
    }

    public IList<Animal> ConvertAnimalDTOCollection(IList<AnimalDTO> DTOCollection)
    {
        IList<Animal> result = new List<Animal>();
        foreach (AnimalDTO DTO in DTOCollection)
        {
            var dto = ProduceAnimalFromDTO(DTO);
            if (dto != null)
                result.Add(dto);
        }

        return result;
    }

    public Animal ProduceAnimalFromDTO(AnimalDTO animalDTO)
    {
        return this.factory.CreateAnimal(animalDTO);
    }
}

public class AnimalDTOProducer : IAnimalDTOProducer
{
    private IAnimalDTOFactory factory;

    public AnimalDTOProducer(IAnimalDTOFactory factory)
    {
        this.factory = factory;
    }

    public IList<AnimalDTO> ConvertAnimalCollection(IList<Animal> collection)
    {
        IList<AnimalDTO> result = new List<AnimalDTO>();
        foreach (Animal animal in collection)
        {
            var _animal = ProduceAnimalDTOFromAnimal(animal);
            if (_animal != null)
                result.Add(_animal);
        }

        return result;
    }

    public AnimalDTO ProduceAnimalDTOFromAnimal(Animal animal)
    {
        return this.factory.CreateAnimalDTO(animal);
    }
}

更新 1

根据 sjb-sjb 和 ChiefTwoPencils 在评论中的建议,我从各自的工厂中删除了 switch 语句。结果如下所示:

public class AnimalFactory : IAnimalFactory
{
    public Animal CreateAnimal(AnimalDTO DTO)
    {
        Type srcType = DTO.GetType();
        Type modelType = Type.GetType(Regex.Replace(srcType.FullName, @"(DTO)$", ""));
        IList<PropertyInfo> props = new List<PropertyInfo>(srcType.GetProperties());
        var propVals = props.Select(prop => prop.GetValue(DTO, null)).ToArray();
        
        Animal animal = (Animal)Activator.CreateInstance(modelType, propVals);

        return animal;
    }
}

public class AnimalDTOFactory : IAnimalDTOFactory
{
    public AnimalDTO CreateAnimalDTO(Animal animal)
    {
        Type srcType = animal.GetType();
        Type dtoType = Type.GetType($"{srcType.FullName}DTO");
        AnimalDTO dto = (AnimalDTO)Activator.CreateInstance(dtoType, new object[] { });
        foreach (PropertyInfo dtoProperty in dtoType.GetProperties())
        {
            PropertyInfo srcProperty = srcType.GetProperty(dtoProperty.Name);
            if (srcProperty != null)
            {
                dtoProperty.SetValue(dto, srcProperty.GetValue(animal));
            }
        }
        return dto;
    }
}

我在最初的问题中忘记提到的一件事是模型的构造函数可能具有比 DTO 对象具有的属性更多的参数。那和参数的顺序可能不一样。我认为在伪代码中,解决方案看起来像这样:

void AssignParamsToConstructor() 
{
    // Extract constructer parameters with names into an ordered list
    // Match DTO properties with extracted parameters via name and type
    // Fill any missing parameters with a default value or null
    // Pass the final list of parameters as an array to Activator.CreateInstance method
}

我会暂时研究解决这个问题的方法,但欢迎任何指点。

更新 2

好的,所以我找到了一种解决先前问题的 hacky 解决方案,该问题是关于调用带有缺失或乱序参数的模型构造函数。

我创建了一个助手 class,它根据模型构造函数参数和 DTO 属性的组合创建一个有序参数数组。然后可以将该数组传递给 Activator.CreateInstance 而不会引起任何问题。

这是更新后的 AnimalFactory.CreateAnimal 方法:

public Animal CreateAnimal(AnimalDTO DTO)
{
    Type srcType = DTO.GetType();
    Type modelType = Type.GetType(Regex.Replace(srcType.FullName, @"(DTO)$", ""));
    object[] propVals = Helpers.GenerateConstructorArgumentValueArray(modelType, DTO);
    Animal animal = (Animal)Activator.CreateInstance(modelType, propVals);
    return animal;
}

这是帮手 class:

public static class Helpers
{
    public static object[] GenerateConstructorArgumentValueArray(Type type, object obj)
    {
        IList<(string, Type)> ctorArgTypes = new List<(string, Type)>();
        IList<(string, object)> propVals = new List<(string, object)>();

        // Get constructor arguments
        ctorArgTypes = GetConstructorArgumentsAndTypes(type);

        // Get object properties
        propVals = GetObjectPropertiesAndValues(obj);

        // Create args array
        IList<object> paramVals = new List<object>();

        foreach (var ctorArg in ctorArgTypes)
        {
            object val;

            string _name = ctorArg.Item1.ToLower();
            (string, object) _namedProp = propVals.Where(prop => prop.Item1.ToLower() == _name).FirstOrDefault();
            if (_namedProp.Item2 != null)
            {
                val = _namedProp.Item2;
            }
            else
            {
                val = ctorArg.Item2.IsValueType ? Activator.CreateInstance(ctorArg.Item2) : null;  
            }
            paramVals.Add(val);
        }

        return paramVals.ToArray();
    }

    private static IList<(string, Type)> GetConstructorArgumentsAndTypes(Type type)
    {
        List<(string, Type)> ctorArgs = new List<(string, Type)>();

        TypeInfo typeInfo = type.GetTypeInfo();
        ConstructorInfo[] ctors = typeInfo.DeclaredConstructors.ToArray();
        ParameterInfo[] ctorParams = ctors[0].GetParameters();

        foreach (ParameterInfo info in ctorParams)
        {
            ctorArgs.Add((info.Name, info.ParameterType));
        }

        return ctorArgs;
    }

    private static IList<(string, object)> GetObjectPropertiesAndValues(object obj)
    {
        List<(string, object)> props = new List<(string, object)>();

        PropertyInfo[] propInfo = obj.GetType().GetProperties();
        foreach (PropertyInfo info in propInfo)
        {
            string name = info.Name;
            object val = info.GetValue(obj);

            props.Add((name, val));
        }

        return props;
    }
}

稍后我将不得不查看它以了解如何对其进行改进。然而,就目前而言,它完成了它的工作。

如果您有任何意见或意见,我将不胜感激。我会继续更新这个 post 直到我找到一个绝对的解决方案。

使用反射可以避免switch语句:

    public AnimalDTO ToDTO( Animal src)
    {
        Type srcType = src.GetType();
        Type dtoType = Type.GetType(srcType.Name + "DTO");
        AnimalDTO dto = (AnimalDTO)Activator.CreateInstance(dtoType, new object[] { }); 
        foreach (PropertyInfo dtoProperty in dtoType.GetProperties()) {
            PropertyInfo srcProperty = srcType.GetProperty(dtoProperty.Name);
            if (srcProperty != null) {
                dtoProperty.SetValue(dto, srcProperty.GetValue(src));
            }
        }
        return dto;
    }

要获得FromDTO方法,只需将ToDTO中的src和dto角色互换即可。

我不会在这种常见情况下重新发明轮子。

https://automapper.org/

https://www.nuget.org/packages/automapper/

https://github.com/MapsterMapper/Mapster

https://www.nuget.org/packages/Mapster/

.......

了解如何使用其中一种框架。

下面是 mapster......“性能”数字......我就是这样找到它的(有人告诉我要注意自动映射器的性能)

所以我想出了一个似乎实现了我最初目标的解决方案。

之所以一开始很难解决,是因为原工厂class责任太多。它必须映射属性并创建一个新对象。将它们分开可以很容易地实现这个 post:

建议的通用工厂

https://web.archive.org/web/20140414013728/http://tranxcoder.wordpress.com/2008/07/11/a-generic-factory-in-c

我创建了一个简单的映射器,可以自动映射实体和 DTO 属性。更简单的解决方案是使用像 grandaCoder 建议的 AutoMapper。我的情况需要,否则自定义映射器是可行的方法。我还尝试尽量减少对 System.Reflection 的调用,这样性能就不会受到太大影响。

最终结果是一个工厂,它可以在任何实体和 DTO 对象之间进行转换,在它们之间映射属性,并且可以在没有默认/空构造函数的情况下实例化实体class。

我最终对原始 post 做了很多更改,所以我将最终结果上传到 github:https://github.com/MoMods/EntityDTOFactory

我对最终解决方案的任何其他想法/批评持开放态度。这是我第一次解决这类问题,所以很可能有更好的想法。

再次感谢您的帮助和建议!