For me, this makes sense as it gives a concrete name to the class, instead of relying on the generic NamedEntity. On the other hand, there is a number of such classes that simply have no additional properties.
Are there any downsides to this approach?
The approach isn't bad, but there are better solutions available. In short, an interface would be a much better solution for this. The main reason why interfaces and inheritance are different here is because you can only inherit from one class, but you can implement many interfaces.
For example, consider that you have named entities, and audited entities. You have several entities:
One
is not an audited entity nor a named entity. That's simple:
public class One
{ }
Two
is a named entity but not an audited entity. That's essentially what you have now:
public class NamedEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Two : NamedEntity
{ }
Three
is both a named and audited entry. This is where you run into a problem. You can create an AuditedEntity
base class, but you can't make Three
inherit both AuditedEntity
and NamedEntity
:
public class AuditedEntity
{
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
public class Three : NamedEntity, AuditedEntity // <-- Compiler error!
{ }
However, you might think of a workaround by having AuditedEntity
inherit from NamedEntity
. This is a clever hack to ensure that every class only needs to inherit (directly) from one other class.
public class AuditedEntity : NamedEntity
{
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
public class Three : AuditedEntity
{ }
This still works. But what you've done here is stated that every audited entity is inherently also a named entity. Which brings me to my last example. Four
is an audited entity but not a named entity. But you can't let Four
inherit from AuditedEntity
as you would then also be making it a NamedEntity
due to the inheritance between AuditedEntityand
NamedEntity`.
Using inheritance, there is no way to make both Three
and Four
work unless you start duplicating classes (which opens up a whole new set of problems).
Using interfaces, this can easily be achieved:
public interface INamedEntity
{
int Id { get; set; }
string Name { get; set; }
}
public interface IAuditedEntity
{
DateTime CreatedOn { get; set; }
DateTime UpdatedOn { get; set; }
}
public class One
{ }
public class Two : INamedEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Three : INamedEntity, IAuditedEntity
{
public int Id { get; set; }
public string Name { get; set; }
DateTime CreatedOn { get; set; }
DateTime UpdatedOn { get; set; }
}
public class Four : IAuditedEntity
{
DateTime CreatedOn { get; set; }
DateTime UpdatedOn { get; set; }
}
The only minor drawback here is that you still have to implement the interface. But you get all the benefits from having a common reusable type, without any of the drawbacks that emerge when you need variations on multiple common types for a given entity.
But your polymorphism remains intact:
var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();
public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}
HandleNamedEntity(one); //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);
HandleNamedEntity(four); //Error - not a named entity
HandleAuditedEntity(one); //Error - not an audited entity
HandleAuditedEntity(two); //Error - not an audited entity
HandleAuditedEntity(three);
HandleAuditedEntity(four);
On the other hand, there is a number of such classes that simply have no additional properties.
This is a variation on the marker interface pattern, where you implement an empty interface purely to be able to use the interface type to check if a given class is "marked" with this interface.
You're using inherited classes instead of implemented interfaces, but the goal is the same, so I'm going to refer to it as a "marked class".
At face value, there's nothing wrong with marker interfaces/classes. They are syntactically and technically valid, and there are no inherent drawbacks to using them provided that the marker is universally true (at compile time) and not conditional.
This is exactly how you should differentiate between different exceptions, even when those exceptions do not actually have any additional properties/methods compared to the base method.
So there's nothing inherently wrong with doing so, but I would advise using this cautiously, making sure that you're not just trying to cover up an existing architectural mistake with badly designed polymorphism.