Сохранение обобщенной коллекции

Имеются объекты:

public abstract class Animal
{
}

public class Dog : Animal
{
}

public class Cat : Animal
{
}

Имеется класс сохранения этих объектов, а также их коллекции.

public class Repository
{
    public void Create(IEnumerable<Animal> animals)
    {

    }

    public void Create(Cat cat)
    {

    }

    public void Create(Dog dog)
    {

    }
}

ВОПРОС: Можно ли реализовать метод Create(IEnumerable<Animal> animals) и обойтись без:

  1. downcast
  2. внедрения логики сохранения модели в саму модель

Если нельзя, то какой способ самый правильный или общепринятый? (вроде бы так ставить вопрос нельзя, но хотелось бы перенять опыт)

Решения, которые я придумал, но они не подходят под эти 2 пункта:

1. DownCast

Такая реализация нарушает принцип открытости-закрытости. Также DownCast сам по себе нарушает строгую типизацию.

public void Create(IEnumerable<Animal> animals)
{
    foreach (var animal in animals)
    {
        if (animal is Cat cat)
        {
            Create(cat);
        }

        if (animal is Dog dog)
        {
            Create(dog);
        }
    }
}

1.1 DownCast v2

Создать класс Creator и создать его наследников CatCreator и DogCreator.

Плюс: Уже вроде не нарушается закрытость-открытость, ведь нам не приходится вносить изменения в репозиторий. Просто нужно будет реализовать еще один Creator.

Минус: По прежнему есть DownCast, что не очень хорошо.

Мне этот способ кажется наименее неправильным.

public abstract class Creator
{
    public abstract bool CanCreate(Animal animal);
    public abstract void Create(Animal animal);
}

public class CatCreator : Creator
{
    public override bool CanCreate(Animal animal)
    {
        return animal is Cat;
    }

    public override void Create(Animal animal)
    {
    }
}

//Для DogCreator аналогично

public class Repository
{
    private readonly IEnumerable<Creator> _creators;

    public Repository()
    {
        _creators = new List<Creator> // конечно надо DI, но для простоты сделал так
        {
            new CatCreator(),
            new DogCreator()
        };
    }

    public void Create(IEnumerable<Animal> animals)
    {
        foreach (var animal in animals)
        {
            _creators.Single(creator => creator.CanCreate(animal)).Create(animal);
        }
    }
}

2 перенести логику сохранения в класс Animal

Избавляет от downcast, но тогда модель животного получает доступ к внешней системе, что тоже не хорошо.

public void Create(IEnumerable<Animal> animals)
{
    foreach (var animal in animals)
    {
        animal.Save();
    }
}

Ответы (1 шт):

Автор решения: Max S

Вероятно, все объекты стоит сохранять в базу одинаково с помощью, например, сериализации.

Если нет, абстрагируясь от деталей сохранения объектов, по смыслу задачи подходит паттерн Посетитель (другое название – двойная диспетчеризация).

Пример реализации для вашего кода:

public interface IVisitor
{
    void Visit(Dog dog);
    void Visit(Cat cat);
}

public abstract class Animal
{
    public abstract void Accept(IVisitor visitor);
}

public class Dog : Animal
{
    //...

    public override void Accept(IVisitor visitor)
        => visitor.Visit(this);
}

public class Cat : Animal
{
    //...

    public override void Accept(IVisitor visitor)
        => visitor.Visit(this);
}

public class Repository : IVisitor
{
    public void Create(IEnumerable<Animal> animals)
    {
        foreach (var animal in animals)
            animal.Accept(this);
    }

    public void Create(Cat cat)
    {

    }

    public void Create(Dog dog)
    {

    }

    public void Visit(Dog dog) => Create(dog);

    public void Visit(Cat cat) => Create(cat);
}

Название методов Visit в интерфейсе можно заменить на Create, чтобы не дублировать методы в классе Repository.

Шаблон хорош тем, что с одной стороны вы соблюдаете все принципы ООП и SOLID, с другой стороны вам для этого нужно только единожды внести небольшие правки в классы-наследники Animal – а именно реализовать абстрактный метод void Accept(IVisitor visitor), причем стандартной однострочной реализацией. После этого вы можете создавать любых "посетителей", реализуя интерфейс IVisitor, – например, менять репозитории на произвольные, либо вообще реализовывать произвольную логику для объектов Animal без внесения изменений в классы-наследники.

→ Ссылка