简体   繁体   中英

How do I organize C# classes that inherit from one another, but also have properties that inherit from one another?

I have an application that has a concept of a Venue , a place where events happen. A Venue has many VenuePart s. So, it looks like this:

public abstract class Venue
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<VenuePart> VenueParts { get; set; }
}

A Venue can be a GolfCourseVenue , which is a Venue that has a Slope and a specific kind of VenuePart called a HoleVenuePart :

public class GolfCourseVenue : Venue
{
    public string Slope { get; set; }
    public virtual ICollection<HoleVenuePart> Holes { get; set; }
}

In the future, there may also be other kinds of Venue s that all inherit from Venue . They might add their own fields, and will always have VenuePart s of their own specific type.

Here are the VenuePart classes:

public abstract class VenuePart
{
    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string NameDescriptor { get; }
}

public class HoleVenuePart : VenuePart
{
    public override string NameDescriptor { get { return "Hole"; } }
    public int Yardage { get; set; }
}

My declarations above seem wrong, because now I have a GolfCourseVenue with two collections, when really it should just have the one. I can't override it, because the type is different, right? When I run reports, I would like to refer to the classes generically, where I just spit out Venue s and VenuePart s. But, when I render forms and such, I would like to be specific.

I have a lot of relationships like this and am wondering what I am doing wrong. For example, I have an Order that has OrderItem s, but also specific kinds of Order s that have specific kinds of OrderItem s.

Update: I should note that these classes are Entity Framework Code-First entities. I was hoping this wouldn't matter, but I guess it might. I need to structure the classes in a way that Code-First can properly create tables. It doesn't look like Code-First can handle generics. Sorry this implementation detail is getting in the way of an elegant solution :/

Update 2: Someone linked to a search that pointed at Covariance and Contravariance , which seemed to be a way to constrain lists within subtypes to be of a given subtype themselves. That seems really promising, but the person deleted their answer! Does anyone have any information on how I may leverage these concepts?

Update 3: Removed the navigation properties that were in child objects, because it was confusing people and not helping to describe the problem.

Here's one possible option using generics:

public abstract class VenuePart
{
    public abstract string NameDescriptor { get; }
}

public class HoleVenuePart : VenuePart
{
    public string NameDescriptor { get{return "I'm a hole venue"; } }
}

public class Venue<T> where T : VenuePart
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Company Company { get; set; }
    public virtual ICollection<T> VenueParts { get; set; }
}

public class GolfCourseVenue : Venue<HoleVenuePart>
{
}

Here GolfCourseVenue has the collection VenueParts, which can contain HoleVenueParts or super classes HoleVenueParts. Other specializations of Venue would restrict VenueParts to containing VenueParts specific to that venue.

A second possibility is pretty much as you had it

public abstract class VenuePart
{
    public abstract string NameDescriptor { get; }
}

public class HoleVenuePart : VenuePart
{
    public string NameDescriptor { get{return "I'm a hole venue"; } }
}

public class Venue 
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Company Company { get; set; }
    public virtual ICollection<VenuePart> VenueParts { get; set; }
}

public class GolfCourseVenue : Venue
{
}

Now GolfCourseVenue has the collection VenueParts, which can contain VenueParts or super classes VenueParts. Here all specializations of Venue can contain any type of VenuePart which may or may not be appropriate.

In answer to your comment about covariance, I would propose something like this:

public abstract class VenuePart
{
    public abstract string NameDescriptor { get; }
}

public class HoleVenuePart : VenuePart
{
    public override string NameDescriptor { get{return "I'm a hole venue"; } }
}

public abstract class Venue 
{
    public int Id { get; set; }
    public string Name { get; set; }
    public abstract ICollection<VenuePart> VenueParts { get; }
}

public class GolfCourseVenue : Venue
{
    private ICollection<HoleVenuePart> _holeVenueParts;

    public GolfCourseVenue(ICollection<HoleVenuePart> parts)
    {
       _holeVenueParts = parts;
    }

    public override ICollection<VenuePart> VenueParts 
    { 
        get
        { 
            // Here we need to prevent clients adding
            // new VenuePart to the VenueParts collection. 
            // They have to use Add(HoleVenuePart part).
            // Unfortunately only interfaces are covariant not types.
            return new ReadOnlyCollection<VenuePart>(
                   _holeVenueParts.OfType<VenuePart>().ToList()); 
        } 
    }

    public void Add(HoleVenuePart part) { _holeVenueParts.Add(part); }
}

You can use Covariance

public abstract class Venue
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Company Company { get; set; }
    public virtual IEnumerable<VenuePart> VenueParts { get; set; }
}

public class GolfCourseVenue : Venue
{
    public string Slope { get; set; }

    public  GolfCourseVenue()
    {
        List<HoleVenuePart> HoleVenueParts = new List<HoleVenuePart>();
        HoleVenueParts.Add(new HoleVenuePart());
        VenueParts = HoleVenueParts;
    }
}

Assuming HoleVenuePart is inherited from VenuePart

I look forward to the advice of others - but my approach is to use generics in this case. With generics, your GolfCourseVenue 's "parts" are strong typed!

...and as I type this everyone else is saying generics too. HOW DO YOU overstackers type so dang fast?!

Anyways, pretending I'm still first -

public class VenuePart
{
}

public class HoleVenuePart : VenuePart
{
}

public abstract class Venue<T> where T : VenuePart
{
  public int Id { get; set; }
  public string Name { get; set; }
  public virtual Company Company { get; set; }
  public virtual ICollection<T> Parts { get; set; }
}

public class GolfCourseVenue : Venue<HoleVenuePart>
{
  public string Slope { get; set; }
}

Also, as a 2nd option, you could use an interface too, so in case you didn't like the name Parts , you could call it Holes when the derived type is known to be a GolfCourse

public class VenuePart
{
}

public class HoleVenuePart : VenuePart
{
}

public interface IPartCollection<T> where T : VenuePart
{
  ICollection<T> Parts { get; set; }
}

public abstract class Venue<T> : IPartCollection<T> where T : VenuePart
{
  public int Id { get; set; }
  public string Name { get; set; }
  public virtual Company Company { get; set; }
  public virtual ICollection<T> Parts { get; set; }
}

public class GolfCourseVenue : Venue<HoleVenuePart>
{
  public string Slope { get; set; }
  ICollection<HoleVenuePart> IPartCollection<HoleVenuePart>.Parts { get { return base.Parts; } set { base.Parts = value; }}

  public virtual ICollection<HoleVenuePart> Holes { get { return base.Parts; } set { base.Parts = value;}}
}

If you remove "set" portions of both collections than it will make more sense: base class provides "all parts" collection, while derived classes have filtered view in addition to base class one.

Note: Depending on your needs making GolfVenue to be specialization generic of Venue<VenuePart> may not work as Venue<Type1> and Venue<Type2> will not have any good base class to work with.

Consider using interfaces instead of base classes as it would allow more flexibility in implementation.

public interface IVenue 
{ 
    public int Id { get; } 
    public string Name { get; } 
    public virtual IEnumerabe<VenuePart> VenueParts { get; } 
} 

public interface IGolfCourse : IVenue
{ 
    public virtual IEnumerabe<HoleVenuePart> Holes { get; } 
} 

Now you can use GolfCourse:Venue from other samples but since it implements interface you can handle it in gnereic way too:

class GolfCourse:Venue<HoleVenuePart>, IGolfCourse  {
  public virtual IEnumerabe<VenuePart> Holes{ get 
    { 
      return VenueParts.OfType<HoleVenuePart>(); 
    }
  } 
}
class OtherPlace:Venue<VenuePart>, IVenue {...}

List<IVenue> = new List<IVenue> { new GolfCourse(), new OtherPlace() };

Nothe that GolfCourse and OtherPlace don't have common parent class (except object), so without interface you can't use them interchangebly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM