Search code examples
oopgenericsinheritancepolymorphismrepository-pattern

repository pattern : generics vs polymorphism way of implementation


a generic repository interface looks something like below :

 public interface IRepository<T> where T : Entity
{
    T Get(int key);

    IQueryable<T> Get();

    void Save(T obj);

    void Delete(T obj);
}

Another way to achieve the similar functionality is using polymorphism as shown below

public interface IRepository
{
    Entity Get(int key);

    IQueryable<Entity> Get();

    void Save(Entity obj);

    void Delete(Entity obj);
}

In general my question would be in what scenarios or use cases we should use generics? Can generics be avoided completely if we use polymorphism. Or I am completely making non sense here and these 2 are completely unrelated.


Solution

  • The most crucial difference between your first and second example is called type safety.

    I'm assuming that you are asking the question from the point of view of a statically-typed language like C# or Java.

    In the version that uses generics, your compiler makes sure you always work with suitable types. In contrast, in the second version (the one that uses a more general type), you’d be expected to check that manually. Also, the compiler will constantly force you to cast your general type (e.g., Entity) to a more specific one (e.g., Customer) to use the services the object provides.

    In other words, you have to fight the compiler all the time since it will consistently require that we cast types for it to be able to validate the code we write.

    With Generics

    The first example uses a type variable T at the interface level. It means that when you define a variable for this interface type (or implement it), you will also be forced to provide a type argument for T.

    For example

    IRepository<Customer> custRepo = ...;
    

    This means that wherever T appears, it must be replaced by a Customer, right?

    Once you define the type argument for T to be Customer, it is as if your interface declaration would change, in the eyes of the compiler, to be something like this:

    public interface IRepository
    {
        Customer Get(int key);
        IQueryable<Customer> Get();
        void Save(Customer obj);
        void Delete(Customer obj);
    }
    

    Therefore, when you use it, you would be forced by the compiler to respect your type argument:

    Customer customer = custRepo.Get(10);
    customer.setEmail("skywalker@gmail.com");
    custRepo.Save(customer);
    

    In the example above, all repository methods work only with the Customer type. Therefore, I cannot pass an incorrect type (e.g., Employee) to the methods since the compiler will enforce type safety everywhere. For example, contrast it with this other example where the compiler catches a type error:

    Employee employee = empRepo.Get(10);
    custRepo.Save(employee); //Uh oh! Compiler error here
    

    Without Generics

    On the other hand, in your second example, all you have done is decide that you’ll use a more generic type. By doing that, you sacrifice type safety in exchange for some flexibility:

    For example:

    IRepository custRepo = ...;
    Entity customer = custRepo.Get(10); 
    ((Customer) customer).setEmail("skywalker@gmail.com"); //Now you'll need casting
    custRepo.Save(customer);
    

    In the case above, you're forced to always cast your Entity to a more usable type like Customer to use what it offers. This casting is an unsafe operation and can potentially introduce bugs (if we ever make a mistake in our assumptions about the types we're using).

    Also, the repository types do not prevent you from passing the wrong types to the methods, so you could make semantic mistakes:

    Entity employee = (Employee) empRepo.Get(10);
    custRepo.Save(employee); //Uh Oh! 
    

    If you do it this way, you will probably have to make sure in your CustomerRepo that your entity is that of a Customer to prevent a mistake like the one in the last line of the example above.

    In other words, you would be manually implementing the kind of type safety that the compiler gives you automatically when you use generics.

    This is starting to sound like we are trying to use our statically-typed language as if it was a dynamically-typed one, right? That's why we have to fight the compiler all the way to enforce this style of programming.

    About Parametric Polymorphism

    If you want, you could explore the idea that generics is also a form of polymorphism known as parametric polymorphism. I think reading this another answer could help as well. In it, there's a citation to an excellent paper on polymorphism that might help you broaden your understanding of the topic beyond just class and interface inheritance.

    Dynamically-Type Languages vs. Statically-Typed Languages Debate

    An exciting conclusion that might help you explore this further is that dynamic languages (e.g., JavaScript, Python, Ruby, etc.), where you don't have to make explicit type declarations, actually work like your generic-free example.

    Dynamic languages work as if all your variables were of type Object, and they allow you to do anything you want with that object to avoid continually casting your objects to different types. It is the programmer's responsibility to write tests to ensure the object is always used appropriately.

    There has always been a debate between defenders of statically-typed languages and those that prefer dynamically-typed languages. This is what we typically call a holy war.

    I believe that this is a topic you may want to explore more deeply to understand the fundamental differences between your two examples and to learn how static typing and type safety from generics compares to the high flexibility of just using dynamic types.

    I recommend reading an excellent paper called Dynamically–Typed Languages by Laurence Tratt from Bournemouth University.

    Now, in statically-typed languages like C#, or Java, we are typically expected to exploit type safety as best as we can. But nothing prevents us from writing the code as you would do in a dynamic language; it is just that the compiler will constantly fight us. That’s the case with your second example.

    If that's a way of programming that resonates more with you and your work style, or if it offers you the flexibility you seek, then perhaps you should consider using a dynamically-type language. Maybe one that can be combined on top of your current platform (e.g., IronRuby or IronPython) such that you can also reuse pre-existing code from your current statically-typed language.