I am relatively new to clean architecture and I came across an existential doubt.
In case of polymorphism, does clean architecture expect to consider each implementation as an entity with its own repository?
Let's assume we are developing an app storing users animals data for veterinary purposes. We have an abstract class Pet, and then the different implementations as Dog, Cat, Snake and so on. Each implementation has its own peculiarity which forces us to store them separately in the DB.
Should I consider each pet an entity(Dog, Cat, Snake ...) and implement a repository for each? If so, when I want to do a CRUD operation on a generic animal, there would be a huge list of conditions depending on the animal type to see which repository to use. Is that normal?
DISCLAIMER: below examples are written in Dart
Ex:
Future<void> saveAnimal(Animal animal)async{
if(animal is Dog)
await _dogRepository.save(animal);
if(animal is Cat)
await _catRepository.save(animal);
if(animal is Snake)
await _snakeRepository.save(animal)
...
}
From Robert Martin I heard that polymorphism should aim at decreasing such conditions. Source: https://blog.cleancoder.com/uncle-bob/2021/03/06/ifElseSwitch.html
I thought about adding a method inside Animal as save, but that would be depending on the repository and because of the dependency inversion rule entities can't depend on repository.
Ex:
abstract class Animal {
Future<void> save();
}
class Dog{
String name;
...
@override
Future<void> save() async {
final repository = DogRepository();
await repository.save(this);
}
No, you can design your repositories as you like. But it would be a good idea to apply the interface segregation principle and design one repository interface for each use case.
If there is a huge list of conditions they are not the same and can not be handled in a generic way. E.g. if you have a a method
persist(Animal)and you do a lot of if else or switch statements in the implementation, then obviously you can not treat allAnimals equal and it might be a good idea to separate the different logic.In other words. If you do someting like this:
you introduce dependencies to the subtypes of
Animalin a class that was intended to be generic.Both statements are true. So if it's true that you should use polymorphism and if it's true that moving the method to the
Animalclass violates the architecture, then the original idea of a generic save method might not be a good idea.Sometimes we try to treat different things the same way in order to write less code. It's generally a good idea to write less code, but not if it violates our principles and architecture.
One might think "But there are frameworks that provide generic CRUD operations". Yes, but these freameworks still don't know anything about the concrete types in their code. You usually provide the mapping information at runtime as other data structures. Often as metadata that is placed on the "database entities". These "database entitities" differ from the entities that Uncle Bob talks about in the clean architecture.
This examaple might also show the effort you have to do if you want to make something generic and still don't depend on specific things. The generic approach is not always a good idea. Especially not if you sacrifice your principles and architecture.
EDIT
Your initial thought is good. But I would separate the logic that selects a repository by type from the repository methods.
This would prevent type selection logic duplication and the use case could you it like this:
If you have a method like
findAllyou can iterate the animal types to get each repository from the provider.Sometimes you have lists in the ui that contain very different elements. As a result the actions you can do with them are different. In more detail... the use cases you can invoke are different. So each action will lead to another use case invocation. Other use cases can handle other types and use other repositories so there is usually no need to switch by types. But if I have to switch by type I usually separate that logic from the other. And I first try to use map data structures instead of if/else or switch statements, because that reduced the amount of branches.