Сохранение обобщенной коллекции
Имеются объекты:
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) и обойтись без:
- downcast
- внедрения логики сохранения модели в саму модель
Если нельзя, то какой способ самый правильный или общепринятый? (вроде бы так ставить вопрос нельзя, но хотелось бы перенять опыт)
Решения, которые я придумал, но они не подходят под эти 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 шт):
Вероятно, все объекты стоит сохранять в базу одинаково с помощью, например, сериализации.
Если нет, абстрагируясь от деталей сохранения объектов, по смыслу задачи подходит паттерн Посетитель (другое название – двойная диспетчеризация).
Пример реализации для вашего кода:
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 без внесения изменений в классы-наследники.