C# - Entendendo o relacionamento entre objetos (Herança/Composição)


 Neste artigo vamos recordar como funciona o relacionamento entre objetos na linguagem C#.

O relacionamento entre objetos define como eles vão interagir ou colaborar para executar uma operação em uma aplicação.

Em qualquer aplicação, objetos de classes de interface do usuário vão interagir com objetos da camada de negócios para executar uma operação. Além disso, os objetos da camada de negócios podem interagir com objetos de um repositório que por sua vez se comunica com algum objeto da fonte de dados ou com algum objeto de serviço.

Para melhor compreender como isso funciona na prática vamos analisar o seguinte cenário:

Uma aplicação que Gerencia Pedidos de Clientes para alguns produtos onde cada pedido pode ter um ou mais produtos.

Como podemos entender quais objetos participam e como eles se relacionam entre si nesta aplicação ?

A primeira coisa a fazer é classificar as classes a partir da descrição fornecida.

Então, usando o princípio da responsabilidade única podemos definir as seguintes classes:

Quais os tipos de relacionamentos entre os objetos podemos ter ?

Os tipos básicos de relacionamentos definidos na programação orientada a objetos são:

  1. Associação

  2. Colaboração (Collaboration)

  3. Agregação (Aggregation)

  4. Composição (Composition)

  5. Herança (Inheritance)

 

Vejamos com mais detalhes cada um destes relacionamentos...

1 - Associação

A associação descreve um vínculo que ocorre entre classes , sendo que a mais comum é a  associação binária, mas é possível que uma classe esteja vinculada a si própria, e ai temos a associação unária; outro cenário é onde temos uma associação que seja compartilhada por mais de uma classe, o que conhecemos por associação ternária ou N-ária, sendo este tipo de associação a mais rara e também mais complexa.

Falamos sobre associação entre dois objetos quando cada um deles pode usar o outro, mas também cada um deles pode existir sem o outro. Não há dependência entre eles.

2 - Colaboração

O relacionamento de colaboração é muitas vezes conhecido como relacionamento 'usa um' ou 'uses a'.  A colaboração declara que dois objetos estão colaborando quando um objeto faz uso de outro objeto para completar uma operação.

Em nossa exemplo, para salvar e recuperar os detalhes do clientes, a classe ClienteRepository usa um objeto Cliente para salvar e recuperar os dados. Da mesma forma outras classes de repositório como ProdutoRepository e PedidoRepository usam objetos Produto e Pedido respectivamente, e, portanto, estão colaborando para executar uma operação.

Vejamos um exemplo de código que mostra isso:

    public class Cliente
    {
        public Cliente() 
        { }
        public Cliente(int clienteId)
        {
            this.ClienteId = clienteId;
            Enderecos = new List<Endereco>();
        }
        public int ClienteId { get; private set; }
        public string Nome { get; set; }
        public string Email { get; set; }
        public List<Endereco> Enderecos { get; set; }
        public bool Validate()
        {
            bool isValid = true;
            if (string.IsNullOrWhiteSpace(Nome) ||
                string.IsNullOrWhiteSpace(Email))
                isValid = false;
            return isValid;
        }
    }
    public class ClienteRepository
    {
        // Retorna um único cliente
        public Cliente GetCliente(int clienteId)
        {
            // cliente uma instância da classe Cliente
            Cliente cliente = new Cliente(clienteId);
            // retorna um cliente
            return cliente;
        }
        // retorna todos os clientes
        public IEnumerable<Cliente> GetClientes()
        {
            // retorna todos os clientes
            return new List<Cliente>();
        }
        // Salva o cliente atual
        public bool Salvar(Cliente cliente)
        {
            //salva o cliente definido
            return true;
        }
    }

Na classe ClienteRepository, você deve ter notado que recuperamos os detalhes do cliente da fonte de dados; preenche o objeto Cliente criado no processo e retorna o mesmo. Ele também usa um objeto do Cliente ao salvar os detalhes do cliente de volta à fonte de dados.

3 - Agregação

Esse relacionamento é um tipo especial de associação onde as informações de um objeto (chamado objeto-todo) precisam ser complementados pelas informações contidas em um ou mais objetos de outra classe (chamados objetos-parte); temos então o que conhecemos como todo/parte.  A agregação um tipo composição que representa um vínculo fraco entre duas classes.

O relacionamento de agregação ás vezes é referido como relacionamento "tem um" ou 'has a'.

Neste tipo de relacionamento, um objeto pode ser composto de um ou mais objetos na forma de suas propriedades. Assim temos que :

- Todo Cliente tem um endereço para o qual o produto solicitado será enviado
- Cada Pedido tem-um cliente, um endereço de envio e um produto representado como um PedidoItem

Podemos então concluir que nosso objeto da classe Pedido é composto pelos objetos Cliente, Endereco e PedidoItem.

O objeto PedidoItem é ainda composto pelo objeto Produto e a classe Pedido compõe esses objetos com suas propriedades.

Vejamos um exemplo de código que expressa isso:

    public class Pedido
    {
        // um objeto pode compor outros objetos com suas propriedades
        public Cliente Cliente { get; set; }
        public Endereco EnderecoPostagem { get; set; }
        public List<PedidoItem> Pedidos { get; set; }
    }

A representação UML para o relacionamento Agregação é representada como uma linha de associação com um diamente junto da classe agregadora:

4 - Composição

O relacionamento Composição, representa um vínculo forte entre duas classes , e , é também um relacionamento caracterizado como parte/todo, mas, neste caso, o todo é responsável pelo ciclo de vida da parte. Assim a existência do Objeto-Parte NÃO faz sentido se o Objeto-Todo não existir.

No nosso exemplo o objeto da classe Pedido é composto por um Cliente e um PedidoItem.  Se rompermos o relacionamento entre as classes Pedido e Cliente, a classe Cliente ainda vai poder existir, mas se a relação entre a classe Pedido e a classe PedidoItem for quebrada, a classe PedidoItem não pode existir.(um pedido é composto por um ou vários itens)

Suponha que a funcionalidade do nosso aplicativo mude no futuro e, em vez de aceitar pedidos de produtos, agora oferece alguns outros serviços aos clientes existentes, digamos um serviço de mensagens.

Nesse cenário, a classe Pedido não servirá para nada. No entanto, a classe Cliente que já foi composta pela classe Pedido ainda pode existir sem ela, já a classe PedidoItem não pode.

A representação UML para o relacionamento Agregação é representada como uma linha de associação com um diamente preenchido junto da classe agregadora:

Um Pedido tem um Item representado por PedidoItem.

5 - Herança

A herança às vezes é referida como um relacionamento "é um" ou 'is a'. Neste tipo de relacionamento, uma classe herda os membros de outra classe. A classe herdada é conhecida como a classe base, enquanto a classe herdada é conhecida como a classe derivada. Como a classe derivada tem os membros da classe base, pode-se dizer que a classe derivada é um sub-tipo da classe base. A classe derivada pode ou não ter membros diferentes dos herdados.

Suponha que nosso aplicativo esteja funcionando bem no mercado. Ao ver isso, o proprietário do produto agora quer adicionar um novo recurso no aplicativo que monitoraria o tipo de produtos com alta demanda.

A partir do novo requisito, é bastante claro que teremos de criar sub-tipos de nossa classe de produtos. Esses sub-tipos representarão as categorias de produtos especializados no mundo real, conforme mostrado na imagem abaixo.

As classes AlbumMusica e Livro, possuem algumas propriedades próprias; mas como herdam da classe Produto, elas também herdam suas propriedades.

Então, pode-se dizer que o AlbumMusica é um tipo de produto e, da mesma forma o Livro é um tipo de produto.

Dessa forma a herança pode ser usada para reutilizar e estender código.

Mas seria a herança uma boa prática ?

Não seria a composição mais indicada ?

Herança ou Composição ? Qual usar ?

Para responder vamos resumir rapidamente os principais conceitos relacionados a herança e composição.

Herança:

Composição :

Em geral usar composição ao invés de herança trás mais vantagens na maioria das situações.(eu escrevi na maioria, não em todas)

Mas porque devemos mesmos dar preferência à composição e não à herança ?

Usando herança:

1- Ao usar herança estamos violando um dos pilares da orientação a objetos: o encapsulamento, visto que os detalhes da implementação da classe Pai são expostos nas classes Filhas;
2- Ao usar herança estamos violando um dos princípios básicos das boas práticas de programação : manter o acoplamento entre as classe fraco, visto que as classes filhas estão fortemente acopladas à classe Pai e alterar uma classe Pai pode afetar todas as classes Filhas;
3- As implementações herdadas da classe Pai pelas classes Filhas não pode ser alteradas em tempo de execução;

Usando composição :

1- Os objetos que foram instanciados e estão contidos na classe que os instanciou são acessados somente através de sua interface;
2- A composição pode ser definida dinamicamente em tempo de execução pela obtenção de referência de objetos a objetos de do mesmo tipo;
3- A composição apresenta uma menor dependência de implementações;
4- Na composição temos cada classe focada em apenas uma tarefa (princípio SRP);
5- Na composição temos um bom encapsulamento visto que os detalhes internos dos objetos instanciados não são visíveis;

Percebemos que a herança viola dois conceitos básicos que sempre devemos aplicar em nosso código enquanto que a composição naturalmente nos leva a usar tais conceitos.

Mas então eu nunca devo usar herança ??? (Nunca é uma palavra que nós mortais deveríamos pronunciar com muito cuidado....)

Não existe um mandamento para nunca usar herança, mas podemos definir algumas regras para identificar quando podemos usá-la de forma a não ter os problemas que ela acarreta:

Então , considere a utilização da herança se...:

- A classe Filha expressar "um tipo especial de" e não "ser um papel desempenhado por";
- Uma instância de uma classe Filha NUNCA precisar tornar-se um objeto de outra classe;
- A classe filha estender ao invés de substituir total ou parcialmente as responsabilidades da classe Pai;
- A sua hierarquia de herança representar um relacionamento "É um" e não um relacionamento "Tem um";
- Você desejar ou precisar realizar alterações globais para as suas classes filhas alterando uma classe Pai;
- Você precisar aplicar a mesma classe e métodos a diferente tipos de dados;

E estamos conversados...

Todavia digo-vos a verdade, que vos convém que eu vá; porque, se eu não for, o Consolador não virá a vós; mas, quando eu for, vo-lo enviarei.
E, quando ele vier, convencerá o mundo do pecado, e da justiça e do juízo.
Do pecado, porque não crêem em mim;
Da justiça, porque vou para meu Pai, e não me vereis mais;

João 16:7-10

Referências:


José Carlos Macoratti