dartpolymorphismcrudrepository-patternclean-architecture

Polymorphism & Clean Architecture


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

Solution

  • In case of polymorphism, does clean architecture expect to consider each implementation as an entity with its own repository?

    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 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?

    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 all Animals equal and it might be a good idea to separate the different logic.

    In other words. If you do someting like this:

    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)
       ...
    }
    

    you introduce dependencies to the subtypes of Animal in a class that was intended to be generic.

    From Robert Martin I heard that polymorphism should aim at decreasing such conditions.

    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.

    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 Animal class 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

    Let's assume we have a page in the UI that allows a user to see all the pets generic info that all pets have (for instance Name, Owner, age, n of visits and health status) and in the same page there are actions that are dependent on the animal type (for instance "Delete" or "Report as Dead", as Dog, Cat, Snake data are stored in different tables). How would you proceed?

    Your initial thought is good. But I would separate the logic that selects a repository by type from the repository methods.

    class AnimalRepisitoryProvider {
    
       AnimalRepository getByType(AnimalType animaltype) {
           AnimalRepository repo = ... // Either if/else, switch or a use map
          return repo;
       }
    }
    

    This would prevent type selection logic duplication and the use case could you it like this:

    Future<void> saveAnimal(Animal animal) async{
       AnimalRepositoryProvider provider = ....
       AnimalRepository repo = provider.getByType(animal.type);
       repo.save(animal)
    }
    

    If you have a method like findAll you can iterate the animal types to get each repository from the provider.

    What I thought could be done is to implement a method that gets all animals, Future getsAll() , show in the UI the generic animal info that are common to all animals, but then the actions to be triggered in the UI need to be specific for the animal type, so the conditionals statements to check the animal type and to trigger the according action would be here for every single action. Is that normal or is there a better way to handle the polymorphism situation? Thanks again and have a great weekend!

    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.