Ahmad
Ahmad

Reputation: 12737

How can I write a class that is both inheritable and generic?

I have several classes in my project that are mapped to table rows in a database. Most of the classes share a lot of the boiler plate code to fetch data from database FetchAll, and to return certain object/row based on a given id FetchById().

I am thinking of writing a single class that can perform FetchAll and FetchById() and then all of my other classes can inherit from this object. However, I am struggling to define the base class to be generic. Other classes can't inherit of it and at the same time be of the generic type associated with what they are inheriting from.

To better explain my question, I will start simplifying two of my common classes that share similar code:

Person.cs

class Person {
   public int Id { get; set; }
   public string Name { get; set; }
   
   public static List<Person> All {
      get { /* fetch all rows and */ return List<Person>; }
   }
   public static Person FetchById(int id) {
      return All.Where(p => p.Id == id).SingleOrDefault();
   }
}

Department.cs

class Department {
   public int Id { get; set; }
   public string Name { get; set; }
   public string CostCenter { get; set; }

   public static List<Department> All { get { /* same logic as with Person */ } }
   public static Department FetchById(int id) { /* same logic as with Person */ }
}

Now here is what I tried to do to reduce code duplication:

IIdentifiable.cs

public interface IIdentifiable {
   // make sure all objects have an Id property
   int Id { get; set; }
}

DbObject.cs

class DbObject<T> where T: IIdentifiable {
    public static List<T> All { get {  /* return List<T>; */ } }
    public static T FetchById(int id) {
       return All.Where(object => object.Id == id).FirstOrDefault();
    }
}

Now I went back to my Person and Department classes to take advantage of DbObject:

Person.cs (modified)

class Person : DbObject<Person>, IIdentifiable // won't compile: The type cannot be used as type parameter T
{
    public int Id { get; set; }    
    public string Name { get; set; }

   // public static List<Person> All {get{ ... }};    // should be inherited
   // public static Person FetchById(int id) { ... }  // should be inherited
}

I did the same with Department. You get the idea. I feel I am doing something wrong. I know it should be much simpler than this. How can I make DbObject generic and inheritable at the same time?

Upvotes: 1

Views: 65

Answers (2)

Enigmativity
Enigmativity

Reputation: 117174

You can do this by adopting the Curiously recurring template pattern.

Effectively you define DbObject<T> such that T : DbObject<T> then it works.

public class DbObject<T> where T : DbObject<T>, IIdentifiable
{
    public List<T> All { get { return new List<T>(); } }
    public T FetchById(int id)
    {
        return All.Where(x => x.Id == id).FirstOrDefault();
    }
}

public class Person : DbObject<Person>, IIdentifiable
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Now this code works:

Person p = new Person();

List<Person> all = p.All;

In your original code you were using static methods. I assume that is so you could write List<Person> all = Person.All;, but you can't inherit static methods.

You might want to consider making a repository object that is responsible for returning the people.


Do keep in mind that the downside to this approach is that you need to be careful. This code compiles, but would probably not be what you want:

public class Department : DbObject<Person>, IIdentifiable
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Upvotes: 5

Jeremy Lakeman
Jeremy Lakeman

Reputation: 11173

You can use recursive generics and static methods.

public abstract class Identified<T> where T : Identified<T>{
    public static List<T> GetAll(){...}
}
public class Person:Identified<Person>{...}
var allPeople = Person.GetAll();

You shouldn't need to redefine GetAll in each subtype. But I would recommend that FetchById performs a separate query. Do you really want to load the entire table to view one record?

But I would actually recommend a separate generic factory class. It's difficult to configure and test static methods.

Upvotes: 0

Related Questions