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?
L'approccio non è male, ma ci sono soluzioni migliori disponibili. In breve, un'interfaccia sarebbe una soluzione molto migliore per questo. Il motivo principale per cui le interfacce e l'ereditarietà sono diverse qui è perché puoi ereditare solo da una classe, ma puoi implementare molte interfacce .
Ad esempio, considera di avere un nome entità e entità controllate. Hai diverse entità:
One
non è un'entità sottoposta a revisione né un'entità denominata. È semplice:
public class One
{ }
Two
è un'entità denominata ma non un'entità controllata. Questo è essenzialmente ciò che hai ora:
public class NamedEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Two : NamedEntity
{ }
Three
è sia una voce con nome che controllata. Questo è dove si incontra un problema. Puoi creare una classe base AuditedEntity
, ma non puoi rendere Three
ereditare entrambi AuditedEntity
e NamedEntity
:
public class AuditedEntity
{
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
public class Three : NamedEntity, AuditedEntity // <-- Compiler error!
{ }
Tuttavia, potresti pensare a una soluzione alternativa se AuditedEntity
eredita da NamedEntity
. Questo è un trucco intelligente per garantire che ogni classe debba solo ereditare (direttamente) da un'altra classe.
public class AuditedEntity : NamedEntity
{
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
public class Three : AuditedEntity
{ }
Funziona ancora. Ma quello che hai fatto qui è che ogni entità sottoposta a verifica è intrinsecamente anche un'entità denominata . Il che mi porta al mio ultimo esempio. Four
è un'entità sottoposta a revisione ma non un'entità denominata. Ma non puoi lasciare che Four
erediti da AuditedEntity
, altrimenti dovresti renderlo un NamedEntity
a causa dell'ereditarietà di AuditedEntity and
NamedEntity '.
Utilizzando l'ereditarietà, non c'è modo di far funzionare sia Three
che Four
a meno che non inizi a duplicare le classi (che apre una serie di problemi completamente nuova).
Utilizzando le interfacce, questo può essere facilmente raggiunto:
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; }
}
L'unico piccolo inconveniente qui è che devi ancora implementare l'interfaccia. Ma ottieni tutti i benefici dall'avere un tipo riutilizzabile comune, senza nessuno degli svantaggi che emergono quando hai bisogno di variazioni su più tipi comuni per una determinata entità.
Ma il tuo polimorfismo rimane intatto:
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.
Questa è una variante del modello di interfaccia marker , in cui si implementa un'interfaccia vuota puramente per poter utilizzare il tipo di interfaccia per verificare se una determinata classe è "contrassegnata" con questa interfaccia.
Stai utilizzando classi ereditate invece di interfacce implementate, ma l'obiettivo è lo stesso, quindi mi riferirò ad esso come una "classe contrassegnata".
Al valore nominale, non c'è niente di sbagliato nelle interfacce / classi di marcatori. Sono sintatticamente e tecnicamente validi e non ci sono inconvenienti intrinseci nell'usarli a condizione che l'indicatore sia universalmente vero (in fase di compilazione) e non condizionale .
Questo è esattamente il modo in cui devi differenziare tra le diverse eccezioni, anche quando quelle eccezioni non hanno effettivamente proprietà / metodi aggiuntivi rispetto al metodo base.
Quindi non c'è nulla di intrinsecamente sbagliato nel farlo, ma ti consiglierei di usare questo cauto, assicurandoti che non stai solo cercando di coprire un errore architettonico esistente con un polimorfismo mal progettato.