Supondo que eu tenha as seguintes interfaces:
public interface IVehicle
{
ITrackInfo TrackInfo { get; set; }
void ShowInfo();
}
public interface ITrackInfo
{
int Length { get; set; }
}
Agora eu tenho essas classes implementandoITrackInfo
public class RailInfo : ITrackInfo
{
int Length { get; set;}
int SleeperSpacing { get; set; }
}
public class CanalInfo : ITrackInfo
{
int Length { get; set; }
decimal AverageDepth { get; set; }
}
public class RoadInfo : ITrackInfo
{
int Length { get; set; }
Color Colour { get; set; }
}
Mas com as classes que implementam IVehicle
, quero que elas tenham uma implementação específica de ITrackInfo
.
public class Train : IVehicle
{
RailInfo TrackInfo { get; set; }
public void ShowInfo() {
Console.WriteLine("Railway is {0}km long". TrackInfo.Length.ToString());
Console.WriteLine("Distance between sleepers is {0}cm", TrackInfo.SleeperSpacing);
}
}
public class Boat : IVehicle
{
CanalInfo TrackInfo { get; set; }
public void ShowInfo() {
Console.WriteLine("Canal is {0}km long". TrackInfo.Length.ToString());
Console.WriteLine("The average depth is {0}m", TrackInfo.AverageDepth.ToString("F2"));
}
}
public class Bus : IVehicle
{
RoadInfo TrackInfo { get; set; }
public void ShowInfo() {
Console.WriteLine("Road is {0}km long". TrackInfo.Length.ToString());
Console.WriteLine("The road is coloured {0}", TrackInfo.Colour.ToString());
}
}
Obviamente não posso escrever como mostrado acima - isso produz um erro de compilação nas IVehicle
implementações, pois a TrackInfo
propriedade deve ser especificada como ITrackInfo
.
Existe uma maneira de forçar implementações específicas ITrackInfo
para implementações específicas de IVehicle
?
Espero algo um pouco mais elegante do que apenas fazer um check-in ShowInfo()
:
var info = TrackInfo as RailInfo;
if (info == null)
{
throw new Exception("Incorrect track info");
}
Sim, use genéricos como este:
Aqui você está dizendo que
TrackInfo
o tipo será uma implementação concreta doITrackInfo
.Com isso em mãos, as
IVehicle
implementações podem ser corrigidas:Link Dotnet Fiddle: https://dotnetfiddle.net/GAb9k9
A solução direta é apenas usar implementação de interface explícita , o que permite que um
class
com membros com nomes conflitantes implemente totalmente uminterface
.... mas há alguns problemas aqui:
Boat.TrackInfo
eBoat.IVehicle.TrackInfo
são propriedades totalmente separadas. Dentro deBoat
, using semprethis.TrackInfo
retornará a propriedade, mas qualquer consumidor que a acessar pela interface terá acesso apenas à propriedade -typed, que é distinta.CanalInfo
Boat
IVehicle
ITrackInfo
Como ambas as propriedades
CanalInfo
andITrackInfo
possuemset
acessadores, isso significa que (na ausência de documentação de esclarecimento) que aBoat
classe deve aceitar qualquer implementaçãoITrackInfo
para oITrackInfo IVehicle.TrackInfo
setter - e isso colocaria oBoat
objeto em um estado estranho, porque então eleTrackInfo
poderia retornar aRoadInfo
.ou seja
Uma solução possível para o problema acima é criar
IVehicle
uma interface somente leitura, assimIVehicle.TrackInfo
comoget
-only, o que significa que ela sempre pode encaminhar com segurança paraBoat.TrackInfo
, assim:(Além disso, desde que você esteja usando C# 8 (ou é 9?), você também pode aproveitar as vantagens dos tipos de retorno covariantes )
Pode ser útil criar
IReadOnlyVehicle
uma interface genérica porque você pode aproveitar outros tipos de variação suportados pelo .NET:Claro, só
Boat
tem o criador da propriedade.Abrace a Imutabilidade
Depois de projetar seus tipos (
class
,interface
,delegate
, etc) como estruturas de dados imutáveis, ou pelo menos, preferindo expor apenas interfaces ou visualizações somente leitura sobre eles, então isso (um tanto paradoxalmente) elimina muitos dos problemas não ergonômicos inerentes ao OOP (desculpe o trocadilho), como ser capaz de prever (ou afirmar) corretamente o estado de um objeto em qualquer ponto do seu programa ou sistema de software (por exemplo, supondo que você tenha umProduct
objeto totalmente mutável sendo passado como um argumento de parâmetro de método; será muito difícil provar que o estado desseProduct
objeto está em um estado válido sempre que for consumido por seus métodos (por exemplo, ifmyProduct.ProductId
é um número válido ou still0
, ou se algumaString Name { get; set; }
propriedade estiver definida comonull
, uma string vazia""
, ou algo realmente significativo no domínio do seu problema.E eles são mais fáceis de serem repassados livremente a outros consumidores que podem não estar cientes de seus subtipos reais e só ter acesso a supertipos; por exemplo, um tipo somente leitura
IFace<out T>
sempre pode ser convertido com segurança em um supertipo (por exemplo,IReadOnlyList<String>
pode ser armazenado em um slot digitado comoIReadOnlyList<Object>
e/ouIEnumerable<Object>
) - mas o mesmo não pode ser dito de um mutávelList<String>
que não pode ser usado com segurança como se fosse aList<Object>
.Além disso, leia isto: https://boxbase.org/entries/2020/aug/3/case-against-oop/