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

问题描述

我正在尝试将共享公共父对象的模型对象的集合转换为DTO之一。同样,我想反向执行此过程-将具有公共父对象的DTO集合带入模型对象之一。

从我阅读的内容来看,工厂模式似乎是我想要的。我还有一个Producer类,通过调用相关的工厂方法来处理对象模型和DTO之间的转换。

有一些限制:

  1. 这是一个开放源代码库,我不想向现有类中添加方法。否则,访客模式将起作用。如果我错了,请纠正我。
  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,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,bool isPoisonous)
        : base(name,length,isPoisonous)
    {
    }
    // business logic
}

DTO

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

好的,所以我找到了一个解决之前问题的解决方案,该问题涉及使用缺少或乱序的参数调用Model构造函数。

我创建了一个帮助程序类,该类基于Model构造函数参数和DTO属性的组合创建了一个有序的参数数组。然后可以将该数组传递给Activator.CreateInstance而不引起任何问题。

这是更新的AnimalFactory.CreateAnimal方法:

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

这是帮助程序类:

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;
    }
}

稍后,我将不得不查看此内容以了解如何对其进行改进。但是暂时,它正在发挥作用。

如果您有任何意见或建议,我将不胜感激。在找到绝对解决方案之前,我将不断更新此帖子。

解决方法

因此,我制定了一种解决方案,该解决方案似乎可以实现我的最初目标。

之所以难以解决的原因是由于原始的Factory类职责过多。它必须映射属性并创建一个新对象。将它们分开可以轻松实现本文所建议的Generic Factory:

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

我创建了一个简单的映射器,该映射器将自动映射Entity和DTO属性。较简单的解决方案是使用像grandaCoder建议的AutoMapper。否则,我的情况需要如此,因此自定义映射器是必须的。我还尝试最小化对System.Reflection的调用,以免性能受到太大影响。

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

我最终对原始帖子进行了很多更改,因此我将最终结果上传到了github:https://github.com/MoMods/EntityDTOFactory

对于最终解决方案,我欢迎其他任何想法/批评。这是我第一次解决此类问题,因此很可能有一些更好的主意。

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

,

使用反射可以避免使用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/

OR

https://github.com/MapsterMapper/Mapster

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

.......

了解如何使用这些框架之一。

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

enter image description here

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...