Este artigo foi traduzido por máquina.

O programador

.NET com vários paradigmas, Parte 4: Orientação a objeto

Ted Neward

Ted NewardNo artigo anterior, explorou pontos comuns e variabilidade, expressa por meio de programação processual e descobriu vários interessantes "corredores" pelo qual variabilidade pode ser introduzida em desenhos. Em particular, duas abordagens de design surgiram fora da linha processual do pensamento: variabilidade de comportamento e de nome e a variabilidade de algoritmo.

Como a complexidade dos programas e seus requisitos cresceu, os desenvolvedores viram-se lutando para manter todos os vários subsistemas em linha reta. Abstrações processuais, descobrimos, não "escala", assim como talvez já esperávamos. Com o advento da GUI, um novo estilo de programação começou a emergir, um que muitos leitores que aprenderam "plain old sdk" estilo de construção de aplicativos de Windows desde o Windows 3 SDK e Charles Petzold clássico "Programming Windows" (Microsoft Press, 1998) vai reconhecer instantaneamente. Ostensivamente processuais na natureza, esse estilo seguiu um padrão particularmente interessante. Cada procedimento em um nó de perto em cluster de funcionalidade relacionada centrado em torno de um parâmetro de "manipular", mais frequentemente tomá-lo como um parâmetro de primeiro (ou único) ou retornando de uma chamada de criar ou similares: CreateWindow, FindWindow, ShowWindow e muito mais, tudo centralizado em torno de um identificador de janela (HWND), por exemplo.

O que os desenvolvedores não sabia na época era que isso foi realmente uma nova forma de programação, um novo paradigma que fazem-se sentir dentro de poucos anos. Olhando para trás, naturalmente, faz com que seja óbvio que tratava de programação orientada a objeto, e a maioria dos leitores desta coluna será bem versado com seus preceitos e idéias já. Dado que, por que seria nós decidir gastar preciosa coluna polegadas sobre o assunto? A resposta é que não há discussão de design multiparadigm seria completo sem incorporar objetos dentro de sua alçada.

Fundamentos de objeto

Orientação a objetos é, em muitos aspectos, um exercício de herança. Herança de implementação dominou grande parte da discussão sobre design de objeto, com advogados sugerindo que abstrações adequadas são construídas, identificando as entidades do sistema — foram os "substantivos", como ele — e sempre que a semelhança emerge, elevar essa semelhança em um classe base, criando assim uma relação de "IS-A". Uma pessoa de IS-A de estudante, uma pessoa de IS-A de instrutor, uma pessoa IS-A objeto e assim por diante. Herança assim deu aos desenvolvedores um novo eixo no qual analisar os aspectos comuns e variabilidade.

Nos dias de C++, a abordagem de herança de implementação estava sozinha, mas como tempo e experiência evoluiu, herança da interface surgiu como uma alternativa. Em essência, a introdução da herança da interface na caixa de ferramentas do designer permitido para um relacionamento de herança de leve, declarando que um tipo de tipo diferente IS-A, mas sem o comportamento ou a estrutura do pai tipo. Interfaces, portanto, fornecem um mecanismo para tipos de "agrupamento" ao longo de um eixo de herança sem impor quaisquer restrições específicas relativas à sua aplicação.

Considere, por exemplo, o exemplo canônico orientada a objeto, que de uma hierarquia de formas geométricas que podem ser extraídas (se só figurativamente) a tela:

    class Rectangle
    {
      public int Height { get; set; }
      public int Width { get; set; }
      public void Draw() { Console.WriteLine("Rectangle: {0}x{1}", Height, Width); }
    }
    
    class Circle
    {
      public int Radius { get; set; }
      public void Draw() { Console.WriteLine("Circle: {0}r", Radius); }
    }

A semelhança entre as classes sugere que uma superclasse é em ordem aqui, para evitar a repetição desse uso comum em todas as formas geométricas drawable:

abstract class Shape
{
  public abstract void Draw();
}
  
class Rectangle : Shape
{
  public int Height { get; set; }
  public int Width { get; set; }
  public override void Draw() { 
    Console.WriteLine("Rectangle: {0}x{1}", Height, Width); }
}

class Circle : Shape
{
  public int Radius { get; set; }
  public override void Draw() { Console.WriteLine("Circle: {0}r", Radius); }
  }

Até agora, tão bom — a maioria dos desenvolvedores não levaria nenhum problema com o que foi feito até agora. Infelizmente, um problema encontra-se espera para os incautos.

Liskov Rides Again

O problema aqui é conhecido como o princípio da substituição de Liskov: qualquer tipo que herda de outro deve ser completamente substituível para que outros. Ou, para usar as palavras que originalmente descrita o princípio, "que q(x) seja uma propriedade predicada sobre objetos x do tipo t. Em seguida, q(y) deve ser verdadeiro para y de objetos do tipo s onde s é um subtipo de t."

O que isso significa na prática é que qualquer derivação particular do retângulo, tais como uma classe Square, deve garantir que obedece as mesmas garantias comportamentais fornecido pela base. Porque um quadrado é essencialmente um retângulo com a garantia que a altura e largura são sempre os mesmos, parece razoável gravar quadrado como exemplo em de Figura 1.

Figura 1 derivação de um quadrado

class Rectangle : Shape
{
  public virtual int Height { get; set; }
  public virtual int Width { get; set; }
  public override void Draw() { 
    Console.WriteLine("Rectangle: {0}x{1}", Height, Width); }
}

class Square : Rectangle
{
  private int height;
  private int width;
  public override int Height { 
    get { return height; } 
    set { Height = value; Width = Height; } 
  }
  public override int Width {
    get { return width; }
    set { Width = value; Height = Width; }
  }
}

Observe como Height e Width propriedades agora são virtuais, a fim de evitar qualquer tipo de sombreamento acidental ou corte comportamento quando substitui-los na classe Square. Até aqui, tudo bem.

Em seguida, um quadrado obtém passado para um método que leva um Retangular e "cresce" ele (o que geeks gráficos às vezes chamam de uma "transformação"):

class Program
{
  static void Grow(Rectangle r)
  {
    r.Width = r.Width + 1;
    r.Height = r.Height + 1;
  }

  static void Main(string[] args)
  {
    Square s = new Square();
    s.Draw();
    Grow(s);
    s.Draw();
  }
}

Figura 2 mostra o resultado de chamar esse código, que é não o que você esperava.

Figura 2-um surpreendente resultado com código Grow

O problema aqui, como leitores de cuidado podem ter já supôs, é que cada implementação de propriedade pressupõe que ele está sendo chamado no isolamento e, portanto, tem de agir com independência para garantir a altura = = restrição de largura em torno da Praça em todos os momentos. O código de crescer, no entanto, assume que um retângulo está sendo passado no, e permanece totalmente ignorantes sobre o fato de que é um quadrado chegando (como previsto!) e atua de forma totalmente adequado para retângulos.

O núcleo do problema? Praças não são retângulos. Eles têm um monte de semelhança, concedido, mas no final do dia, não mantenha as restrições de um quadrado de retângulos (que também é verdade, aliás, elipses e círculos) e tentar um modelo em termos de outro é baseado em uma falácia. É tentador para herdar praça retangular, especialmente porque ela nos permite reutilizar algum código, mas é uma premissa falsa. Na verdade, eu mesmo vou ir tão longe como a sugerir que um nunca deve usar herança para promover a reutilização até que o princípio da substituição de Liskov para esses dois tipos foi provado para ser verdade.

Este exemplo não é novo — Robert "Uncle Bob" Martin ( bit.ly/4F2R6t ) discutidos Liskov e este exemplo exato volta em meados da década de 90 quando estiver conversando com os desenvolvedores do C++. Alguns problemas como este podem ser resolvidos parcialmente usando interfaces para descrever as relações, mas que não ajuda neste caso específico, porque Height e Width permanecem propriedades separadas.

Existe uma solução neste caso? Não realmente, não quando mantendo a relação do quadrado--derivados-de-Rectangle no lugar. A melhor resposta que vem fazendo quadrados directa descendente da forma e abandonando a abordagem de herança inteiramente:

 

class Square : Shape
{
  public int Edge { get; set; }
    public override void Draw() { Console.WriteLine("Square: {0}x{1}", Edge, Edge); }
}

class Program
{
    static void Main(string[] args)
    {
      Square s = new Square() { Edge = 2 };
      s.Draw();
      Grow(s);
      s.Draw();
    }
}

Obviamente, agora temos o problema que Square não pode ser passado em para crescer em tudo, e parece que há uma relação de reutilização de código potencial lá. Podemos resolver isso em um aspecto, fornecendo uma exibição do quadrado como um retângulo usando uma operação de conversão, como mostrado em de Figura 3.

Operação de conversão de Figura 3

class Square : Shape
{
  public int Edge { get; set; }
  public Rectangle AsRectangle() { 
    return new Rectangle { Height = this.Edge, Width = this.Edge }; 
  }
  public override void Draw() { Console.WriteLine("Square: {0}x{1}", Edge, Edge); }
}

class Program
{
  static void Grow(Rectangle r)
  {
    r.Width = r.Width + 1;
    r.Height = r.Height + 1;
  }

  static void Main(string[] args)
  {
    Square s = new Square() { Edge = 2 };
    s.Draw();
    Grow(s.AsRectangle());
    s.Draw();
  }
}

Ele trabalha — mas é um pouco estranho. Nós também pode usar o recurso de operador de conversão c# para torná-lo mais fácil de converter quadrados em retângulos, como mostrado em de Figura 4.

Figura 4 A facilidade de operador de conversão c#

class Square : Shape
{
  public int Edge { get; set; }
  public static implicit operator Rectangle(Square s) { 
    return new Rectangle { Height = s.Edge, Width = s.Edge }; 
  }
  public override void Draw() { Console.WriteLine("Square: {0}x{1}", Edge, Edge); }
}

class Program
{
  static void Grow(Rectangle r)
  {
    r.Width = r.Width + 1;
    r.Height = r.Height + 1;
  }

  static void Main(string[] args)
  {
    Square s = new Square() { Edge = 2 };
    s.Draw();
    Grow(s);
    s.Draw();
  }
}

Esta abordagem, embora talvez surpreendentemente diferente do esperado, oferece a mesma perspectiva de cliente como antes, mas sem os problemas da implementação anterior, como de Figura 5 mostra.

Figura 5 O resultado do uso da facilidade de operador de conversão c#

Na verdade, temos um problema diferente — onde antes o método Grow modificado o retângulo que está sendo passado, agora parece que ele está fazendo nada, em grande parte porque ele está modificando uma cópia do quadrado, não o original quadrado propriamente dito. Nós poderia corrigir isso por ter o retorno de operador de conversão de uma nova subclasse do retângulo que contém uma referência secreta para esta instância quadrada, para que modificações para as propriedades de Height e Width, por sua vez vão voltar e modificar borda da Praça... mas, em seguida, estamos novamente para o problema original!

Não feliz terminando aqui

Em Hollywood, filmes tem que terminar de forma compatível com as expectativas do público ou enfrentam rejeição na bilheteria. Eu não sou uma cineasta, assim que eu sinto compulsão para apresentar os leitores desta coluna com um final feliz em todos os casos. Este é um daqueles casos: tentando manter o código original no lugar e fazer tudo funcionar apenas cria hacks mais profundas e mais profundas. As soluções podem ser mover o método de crescer ou transformar directamente sobre a forma de hierarquia ou apenas fazer o método Grow retornar o objeto modificado em vez de modificar o objeto transmitido no (que é algo que vamos falar em outra coluna), mas em suma, não podemos manter o código original no lugar e manter tudo funcionando corretamente.

Tudo isso é projetado para mostrar precisamente uma coisa: desenvolvedores orientados a objeto são confortáveis com a modelagem de elementos comuns e variabilidade com herança, talvez demasiado tanto assim. Lembre-se, se você optar por usar o eixo de herança para capturar a semelhança, você tem que garantir esta semelhança mantém através de toda a hierarquia se erros sutis como este estão a ser evitado.

Lembre-se, também, que a herança é sempre uma variação positiva (adicionando novos campos ou comportamentos), e que a modelagem negativa variabilidade na herança (o que é que quadrado tentou fazer) é quase sempre uma receita para o desastre ao longo de linhas de Liskovian. Certifique-se de que todas as relações de herança envolvem comuns positivo, e as coisas deveriam ser boas. Feliz codificação!

Ted Neward é uma entidade com Neward &Associates, uma empresa independente, especializada em enterprise.Sistemas de plataforma NET Framework e Java. Ele escreveu mais de 100 artigos, é um MVP c# e INETA orador e tem o autor e co-autor de uma dúzia de livros, incluindo "Profissional F # 2. 0" (Wrox, 2010). Ele também consulta e mentores regularmente. Contatá-lo pelo ted@tedneward.com de com perguntas ou solicitações de consultoria e ler seu blog em blogs.tedneward.com de.

Graças ao especialista técnico seguir para revisar este artigo: Anthony Green