Search code examples
c#classinheritancestyles

How do I avoid code repetition when defining a class of functions that only vary the data type of the parameters they handle?


I have a program that performs database operations using SQLite.SQLiteAsyncConnection. I have two different models I'd like to work with, say Customers and Cars. Customers could be defined as

public class Customer
{
    [PrimaryKey]
    public string Id {get; set;}
    public string Name {get; set;}
    public int NumberOfKids {get; set;}
    public decimal Budget {get; set;}
}

and Cars could be defined as

public class Car
{
     [PrimaryKey]
     public string Id {get; set;}
     public int Year {get; set;}
     public string Make {get; set;}
     public string Model {get; set;}
     public int Mileage {get; set;}
 }

Note that the models have different numbers and types of members, but both use a string Id as the primary key. Database operations are defined in a class that I inherit from a public interface IDataStore which declares operations for adding, updating, deleting, and getting items:

public interface IDataStore<T>
{
    Task<bool> AddItemAsync(T item);
    Task<bool> UpdateItemAsync(T item);
    Task<bool> DeleteItemAsync(T item);
    Task<T> GetItemAsync(string id);
    Task<IEnumerable<T>> GetItemsAsync(bool forceRefresh = false);
}

Do I have to define a separate DataStore class, or separate overloaded functions, for Cars and Customers, even though these operations are exactly the same for both models except for the data types they take and use as parameters? What I'd like to be able to declare is something like

public class SQLiteItemDataStore : IDataStore<var>
{
   ...
   public async Task<var> GetItemAsync(string Id)
        {
            return await DrinkDatabase.Table<var>().Where(i => i.Id == id).FirstOrDefaultAsync();
        }
   ...
}

But of course this creates a compiler error since you can only use "var" in a variable declaration, so I instead have to create one class for Cars

public class SQLiteItemDataStore : IDataStore<Car>
{
   ...
   public async Task<Car> GetItemAsync(string Id)
        {
            return await DrinkDatabase.Table<Car>().Where(i => i.Id == id).FirstOrDefaultAsync();
        }
   ...
}

and another for Customers

public class SQLiteItemDataStore : IDataStore<Customer>
{
   ...
   public async Task<Customer> GetItemAsync(string Id)
        {
            return await DrinkDatabase.Table<Customer>().Where(i => i.Id == id).FirstOrDefaultAsync();
        }
   ...
}

Is there some standard way to avoid having to repeat the database operation functions for every model type I want to use with a database?


Solution

  • I think you are looking for the following solution:

    public interface IDataStore
    {
        Task<bool> AddItemAsync<T>(T item);
        Task<bool> UpdateItemAsync<T>(T item);
        Task<bool> DeleteItemAsync<T>(T item);
        Task<T> GetItemAsync(string id);
        Task<IEnumerable<T>> GetItemsAsync(bool forceRefresh = false);
    }
    
    public class SQLiteDataStore
    {
    ...
        public async Task<T> GetItemAsync<T>(string Id)
        {
            return await DrinkDatabase.Table<T>().Where(i => i.Id == id).FirstOrDefaultAsync();
        }
    ...
    }
    

    but this is not the commonly used approach, moreover it won't work because you can't know that T has Id property.

    Try this:

    • you should define a base entity class and let all your model inherit from it

      public abstract class Entity {
         public abstract string Id {get;set;}
      }
      
      public class Car : Entity 
      {
         //all others cars properties
      }
      public class Customer : Entity 
      {
         //all others customers properties
      }
      
    • you should define a common generic repository interface (like you did with IDataStore< T >)

      public interface IRepository<T> where T : Entity // T must inherit from Entity
      {
          //all the methods you defined in IDataStore<T>
      }
      
    • create an abstract class which implements all the methods of IRepository

      public abstract class RepositoryBase<T> : IRepository<T> where T : Entity
      {
        //all the methods of IRepository<T> are implemented in this class
        //now you don't have problems when calling t.Id if t is of type T because compiler knows that T derives from Entity
      }
      
    • create the specific interfaces for your entities

      public interface ICarRepository : IRepository<Car>
      {
          //add car specific methods, i.e. IQueryable<Car> GetCarByCustomer(string customerId);
      }
      
      public interface ICustomerRepository : IRepository<Customer>
      {
          //add customer specific methods, i.e. Task<Customer> GetCustomerByCar(string carId);
      }
      
    • Create the concrete classes

      public class CarRepository : RepositoryBase<Car>, ICarRepository
      {
           //here you have to implement only car specific methods, i.e. IQueryable<Car> GetCarByCustomer(string customerId);
      }
      

    this is the most used approach, aka Repository Pattern