Problema com polimorfismo – c# orientação-a-objetos polimorfismo

Pergunta:


Tenho um problema com polimorfismo. Mas antes de explicá-lo, quero deixar claro que estou aprendendo bastante coisa ainda, se quiserem jogar minha estrutura no lixo, fiquem a vontade.
Eu tenho um projeto que Converte tipos de Temperaturas em outras.
Eu tenho uma interface que diz que cada temperatura deve converter de um tipo de temperatura para outro tipo.

public interface ITemperatura
{
    double Convert(double valor, ITemperatura temperatura);
}

Celsius, Farenheit e Kelvin assinam ITemperatura. Ou seja, Celsius, por exemplo, usaria a Convert para converter uma temperatura em Celsius para outra que for passada em ITemperatura, como Farenheit, por exemplo.

public class Celsius : ITemperatura
{
    public double Convert(double valor, ITemperatura temperatura)
    {
        return 0; // Não deve entrar aqui, esse método só está aqui para respeitar a interface,
                  // A ideia é fazer o polimorfismo e entrar nos métodos especializados abaixo
    }

    public double Convert(double valor, Celsius temperatura)
    {
        return 1; // implementacao de calculo de Celsius  para Celsius
    }

    public double Convert(double valor, Farenheit temperatura)
    {
        return 2; // implementacao de calculo de Celsius para Farenheit (pois esta classe é celsius e o parametro é farenheit)
    }

    public double Convert(double valor, Kelvin temperatura)
    {
        return 3; // implementacao de calcula de Celsius para Kelvin
    }
}

Perceba acima que, além dos métodos de cada temperatura que são 3, tenho mais um que recebe ITemperatura. Ele só está ali porque a interface pede. Mas ele não deve ser chamado, a ideia é chamar os tipos especializados, Celsius, Farenheit e Kelvin.
Obs.: não implementei corretamente com os cálculos específicos, apenas retorno 1 2 e 3, representando retorno de Celsius, Farenheit e Kelvin, respectivamente, além do 0 que seria um erro retornar.

Tenho então a classe Converter que tem um método Convert também, mas não assina ITemperatura. Esse método é responsável por fazer o intermédio entre o main e as classes de conversão. Ela recebe duas temperaturas, DE e PARA, e também o valor que será convertido.
Ele utiliza as classes desta maneira:

public class Converter
{
    public double Convert(double valor, ITemperatura de, ITemperatura para)
    {
        return de.Convert(valor, para);
    }
}   

Já tudo pronto, para o consumo no main, tenho ali 2 casos, um de sucesso e um de falha.

static void Main(string[] args)
    {
        // 1º caso (ok):
        Celsius celsius = new Celsius();
        double valor = celsius.Convert(100, new Farenheit());
        Console.WriteLine(valor);

        // 2º caso (entrou no ITemperatura, não na especializada):
        Converter converter = new Converter();
        valor = converter.Convert(100, new Celsius(), new Farenheit());
        Console.WriteLine(valor);
        Console.Read();
    }
    //outputs: 2
    //         0

O primeiro caso é o de sucesso, ele cria uma instancia de Celsius e desta instância ele chama o Convert para transformá-lo em Farenheit. Fazendo isto, ele entrará no método correto da sobrecarga na classe Celsius que possui parâmetro Farenheit. resultando em 2 (retorno de farenheit ok).

O segundo caso é o de erro. Agora utilizando a classe Converter. Neste caso, ele não está mais indo direto para a classe Celsius. Agora, ele está passando antes por Converter, que recebe 2 Itemperatura, o DE e o PARA, cada um sendo uma temperatura, e só então envia para a Classe Celsius fazer sua sobrecarga.

inserir a descrição da imagem aqui

Desta maneira, quando ele chama o de.Convert(valor, para); eu queria que ele pegasse a mesma instância de Farenheit no PARA que eu enviei do main e encaminhar para a classe Celsius na sua sobrecarga correta que tem um Farenheit no parâmetro.

Porém, ao enviar para a Classe Celsius, ele não faz isso, ele encaminha ao método abstrato que recebe ITemperatura, retornando 0;

Importante notar que, em Converter, eu tenho a instancia correta que recebo no main, e inclusive depois, quando ele envia para Celsius no método que tem ITemperatura, eu debuguei e está lá a instância de Farenheit, mas ele mesmo assim entrou no método genérico que usa ITemperatura.

Existe alguma maneira de, mesmo recebendo no Converter o PARA como ITemperatura, fazê-lo entrar no método de Celsius específico sem ter que fazer if para testar seu tipo específico? Dizer algo para o compilador como: “Quero chamar o método mais específico para essa classe, sempre”.

A minha intenção é aprender. Podem jogar fora minha solução e dizer que tem uma outra maneira totalmente diferente e que não utiliza ifs ou switch explícitos, será ótimo conhecer.
Se tiverem uma sugestão em cima deste projeto também seria interessante.

Agradeço desde já às análises,
Robson Faxas.

Autor da pergunta robson ribeiro faxas

Comunidade

Não sei qual é o objetivo do AP, não deveria fazer isso, nem como aprendizado. Aprender é fazer certo. A não ser que queria provar que é errado.

Do ponto de vista de OOP tudo isso é um completo absurdo. E se alguém disser que isso é bom, ou não entende de OOP ou confirmará que OOP só serve pra atrapalhar.

De fato tudo o que essas classes da pergunta fazem é complicar algo simples.

Eu até poderia fazer algo mais orientado a objeto, mas as classes de unidades de temperatura teriam que ser completamente diferentes, começando pelo fato que elas realmente sejam objetos de temperatura e não apenas um veículo para métodos que não fazem sentido. Eu tentei até tirar se isso era a intenção do AP nos comentários, mas ficou claro que a intenção das classes é só gerar um marcador e não ser o objeto. Isso não faz sentido.

Fica mais claro que não se está fazendo nada útil, e nem mesmo orientado a objeto quando na verdade a conversão gera um número sem qualificação alguma de que é uma medida de temperatura específica qualificada.

O que se está pretendendo fazer de fato é apenas ter métodos de conversão de temperaturas. Isso pode ser feito de forma muito simples.

public static class TemperatureConverter {
    public double CelsiusToFahrenheit(double valor)  { //calcula aqui }
    public double CelsiusToKelvin(double valor) { //calcula aqui }
    public double FahrenheitToCelsius(double valor) { //calcula aqui }
    public double FahrenheitToKelvin(double valor) { //calcula aqui }
    public double KelvinToFahrenheit(double valor) { //calcula aqui }
    public double KelvinToCelsius(double valor) { //calcula aqui }
}

Na classe principal da aplicação:

public static void Main(string[] args) {
    double valor = Temperature.ConverCelsiusToKelvin(100);
}

Há ganho zero em fazer diferente disso. Há perdas em fazer da forma pretendida.

A impressão que tenho é que se está tentando fazer por não entender o que é orientação a objeto e os recursos da linguagem. A primeira versão da pergunta fazia algo pior ainda. Provavelmente porque estava tentando consertar um problema que só existia porque a arquitetura já estava toda errada. Esse é o problema de arquitetar errado. Começa procurar soluções insanas para consertar o que começou errado.

A resposta já estava preparada desde o início só esperando melhores esclarecimentos. Quando fui postar vi que essa solução é o que o ramaral propôs no final, que é a “única” solução sã para isso. A inicial dele ainda seria ruim. Mas entenderei qualquer resposta que tente ir no caminho que o AP deseja.

Em chat foi discutido o assunto e o Dener Carvalho falou em usar enum para marcar as temperaturas. Já seria uma solução ligeiramente melhor, mas ainda sem sentido. Reafirmo que as classes de temperaturas são apenas marcadores e uma tentativa de usar delegação onde não cabe.

Mesmo que isso tivesse algum sentido, o uso de ITemperatura está conceitualmente errada. Criar uma interface, usá-la na classe e não implementar apropriadamente é um escândalo 🙂

Algo assim até poderia ser usado em casos muito específicos onde haveria ganho. Não é o caso. Assunto relacionado: É uma prática ruim usar interfaces vazias?

A resposta do @bigown é bem simples, mas não resolve um requisito do AP que é evitar uso de if.

Para atender este requisito, é necessário ir um pouco mais além, eventualmente usando interfaces como você tentou na sua solução inicial. Mas felizmente o C# possui delegates, o que permite armazenar referências para métodos e dispensa as interfaces!

Então, considerando esta declaração de conversor:

public static class TemperatureConverter 
{
    public double CelsiusToFahrenheit(double valor)  { //calcula aqui }
    public double CelsiusToKelvin(double valor) { //calcula aqui }
    public double FahrenheitToCelsius(double valor) { //calcula aqui }
    public double FahrenheitToKelvin(double valor) { //calcula aqui }
    public double KelvinToFahrenheit(double valor) { //calcula aqui }
    public double KelvinToCelsius(double valor) { //calcula aqui }
}

Você pode declarar um dicionário que associe cada entrada possível do usuário a um determinado método de conversão, mais ou menos assim:

delegate double TemperatureConvert(double valor);

static Dictionary<String, TemperatureConvert> converters = 
        new Dictionary<string, TemperatureConvert>
{
    {"CelsiusToFahrenheit", TemperatureConverter.CelsiusToFahrenheit},
    {"CelsiusToKelvin", TemperatureConverter.CelsiusToKelvin},
    {"FahrenheitToCelsius", TemperatureConverter.FahrenheitToCelsius},
    {"FahrenheitToKelvin", TemperatureConverter.FahrenheitToKelvin},
    {"KelvinToFahrenheit", TemperatureConverter.KelvinToFahrenheit},
    {"KelvinToCelsius", TemperatureConverter.KelvinToCelsius}
};

E então você seleciona um conversor a partir da entrada do usuário, assim:

static void Main(string[] args)
{
    var temperature = double.Parse(args[0]);
    var conversor = args[1] + args[2] + args[3];

    var convertedTemperature = converters[conversor](temperature);

    Console.WriteLine(convertedTemperature);
}

Agora este aplicativo pode ser invocado passando a temperatura, a escala original e a escala a ser convertida, mais ou menos assim:

converter.exe 50 Fahrenheit To Celsius

E, neste caso, o output será a conversão implementada no método FahrenheitToCelsius 🙂

Veja funcionando no ideone.

É claro que talvez você queira se preocupar com validações, tolerância a maiúsculas e minúsculas, etc. E é claro também que, num aplicativo com interface gráfica, as entradas do usuário podem ser limitadas utilizando-se comboboxes, por exemplo.

E se você quiser exercitar o uso de interfaces (que é realmente desnecessário neste exemplo), você pode implementar cada conversor em sua própria classe, todos implementando uma interface comum para o método de conversão, e referenciar instâncias dos conversores no dicionário em vez de apenas o método de conversão.

Se você usasse Java, por exemplo, que não tem suporte a delegates, usar interfaces provavelmente seria uma solução válida para evitar os ifs.

Pondo de parte qualquer opinião sobre qual é o melhor modo de implementar o conversor, esta seria a implementação da forma como sugere na pergunta.

Interface que cada “unidade de temperatura” deve implementar:

public interface ITempertureUnit
{
    double FromKelvin(double value);
    double ToKelvin(double value);
}

Os método devem implementar a conversão da unidade de e para Kelvin

Implementação das unidades Celsius, Kelvin e Farenheit

public class Celsius : ITempertureUnit
{
    public double FromKelvin(double value)
    {
        return value - 273.15;
    }
    public double ToKelvin(double value)
    {
        return value + 273.15;
    }
}

public class Kelvin : ITempertureUnit
{
    public double FromKelvin(double value)
    {
        return value;
    }
    public double ToKelvin(double value)
    {
        return value;
    }
}

public class Farenheit : ITempertureUnit
{

    public double FromKelvin(double value)
    {
        return value * 1.8 - 459.67;
    }
    public double ToKelvin(double value)
    {
        return (value + 459.67) / 1.8;
    }

}

Implementação do conversor:

public static class Converter
{
    public static double Convert(double value, ITempertureUnit from, ITempertureUnit to)
    {
        return to.FromKelvin(from.ToKelvin(value));
    }
}

Modo de uso:

double valor = Converter.Convert(100, new Celsius(), new Farenheit());

Veja funcionando no ideone

As respostas obtidas estão todas muito boas e já fornecem muito do que
o AP precisa para aprender, considerando que a necessidade dele está
em aprender os conceitos de interfaces e polimorfismo. Ainda assim,
como esses conceitos são parte de um conceito maior (da Orientação a
Objeto), eu achei que valia a pena fornecer uma resposta complementar
(com uma sugestão de implementação alternativa SEM herança ou polimorfismo).

Em outras palavras, eu só ofereço aqui uma descrição complementar com a
solução já proposta pelo @bigown na resposta dele quando menciona:

[…] mas as classes de unidades de temperatura teriam que ser completamente
diferentes, começando pelo fato que elas realmente sejam objetos de
temperatura e não apenas um veículo para métodos que não fazem sentido.

Tudo bem que você esteja aprendendo. Mas essencialmente eu concordo com o @bigown a respeito de você estar usando algo de forma desnecessária e, portanto, equivocada.

A temperatura é uma entidade que está mais para um atributo (um valor utilizado em algum outro contexto, talvez até mesmo em uma outra classe) do que uma classe por si só. Faz pouquíssimo sentido pensar em temperatura como uma classe porque ela não teria nenhum comportamento (algo que ela literalmente “fizesse”, e que seria implementado na forma de métodos segundo a Orientação a Objetos). Você tem a necessidade de converter valores de temperatura segundo diferentes unidades, mas é um tanto forçado transformar essas funções de conversão em métodos porque essas conversões não são realmente comportamentos de uma classe Temperatura (ou de qualquer classe herdada dela). De fato, herdar de uma classe-base (ou mesmo de uma interface) “Temperatura” também soa estranho, porque não há mesmo especialização nenhuma a ser feita nas classes filhas!

Dessa forma, se você mantiver em mente que a temperatura está mais para um valor ou um atributo do que para uma classe propriamente dita, talvez fique mais fácil enxergar que o seu problema não tem a ver com polimorfismo, mas sim com representação. Isto é, como você faria para representar uma temperatura de forma que ela fosse facilmente convertida em diferentes unidades?

Assim, uma possível solução é implementar uma classe que represente uma temperatura. Internamente ela faz isso da forma como o seu desenvolvedor desejar. É uma caixa preta. Isto é, não importa como ela armazena o valor, desde que ele seja facilmente acessível de fora e em qualquer uma das unidades. Tá, essa implementação será uma classe de qualquer jeito, mas não há polimorfismo algum envolvido porque não há herança. A “classe” é só a forma de implementação de “um novo tipo”, que conceitualmente você ainda pode continuar pensando como um valor ou um atributo.

Se você parar pra pensar, há inúmeros exemplos que fazem exatamente isso. A própria classe String, ou a classe DateTime são assim. Elas implementam uma “estrutura” (e esse nome é bem propício, até mais do que “classe”, apesar da implementação ser de fato uma classe) que armazena, representa e manipula aquele tipo de dado mais “complexo” do que um int ou um float.

Bom, tendo dito isso, considere esse exemplo de implementação em C#:

/**
 * Implementa uma classe genérica e geral para manipulação de temperaturas
 * EM DIFENRENTES UNIDADES. 
 */
public class Temperature
{
    /** Valor da temperatura (sempre em Celsius, por convenção minha). */
    private double m_dValue;

    /**
     * Construtor protegido, para evitar que a classe seja instanciada
     * diretamente. Para instanciação, deve-se utilizar os métodos
     * apropriados para cada unidade.
     * @param dValue Double com o valor da temperatura EM CELSIUS.
     */
    protected Temperature(double dValue)
    {
        m_dValue = dValue;
    }

    /**
     * Método estático de criação de temperaturas em Celsius. 
     * @param dValue Double com o valor da temperatura em Celsius.
     * @return Instância do objeto Temperature com a temperatura dada.
     */
    public static Temperature asCelsius(double dValue)
    {
        return (new Temperature(dValue));
    }

    /**
     * Método estático de criação de temperaturas em Fahrenheit. 
     * @param dValue Double com o valor da temperatura em Fahrenheit.
     * @return Instância do objeto Temperature com a temperatura dada.
     */
    public static Temperature asFahrenheit(double dValue)
    {
        Temperature oRet = new Temperature(0);
        oRet.Fahrenheit = dValue;
        return oRet;
    }

    /**
     * Método estático de criação de temperaturas em Kelvin. 
     * @param dValue Double com o valor da temperatura em Kelvin.
     * @return Instância do objeto Temperature com a temperatura dada.
     */
    public static Temperature asKelvin(double dValue)
    {
        Temperature oRet = new Temperature(0);
        oRet.Kelvin = dValue;
        return oRet;
    }

    /** Propriedade de acesso ao valor em Celsius. */
    public double Celsius
    {
        get { return m_dValue; }
        set { m_dValue = value; }
    }

    /** Propriedade de acesso ao valor em Fahrenheit. */
    public double Fahrenheit
    {
        get { return m_dValue * 1.8 + 32; }
        set { m_dValue = (value - 32) / 1.8; }
    }

    /** Propriedade de acesso ao valor em Fahrenheit. */
    public double Kelvin
    {
        get { return m_dValue + 273; }
        set { m_dValue = value - 273; }
    }

    /**
     * Implementação de exemplo do operator de soma.
     * @param t1 Instância da primeira temperatura.
     * @param t2 Instância da segunda temperatura.
     * @return Instância da temperatura com o resultado da soma. 
     */
    public static Temperature operator+(Temperature t1, Temperature t2)
    {
        return new Temperature(t1.Celsius + t2.Celsius);
    }

    /**
     * Retorna a representação da temperatura em string com todos os
     * valores em cada uma das unidades suportadas.
     * @return String com a representação da temperatura.
     */
    public override string ToString()
    {
        return string.Format("Temperatura: {0} °C ({1} °F ou {2} °K)", Celsius, Fahrenheit, Kelvin);
    }
}

Observe que:

  • O construtor foi feito protegido (protected) de forma que ele não pode ser usado diretamente (fora dessa classe). Isso se deve ao fato de que a assinatura do construtor simplesmente chamado Temperature com um valor double é confusa. Note em outras respostas há sempre algo como fromKelvin(double value), pois o nome da unidade no nome do método indica a intenção. Nesse caso, eu criei métodos estáticos para construir uma temperatura a partir de uma unidade dada (asCelcius, asFahrenheit e asKelvin) dessa mesma forma, só que ao invés de chamar de “from” (“de”, em Português) eu chamei de “as” (“como” em Português) porque quero transmitir a intenção de que não se trata de uma conversão, mas sim de uma criação. A “estrutura” internamente se vira pra manter tudo correto.

  • Há propriedades para acesso aos valores nas diferentes unidades. Quer ler ou atualizar em Kelvin? Basta fazer x = t.Kelvin ou t.Kelvin = x. Simples e direto (tal como se estivesse atualizando o dia de uma data ao fazer d.day = 10 em uma outra estrutura desse tipo). Observe que internamente eu optei por manter o valor em Celsius. Poderia ter uma variável para cada um e sempre atualizar todas? Poderia, mas é desnecessário. Por isso, eu sempre converto e mantenho em Celsius nas atualizações, e converto pra devolver nos acessos se for necessário.

  • Essa abordagem dá margem para outras coisas interessantes. Se você sobrecarregar um operador, você pode fazer contas com temperaturas, comparar valores, etc, da mesma forma como faria se usasse double, mas sem se preocupar com a unidade! Eu implementei o operador de soma (operator+) para exemplificar, no código abaixo.

Exemplo de uso da classe Temperature:

class Program
{
    static void Main(string[] args)
    {
        Temperature t = Temperature.asCelsius(100);
        Console.WriteLine(t);

        t.Fahrenheit = 48;
        Console.WriteLine(t);

        t.Kelvin = 397;
        Console.WriteLine(t);

        Temperature t1 = Temperature.asKelvin(380);
        Temperature t2 = Temperature.asCelsius(22);
        Temperature t3 = Temperature.asFahrenheit(80);
        Console.WriteLine(t1 + t2 + t3);
    }
}

Esse programa gera a seguinte saída (a última linha apresenta o resultado da soma t1 + t2 + t3 no código acima):

Temperatura: 100 °C (212 °F ou 373 °K)
Temperatura: 8,88888888888889 °C (48 °F ou 281,888888888889 °K)
Temperatura: 124 °C (255,2 °F ou 397 °K)
Temperatura: 155,666666666667 °C (312,2 °F ou 428,666666666667 °K)

Fonte

Related Posts:

Qual a diferença entre AppCompatActivity e Activity? – android android-activity
Pergunta: Qual a diferença da AppCompatActivity para Activity ? A partir de qual versão a AppCompatActivity foi adicionada ao Android? Autor da pergunta Luhhh A diferença reside ...
Como abreviar palavras em PHP? – php string
Pergunta: Possuo informações comuns como nome de pessoas e endereços, e preciso que elas contenham no máximo 30 caracteres sem cortar palavras. Exemplo: 'Avenida Natalino João Brescansin' ...
Qual é a finalidade de um parêntese vazio numa declaração Lambda? – c# expressões-lambda característica-linguagem
Pergunta: Criei um exemplo de uma declaração Lambda sem argumentos, entretanto, estou com duvidas referente a omissão do parêntese vazio () na declaração. Veja o exemplo: class ...
Boas práticas para URI em API RESTful – api rest restful
Pergunta: Estou com dúvida em relação às URIs de alguns recursos da api que estou desenvolvendo. Tenho os recursos projetos e atividades com relação 1-N, ...
Dúvidas sobre a integração do MySQL com Java – java mysql netbeans
Pergunta: Estou criando um sistema no NetBeans, utilizando a linguagem Java e o banco de dados MySQL. Escrevi o seguinte código para realizar a conexão ...
Qual é a finalidade da pasta Model do framework Inphinit? – php inphinit
Pergunta: No Inphinit micro-framework existe a pasta Model que fica dentro da pasta application, e nela é onde ficam as classes, mas eu estou muito ...
Uso do ‘@’ em variáveis – javascript typescript coffeescript
Pergunta: Vejo em algumas linguagens que compilam para javascript, como TypeScript e CoffeeScript, o uso do @ em variáveis, como também, casos em que o ...
Qual tamanho máximo um arquivo JSON pode ter? – json arquivo
Pergunta: Vou dar um exemplo para conseguir explicar minha duvida: Preciso recuperar informação de imagens vindas de uma API, esse banco de imagens me retorna JSON's ...
O que é Teste de Regressão? – terminologia engenharia-de-software testes
Pergunta: Na matéria de Teste de Software o professor abordou um termo chamado Teste de Regressão, isto dentro da disciplina de teste de software. Sendo ...
O que é um construtor da linguagem? – php característica-linguagem
Pergunta: Em PHP, já li e ouvi várias vezes a respeito dos Construtores da Linguagem. Os casos que sempre ouvi falar deles foi em casos ...
Função intrínseca para converter numérico para string – cobol
Pergunta: Estou a tentar saber se existe alguma função intrínseca do COBOL para converter um data numérico para string sem precisar usar a cláusula REDEFINES: ( ...
Porque usar implements? – java android
Pergunta: Qual a diferença entre usar btn.setOnClickListener(new OnClickListener() { e public class MainActivity extends Activity implements OnClickListener{ Estive fazendo um curso de Android e meu professor falou que ...
O que é XHTML e quando deve ser usado? – html xml xhtml
Pergunta: O que eu sei é que o XHTML precisa ser XML válido. Isso implica, por exemplo, que todas as tags precisam ser fechadas. Por ...
Uma placa aceleradora de vídeo pode melhorar o desempenho não-gráfico? [fechada] – desempenho
Pergunta: Para desenvolver em Ruby on Rails, eu utilizo aqui uma máquina virtual do VirtualBox com Ubuntu Server 14.04 sem interface gráfica instalada. Recentemente descobri uma ...
Concat() VS Union() – c# .net
Pergunta: Qual a diferença entre Concat() e Union() ? Quando usar Concat() e quando usar Union() ? Somente pode ser usado em list ? ...

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *