Por que eu devo ler este artigo:Este artigo trata sobre os principais fundamentos da orientação a objetos em C#, explicando os conceitos de abstração, herança, polimorfismo além de falar sobre a implementação de alguns destes fundamentos em C#, assim como palavras reservadas dele que incidem sobre estes conceitos.

Os tópicos abordados neste artigo poderão ser úteis no dia a dia de todo desenvolvedor, pois abordará:


Guia do artigo:

Fundamentos básicos de Orientação a Objetos

Neste artigo veremos os principais conceitos e fundamentos da orientação a objetos aplicados no C#. Além disso, veremos o uso das palavras chaves virtual, override, abstract, sealed, partial e dos modificadores de acesso public, protected, internal, protected internal e private.

Atualmente existem dois paradigmas principais de desenvolvimento de software na mente dos desenvolvedores e no mercado. Temos o paradigma estruturado, que antes dominava o mercado, e o paradigma da orientação a objetos (O.O), que cada vez mais vai tomando conta do mercado.

Normalmente, profissionais mais antigos estão habituados com o paradigma estruturado e os mais novos com o paradigma O.O, mas é claro que isso não é uma regra.

O fato é que a orientação a objetos vem tendo sua adoção numa crescente no mercado, mas é fato também que na maioria das vezes ela é subutilizada. É muito comum vermos equipes de desenvolvimento que desconhecem alguns fundamentos básicos da orientação a objetos, que são os pilares dela.

A ideia deste artigo é apresentar alguns dos conceitos mais básicos e fundamentais da orientação a objetos, com exemplos pontuais e aplicados ao C#. Vamos aproveitar também para falar sobre algumas palavras reservadas do C# que estão de certa forma relacionadas aos conceitos que vamos apresentar.

Para começar, vamos traçar um breve comparativo, sobre os paradigmas estruturados e orientado a objetos.

Programação estruturada VS Programação Orientada a Objetos

Antigamente, há algumas décadas atrás, o paradigma que predominava no desenvolvimento de software era o da programação estruturada. Basicamente, a grosso modo, os softwares eram compostos por rotinas e sub-rotinas que chamavam umas às outras, além de variáveis que tinham escopo local (dentro de rotinas / sub-rotinas) ou global. Assim como todo paradigma, o paradigma estruturado tinha seus prós e contras e foi bastante eficiente no que se propôs durante seus anos de domínio no mercado, além de ter sido bastante importante para a evolução da engenharia de desenvolvimento de software.

Com o passar dos anos, surgiu o paradigma da orientação a objetos, que também não é tão novo como muitos pensam, mas que veio a ganhar mais força na última década.

A orientação a objetos surgiu com o objetivo de tornar o desenvolvimento de software menos complexo e mais produtivo. A ideia era termos estruturas de dados, que possuem estado e comportamento e colaboram entre si. Dessa forma, deixaríamos de ter todas as rotinas e sub-rotinas “espalhadas” pelo sistema e passamos a atribuir elas a uma dessas estruturas de dados, de forma coesa, cada qual com sua responsabilidade. Além disso, encapsularíamos as variáveis nestas mesmas estruturas, controlando o acesso às mesmas e tornando público apenas aquilo que for pertinente.

O paradigma da orientação a objetos possui três conceitos fundamentais:

  • Encapsulamento – Prevê o isolamento a determinados elementos do objeto (métodos /atributos) de acordo com a necessidade de acesso a eles. Este conceito parte da premissa de que nem todo método e atributo precisam estar visíveis e acessíveis publicamente. Existem elementos que são pertinentes apenas ao próprio objeto, outros pertinentes aos objetos filhos e outros que são pertinentes todos os objetos associados. O encapsulamento se dá através dos modificadores de acesso, que veremos mais a frente.
  • Abstração – É a capacidade de focar nos pontos mais importantes do domínio de aplicação do sistema e abstrair os detalhes menos relevantes. Na modelagem de um sistema orientado a objetos, uma classe tende a ser a abstração de entidades existentes no mundo real (domínio da aplicação). Ex.: Cliente, Funcionário, Conta Bancária.
  • Polimorfismo – É a capacidade de um elemento ser capaz de assumir diferentes formas. Na orientação a objetos, chamamos de polimorfismo a possibilidade que temos de mudar o comportamento de um mesmo método de um objeto dentro da sua hierarquia.

Além disso, podemos citar os seguinte elementos com sendo os alguns dos principais da orientação a objetos:

  • Classe – Uma classe é um tipo de dado que representa tudo aquilo que um objeto deste tipo poderá ter/fazer. Na classe determinamos o que será armazenado em seu estado e quais comportamentos ele terá, ela funciona como uma estrutura de referência para a criação de objetos. Uma classe pode ter vários objetos.
  • Objeto – Um objeto é uma instância de uma classe. É a estrutura completa, criada em memória, que representará a classe com tudo o que foi definido nela, inclusive com os valores armazenados nos seus respectivos atributos.
  • Atributos – Os atributos representam o estado de um objeto. É neles que armazenaremos as informações de nossos objetos. Ex.: Nome, Idade, Endereço etc.
  • Métodos – Representam os comportamentos (operações) de nossos objetos, são as operações que ele pode executar. Ex.: ValidarCPF, AprovarCredito, LiberarPagamento etc.
Representação Gráfica de
classe com muitos objetos
Figura 1. Representação Gráfica de classe com muitos objetos.

Na Figura 1, podemos ver que uma classe pode ser instanciada diversas vezes, dando origem a diversos objetos diferentes.

Tipos de Referência x Tipos de Valor

Em C# temos duas classificações de tipos de dados. Que são os tipos de referência (References Types) e os tipos de valor (Value Types). A diferença chave entre os dois tipos é a na passagem de valores dos mesmos. No caso dos reference types, os valores dos objetos não são copiados, mas apenas sua referência, enquanto nos value types os valores são copiados de um objeto para o outro.

Todos os objetos que são do tipo de uma classe ou interface são reference types. Tipos enumerados e tipos primitivos são value types.

Ao atribuirmos uma referência de value type a outra, estamos literalmente copiando o seu valor, replicando-o para o novo elemento. Ao atribuirmos uma referência de um reference type para outro, não há cópia de valores, mas apenas de suas referências.

Listagem 1. Exemplo de Value Types
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.Modificadores;
  using ExemplosFundamentos.AbstractExemplo;
  using ExemplosFundamentos.PartialClass;
  
  namespace ExemplosFundamentos
  {
      class Program
      {
          static void Main(string[] args)
          {
              double valorA = 10;
              double valorB = 20;
              Console.WriteLine("valorA : " + valorA);
              Console.WriteLine("valorB : " + valorB);
              Console.WriteLine("Copiando valor de A para B...");
              valorB = valorA;
              Console.WriteLine("valorA : " + valorA);
              Console.WriteLine("valorB : " + valorB);
              Console.WriteLine("Alterando valor de A para 50...");
              valorA = 50;
              Console.WriteLine("valorA : " + valorA);
              Console.WriteLine("valorB : " + valorB);
              Console.ReadLine();
          }
      }
  }

Observe no exemplo da Listagem 1, primeiro declaramos duas variáveis valorA e valor B, nas linhas 15 e 16 respectivamente. Com esta declaração, imagine que temos dois espaços em memória, um para a variável valorA com o valor 10 e outro para a variável valorB com o valor 20. Na linha 20 atribuímos valorA ao valorB. Neste momento, o sistema copiou o valor de B para A, então agora temos ainda dois espaços em memória, um com o valorA de 20 e outro com o valorB também com 20. Na linha 24 alteramos o valorA mesmo para 50, passando a ter no primeiro espaço em memória o valor de 50 e no segundo o valor de 20. Observe que ao atribuirmos o valorA ao valorB na linha 20, copiamos todo conteúdo de uma posição de memória para outra. Na Figura 2, temos uma ilustração do que ocorre após a atribuição.

Ilustração - Atribuição de
Value Types
Figura 2. Ilustração - Atribuição de Value Types.
Resultado
Figura 3. Resultado da Listagem 1

Observando a Figura 3, podemos ver que o resultado da execução de nosso código. Observe que os valores de A e B se mantiveram independentes, mesmo após atribuição de um ao outro, pois o valor foi simplesmente copiado.

No caso dos reference types, ocorre algo diferente. Vejamos a Listagem 2.

Listagem 2. Exemplo de Reference Types
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.Modificadores;
  using ExemplosFundamentos.AbstractExemplo;
  using ExemplosFundamentos.PartialClass;
  using ExemplosFundamentos.SealedClass;
  
  namespace ExemplosFundamentos
  {
      class Program
      {
          static void Main(string[] args)
          {
              Produto produtoA = new Produto();
              produtoA.Descricao = "Computador Desktop";
              Produto produtoB = new Produto();
              produtoB.Descricao = "Notebook";
              Console.WriteLine("Produto A: " + produtoA.Descricao);
              Console.WriteLine("Produto B: " + produtoB.Descricao);
              Console.WriteLine("......");
              Console.WriteLine("Atribuindo produtoB ao produtoA");
              produtoA = produtoB;
              Console.WriteLine("Produto A: " + produtoA.Descricao);
              Console.WriteLine("Produto B: " + produtoB.Descricao);
              Console.WriteLine("......");
              Console.WriteLine("Alterando a descrição de produtoA para Scanner");
              produtoA.Descricao = "Scanner de Mesa";
              Console.WriteLine("Produto A: " + produtoA.Descricao);
              Console.WriteLine("Produto B: " + produtoB.Descricao);
              Console.ReadLine();
          }
      }
  }

Na Listagem 2 temos um exemplo semelhante, porém ao invés de usar variáveis do tipo Double, estamos usando objetos do tipo Produto. Nas linhas 16 e 18 criamos dois objetos do tipo Produto. Neste momento, temos duas variáveis de referência apontando para dois espaços de memória diferentes.

Na linha 24, atribuímos produtoB ao produtoA. É neste ponto que entra a principal diferença entre value types e reference types. No caso de value types vimos que o valor foi copiado de uma variável para outra. Neste caso, apenas a referência de produtoB é copiada para produtoA. Com isso, passamos a ter duas variáveis, produtoA e produtoB, apontando para o mesmo espaço em memória. Isso significa que a partir deste momento, qualquer alteração que fizermos tanto em produtoA quanto em produtoB estarão refletindo no mesmo espaço em memória, isso pode ser mais bem observado na ilustração da Figura 4.

Ilustração - Atribuição de Reference Types
Figura 4. Ilustração - Atribuição de Reference Types.
Resultado
Figura 5. Resultado da Listagem 2

Observando a Figura 5, podemos observar que a partir do momento que atribuímos produtoB ao produtoA, ambos passam a apontar para o mesmo espaço em memória. Observe que ao alterarmos a descrição do produtoA para Scanner, o valor de produtoB também parece ter mudado. Na verdade, o que acontece é que produtoB está apontando para o mesmo lugar de produtoA em memória.

Mas e o que aconteceu com a instância criada na linha 16? Ela se perdeu e não conseguimos mais acessar a mesma, ficando a cargo do Garbage Collector a liberação dela da memória.

Nota do DevMan

Garbage Collector (GC) é o gerenciador de objetos em memória do .NET responsável por liberar da memória as instâncias não utilizadas pela aplicação. Quando o GC identifica um objeto de sua aplicação que não possui nenhuma referência, ele o destrói liberando espaço em memória.

Class x Struct

Quando declaramos uma classe, estamos criando um “modelo” de objetos. Estamos dizendo que todos os objetos daquele tipo terão determinados métodos e atributos. Ao criarmos um novo objeto (com o operador new), estamos criando uma nova instância desta classe e retornando a referência para a esta instância.

Listagem 3. Exemplo de classe em C#
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace DevMedia.EasyNet
  {
      public class Cliente
      {
          public string Nome { get; set; }
   
          public int Idade { get; set; }
   
          public DateTime DataCadastro { get; set; }
      }
  }
Listagem 4. Exemplo de criação de um objeto em C#
 
  Cliente cliente = new Cliente();
  cliente.Nome = "Ricardo Coelho";
  cliente.Idade = 25;
  cliente.DataCadastro = DateTime.Parse("09/02/2005");
  

Na Listagem 3 temos um exemplo de classe com os atributos referentes à cliente, enquanto na Listagem 4 temos a criação de um objeto deste tipo (classe cliente). Vale ressaltar que toda classe é um reference type.

Compreendido este conceito, outro conceito que temos é o de struct. Um struct em .NET é uma estrutura de dados usada para agrupar dados que possuem algum tipo de relação.

Um struct, apesar de ter a sintaxe parecida com a de uma classe, é um value type. Essa é a diferença fundamental entre eles. Um struct também suporta construtores (parametrizados), métodos e properties, mas é preciso tomar cuidado, pois a ideia do struct é armazenar estruturas de dados pequenas, se você tem uma estrutura grande, ou com muitos métodos, regras, é melhor avaliar o uso de uma classe. Nas Listagens 5 e 6 temos um exemplo de declaração e uso de um struct.

Listagem 5. Exemplo de declaração de um struct
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.StructExample
  {
      public struct PontoGeometrico
      {
          public string Latitude { get; set; }
          
          public string Longitude { get; set; }
      }
  }
Listagem 6. Exemplo de uso de um struct
 using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.StructExample;
   
  namespace ExemplosFundamentos
  {
      class Program
      {
          static void Main(string[] args)
          {
              PontoGeometrico ponto = new PontoGeometrico();
              ponto.Latitude = "-40.0.0";
              ponto.Longitude = "-20.0.0";
              Console.WriteLine("Latitude:" + ponto.Latitude);
              Console.WriteLine("Longitude:" + ponto.Longitude);
              Console.ReadLine();
          }
      }
  }

Herança

Em orientação a objetos, herança é quando uma classe (filha) herda todas as características (atributos e métodos) de outra classe (pai). A herança está diretamente relacionada ao polimorfismo, pois através da herança podemos sobrescrever e estender funcionalidades do sistema.

Quando uma classe filha herda de uma classe pai, estamos dizendo que ela possui todas as características dela. A classe pai é considerada a classe genérica enquanto que suas classes filhas são consideras as classes especializadas.

A herança é uma faca de dois gumes, pois ao mesmo tempo em que pode ajudar trazendo mais reuso de código, pode também atrapalhar gerando muito acoplamento entre seus objetos, visto que qualquer alteração na classe pai será refletida diretamente em todas as classes filhas.

Uma classe pai pode ser herdada por diversas classes filhas, não existe limite para este caso. Por outro lado, uma classe filha só pode ter uma classe pai, ou seja, o .NET não suporte herança múltipla de classes.

Listagem 7. Classe Pessoa – Classe Pai
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.Heranca
  {
      public class Pessoa
      {
          public string Nome { get; set; }
  
          public String Endereco { get; set; }
  
          public String Pais { get; set; }
      }
  }

Na Listagem 7 definimos nossa classe pai, Pessoa, que servirá de base para as outras. Observe que definimos apenas os itens que são comuns a todas as pessoas de nosso sistema, sendo os atributos Nome, Endereco e Pais. Sempre que criarmos uma hierarquia de classes em nossos sistemas, temos que tomar cuidado em colocar na classe pai apenas aquilo que realmente for comum a todos os filhos.

Listagem 8. Classe PessoaFisica
using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace ExemplosFundamentos.Heranca
  {
      public class PessoaFisica:Pessoa
      {
          public string CPF { get; set;}
   
          public string RG { get; set;}
      }
  }
Listagem 9. Classe PessoaJuridica
 using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace ExemplosFundamentos.Heranca
  {
      public class PessoaJuridica:Pessoa
      {
          public string CNPJ { get; set; }
   
          public string InscricaoEstadual { get; set; }
      }
  }

Nas Listagens 8 e 9 temos nossas classes filhas(especialistas), PessoaFisica e PessoaJuridica, onde temos a definição dos atributos referentes à documentação de cada uma delas. Observe que declaramos apenas aquilo que é exclusivo e pertinente a cada uma delas, porém ambas possuem Nome, Endereco e Pais, pois herdam de Pessoa.

Nos exemplos acima, colocamos apenas um nível de hierarquia, porém não há limites impostos pelo framework para isso, poderíamos, por exemplo, criar outras classes, FornecedorNacional e FornecedorInternacional herdando de PessoaJuridica. Vale ressaltar o bom senso e a análise de cada caso, para evitarmos criar estruturas desnecessariamente complexas e acopladas em nossos sistemas.

Outro ponto interessante para citar a respeito de herança é sobre as regras de casting da mesma. Utilizando herança, é possível atribuirmos uma instância de um objeto filho à uma variável declarada do tipo de um objeto pai, como podemos ver na Listagem 10.

Listagem 10. Cast implícito - Herança

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.Heranca;
  
  namespace ExemplosFundamentos
  {
      class Program
      {
          static void Main(string[] args)
          {
              Pessoa pessoa = new Pessoa();
              Pessoa pessoaA = new PessoaFisica();
              Pessoa pessoaB = new PessoaJuridica();            
             (pessoaA as PessoaFisica).CPF = "9999999";
             Console.WriteLine((pessoaA as PessoaFisica).CPF);
             Console.ReadLine();
          }
      }
  }

Observe na Listagem 10, nas linhas 13, 14 e 15, que temos três variáveis declaradas e cada uma recebe uma instância de um tipo diferente. Isso é possível, pois todos as instâncias usadas são da hierarquia de Pessoa (usada na declaração da variável). Por outro lado, no caso de pessoaA por exemplo, não conseguiremos acessar as propriedades específicas de PessoaFísica pois a variável é do tipo Pessoa.

Para acessarmos as características de PessoaFisica na variável pessoaA, teríamos que fazer um cast explícito com o operador as, como mostrado nas linhas 16, 17. Vale ressaltar que o cast explícito só será validado em tempo de execução, podendo gerar um erro em tempo de execução caso seja realizado um cast para um tipo diferente do tipo da instância. Por exemplo, se fizéssemos (pessoaA as PessoaJuridica).CNPJ = "5555555", obteríamos um erro em tempo de execução.

Nota do DevMan

Type casting é como chamamos a conversão de um determinado tipo para outro. Vale ressaltar que só é possível realizar o typecasting de elementos compatíveis com a instância da declaração.

Virtual x Override

Agora que já vimos os conceitos fundamentais da orientação a objetos, o que é uma classe, um objeto e como funciona a herança, podemos falar sobre as palavras reservadas virtual e override.

Como vimos, podemos criar estruturas hierárquicas nas classes, reaproveitando comportamento e estado das classes superiores, porém, algumas vezes é preciso sobrescrever determinados comportamentos da classe pai nas classes filhas, aplicando assim o conceito de polimorfismo.

Em orientação a objetos, dizemos que um método é sobrescrito quando a implementação dele prevalece com relação à implementação de uma classe pai.

As palavras reservadas virtual e override se aplicam exatamente a isso. Devemos usar a palavra reservada virtual para indicar que um método pode ser sobrescrito, ou seja, na classe pai. E a palavra reservada override para indicar que o método está sobrescrevendo o método da classe pai.

Vamos imaginar o seguinte cenário para nosso exemplo. Temos uma classe conta bancária, que possui os atributos de uma conta normal, agencia, numero da conta e dígito. A ideia é que esta classe represente uma conta comum. Além disso, esta classe tem um método GetTarifaManutencao que nos dirá qual o valor da tarifa mensal desta conta, conforme podemos ver na Listagem 11.

Listagem 11. Classe ContaBancaria

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.VirtualOverride
  {
      public class ContaBancaria
      {
          public int Agencia { get; set; }
  
          public int Conta { get; set; }
  
          public int Digito { get; set; }
  
          public double GetTarifaManutencao() {
              return 30;
          }
      }
  }

Além da ContaBancaria, teremos outras duas classes, ContaEspecial e ContaUniversitaria. A ideia é que estas tenham tarifas de manutenção diferenciadas da conta comum. Estas classes irão herdar de ContaBancaria e também terão o método GetTarifaManutencao(), porém por enquanto não vamos definir os mesmos como virtual/override, como podemos ver na Listagem 12 e 13.

Listagem 12. Classe ContaEspecial

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.VirtualOverride
  {
      public class ContaEspecial:ContaBancaria
      {
          public double GetTarifaManutencao()
          {
              return 20;
          }
      }
  }
Listagem 13. Classe ContaUniversitaria

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.VirtualOverride
  {
      public class ContaUniversitaria:ContaBancaria
      {
          public double GetTarifaManutencao()
          {
              return 10;
          }
      }
  }

Nas Listagens 12 e 13, temos as classes mais especializadas com as tarifas específicas de manutenção. Observe que não sobrescrevemos os métodos, apenas declaramos com o mesmo nome da classe pai. Vamos agora consumir estes objetos para ver o comportamento deles. Na Listagem 14 temos o código para consumir nossos objetos. Observe que declaramos três objetos ContaBancaria, sendo contaBancariaA, contaBancariaB e contaBancariaC. Porém, temos uma instância de ContaBancaria, uma instância de ContaUniversitaria e uma instância de ContaEspecial.

Listagem 14. Classe ContaUniversitaria
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.VirtualOverride;
  
  namespace ExemplosFundamentos
  {
      class Program
      {
          static void Main(string[] args)
          {
              ContaBancaria contaBancariaA = new ContaBancaria();
              Console.WriteLine("contaBancariaA: " + contaBancariaA.GetTarifaManutencao());
              Console.WriteLine("...............");
              ContaBancaria contaBancariaB = new ContaUniversitaria();
              Console.WriteLine("contaBancariaB: " + contaBancariaB.GetTarifaManutencao());
              Console.WriteLine("...............");
              ContaBancaria contaBancariaC = new ContaEspecial();
              Console.WriteLine("contaBancariaC: " + contaBancariaC.GetTarifaManutencao());
              Console.WriteLine("...............");
              Console.ReadLine();          
          }
      }
  }
Resultado da execução
Figura 6. Resultado da execução da Listagem 11 sem Virtual-Override.

Observando a Figura 6 podemos ver que apesar de termos criado os objetos de ContaUniversitaria e ContaEspecial, o método que foi executado foi o da ContaBancaria, nos três casos. Isso ocorreu porque nós não criamos uma cadeia polimórfica para este método na nossa hierarquia de objetos ContaBancaria. Para criar esta cadeia, precisamos declarar o método GetTarifaManutencao como virtual na classe ContaBancaria e declará-lo como override nas classes filhas. Realizando estas alterações, código ficaria assim:


  ContaBancaria
          public virtual double GetTarifaManutencao() {
              return 30;
          }
   
  ContaUniversitaria
  public override double GetTarifaManutencao(){
              return 10;
          }
   
  ContaEspecial
  public override double GetTarifaManutencao()
          {
              return 30;
          }

Com estas definições criamos nossa cadeia polimórfica. Agora se executarmos novamente o código da Listagem 14 teremos o resultado mostrado na Figura 7.

Resultado da execução
Figura 7. Resultado da execução da Listagem 14 com Virtual-Override

Observando a Figura 7 percebemos que agora foi executada a implementação de cada classe filha devido ao override. Com este exemplo, fica mais fácil de compreender que, caso o método seja sobrescrito em uma classe filha (assinado com a palavra reservada override), ele sempre prevalecerá com relação ao método de sua classe pai.

Modificadores de Acesso

Os modificadores de acesso (Access modifiers) são palavras reservadas do C# para determinar o nível de acesso que um determinado membro de um tipo terá. Os modificadores de acesso são fundamentais para garantir o encapsulamento da orientação a objetos. Os modificadores de acesso são aplicados aos elementos das classes e structs, em seus respectivos métodos e atributos. O C# possui os seguintes modificadores de acesso:

  • Public – Este é o nível mais baixo de restrição. Ele indica que o elemento não terá restrição de acessos, ou seja, qualquer classe/struct que o conheça poderá acessá-lo.
  • Protected – Este nível indica que o elemento só poderá ser acessado de dentro da própria classe/struct ou a partir de uma classe descendente.
  • Internal - Este nível de acesso indica que o elemento só poderá ser acessado de dentro do assembly em que foi declarado.
  • Protected Internal –Este nível indica que o elemento só poderá ser acessado por classes do mesmo assembly, e só poderá ser acessado por classes de assemblies diferentes quando estas forem descendentes da classe onde fora declarado o elemento.
  • Private – Indica que o elemento só poderá ser acessado de dentro da própria class/struct em que for declarado.
Um assembly é uma unidade básica gerada pela compilação de aplicações no .NET. Servem para facilmente distribuir módulos e executáveis que podem ser versionados no GAC. O GAC por sua vez serve para terminar com o problema existente de múltiplas versões da mesma DLL rodando no S.O. Para saber mais temos um artigo aqui na plataforma.
Listagem 15. Exemplo de Modificadores de acesso em C#

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.Modificadores
  {
      public class Pessoa
      {
          public string Nome { get; set; }
  
          protected int Idade { get; set; }
  
          private double Rendimento { get; set; }
  
          protected internal string Endereco { get; set; }
  
          internal string Estado { get; set; }
      }
  }

Na Listagem 15, temos a aplicação de todos os modificadores citados acima. Com esta declaração teríamos o seguinte cenário:

Listagem 16. Classe sendo consumida no próprio assembly

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using ExemplosFundamentos.Modificadores;
 
 namespace ExemplosFundamentos
 {
     class Program
     {
         static void Main(string[] args)
         {
             Pessoa pessoa = new Pessoa();
             pessoa.Nome = "Ricardo";
             pessoa.Estado = "RJ";
             pessoa.Endereco = "Rua XPTO";
         }
     }
 }

Na Listagem 16 temos a nossa primeira simulação de uso do nosso objeto pessoa. Imaginemos uma classe consumidora (no mesmo assembly de Pessoa), criando um objeto do tipo pessoa. Neste caso, só conseguimos acessar os atributos Nome, Estado e Endereco, pois os mesmos são Public e Internal.

Ao tentarmos criar um objeto Pessoa (linhas 13 a 16) e popular o mesmo em um outro assembly, obteremos um erro de compilação, indicando que não podemos acessar as propriedades Estado e Endereco, devido ao fato das mesmas terem o modificador Internal.

Por outro lado, na Listagem 17 temos a declaração de uma classe PessoaFisica, que herda de pessoa. Vamos imaginar que esta classe está em um assembly diferente do de Pessoa.

Listagem 17. Classe PessoaFisica herdando de Pessoa, em outro assembly

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.Modificadores;
  
  namespace ExemplosFundamentos.V2
  {
      public class PessoaFisica:Pessoa
      {
          public PessoaFisica(){
              base.Nome = "Ricardo";
              base.Idade = 25;
              base.Endereco = "Rua XPTO";
              base.Estado = "RJ";
          }
      }
  }

No caso da Listagem 17, conseguiremos acessar Endereco, pois ele é Internal Protected, o que significa que este só poderá ser acessado em outros assemblies quando estiver classes descendentes.

Assim como no caso das properties, que exemplificamos acima, os modificadores de acesso também podem ser aplicados a métodos da classe/struct, seguindo as mesmas regras das listagens explicadas acima. Inclusive, eles podem ser aplicados a métodos construtores. Há casos em que queremos obrigar que a classe consumidora, ao instanciar um novo objeto passe determinados parâmetros, então colocamos o construtor default como private para que ele não possa ser acessado, como mostrado na Listagem 18.

Listagem 18. Construtor padrão privado

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.Modificadores;
  
  namespace ExemplosFundamentos.V2
  {
        public class PessoaFisica:Pessoa
        {
             private PessoaFisica(){
        
             }
  
             private PessoaFisica(string nome, int idade, string endereco, string estado){
                  this.Nome = nome;
                  this.Idade = idade;
                  this.Endereco = endereco;
                  this.Estado = estado;  
              }        
         }
  }

Na Listagem 18 podemos ver que estamos obrigando que o objeto PessoaFisica seja criado passando-se o nome, idade, endereço e estado como parâmetros.

Outro caso muito comum de uso do construtor privado é na implementação do design pattern Singleton.

Nota do DevMan

Singleton é um design pattern que prevê a criação de apenas uma instância de um determinado objeto. Para isso, os construtores são definidos como private e é criado um atributo estático de referê ncia da própria classe, além de um método, normalmente chamado GetInstance. Na implementação do GetInstance, é verificado se o objeto da variável estática existe ou não, caso não exista, ele é instanciado, caso exista ele é simplesmente retornado. Podemos observar um exemplo de implementação do Singleton na Listagem 19.

Listagem 19. Exemplo de construtor privado com Singleton

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 
 namespace ExemplosFundamentos.V2
 {
     public class Conexao
     {
         private static Conexao _conexao { get; set; }
 
         public string DataBasePath { get; set; }
 
         public string UserName { get; set; }
 
         public string PassWord { get; set; }
 
         private Conexao()
         {
 
         }
 
         public static Conexao GetInstance(){
             if(_conexao == null)
                 _conexao = new Conexao();
 
             return _conexao;
         }
     }
 }

Modificador Abstract

Este modificador pode ser aplicado tanto a classes quanto a métodos. Quando utilizamos ele, estamos dizendo ao .NET que o elemento em questão é um elemento abstrato e que não terá implementação, ficando a mesma a cargo das classes filhas. Quando este modificador é usado em uma classe, estamos dizendo que a mesma não pode ser instanciada.

Além disso, um método só poderá ser definido como abstract se a classe em que ele estiver contido for uma abstract class, como podemos ver na Listagem 20.

Listagem 20. Exemplo de abstract class

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace ExemplosFundamentos.AbstractExemplo
  {
      public abstract class ClasseBase
      {
          public abstract string GerarIdentificador();
      }
  }

Se tentarmos instanciar um objeto do tipo ClasseBase (como no código abaixo), obteremos o seguinte erro:


Cannot create an instance of the abstract class or interface 'ExemplosFundamentos.AbstractExemplo.ClasseBase'

Outra característica importante de ressaltar é que os métodos definidos como abstract, precisam obrigatoriamente ser implementados nas classes descendentes, fazendo com que tenhamos um comportamento semelhante ao da interface. Além disso, como existe esta obrigatoriedade, tais métodos são definidos internamente como virtuais pelo .NET. Na Listagem 21 vemos um exemplo de classe descendente de uma classe abstrata. Observe que se não implementarmos o método GerarIdentificador na classe cliente, obteremos uma mensagem de erro de compilação.

Listagem 21. Exemplo de abstract class
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.AbstractExemplo
  {
      public class Cliente : ClasseBase
      {
          public string Nome { get; set; }
  
          public string CPF { get; set; }
  
  
          public override string GerarIdentificador()
          {
              return CPF;
          }
      }
  }

A seguir segue o erro apresentado caso o GerarIdentificador não seja implementado na classe descendente.

'ExemplosFundamentos.AbstractExemplo.Cliente' does not implement inherited abstract member 'ExemplosFundamentos.AbstractExemplo.ClasseBase.GerarIdentificador()'

Métodos abstratos são muito comuns quando estamos implementando o design pattern TemplateMethod.

Template Method é um design pattern que prevê a quebra de pedaços de um determinado algoritmo em partes abstratas, de forma que a classe base não necessite conhecer sua total implementação, mas apenas o roteiro do mesmo. Na implementação do template method, a classe base pode ter a implementação de algumas partes do algoritmo e delegar a responsabilidade de implementação de outras partes para classes filhas.

Podemos exemplificar o uso do template method na Listagem 22, imagine um cenário hipotético onde teríamos a classe venda, que teríamos que aplicar descontos diferenciados para cada tipo de venda. Por exemplo, venda a vista teria 10 % de desconto e venda a prazo teria 5% de desconto. No nosso exemplo, teríamos uma classe pai, chamada venda, e duas classes filhas, chamadas VendaVista, VendaPrazo respectivamente.

Listagem 22. Exemplo de Template Method - Classe pai
 
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using ExemplosFundamentos.SealedClass;
  
  namespace ExemplosFundamentos.AbstractExemplo
  {
      public abstract class Venda
      {
         public DateTime DataVenda { get; set; }
  
          public String Vendedor { get; set; }
  
          public IList<Produto> Produtos { get; set; }
  
          public double  CalcularValorLiquidoVenda() {
              double desconto = CalcularDescontoCliente();
  
              double valorTotalProdutos = CalcularTotalProdutos();
  
              return valorTotalProdutos - desconto;
          }
  
          public abstract double CalcularDescontoCliente();
  
          public double CalcularTotalProdutos(){
              double total = 0;
              
              foreach(Produto produto in Produtos){
                  total += produto.Valor;
              }
  
              return total;
          }
      }
  }

Observe na Listagem 22, que a classe venda conhece o algoritmo para calcular o valor liquido de uma venda, mas desconhece o algoritmo para calcular o desconto do tipo de venda. Na linha 17, perceba que fazemos uso do método CalcularDescontoCliente, mesmo este sendo abstract (veja linha 23).

Na Listagem 23 temos a implementação deste método nas classes filhas.

Listagem 23. Exemplo de Template Method - Classes filhas

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.AbstractExemplo
  {
        public class VendaVista:Venda
        {
               public override double CalcularDescontoCliente()
               {
                      return 5;
               }
        }
  
        public class VendaPrazo : Venda
        {
               public override double CalcularDescontoCliente()
               {
                      return 10;
               }
         }
  }

Modificador Sealed

Este modificador é bem simples. Ele indica que a classe em questão não pode ser herdada. Caso você tente criar uma classe filha de um uma Sealed class, obterá um erro em tempo de compilação.

Listagem 24. Exemplo de sealed class

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.SealedClass
  {
      public sealed class Produto
      {
          public string Descricao { get; set; }
  
          public double Valor { get; set; }
      }
  }

Na Listagem 24 temos um exemplo de uso da palavra reservada sealed. Neste caso, se tentássemos criar uma classe herdando de Produto, obteríamos o seguinte erro de compilação:

'ExemplosFundamentos.SealedClass.ProdutoIndustrializado': cannot derive from sealed type 'ExemplosFundamentos.SealedClass.Produto'

Outro ponto importante a ressaltar, é que uma classe marcada como Abstract não pode ser sealed, visto que a classe abstract necessita de classes descendentes para implementar seus métodos abstratos, enquanto a classe sealed não permite isso.

Modificador Partial

O C# nos permite separar um mesmo tipo, seja uma classe, uma interface ou um struct, em diversos arquivos diferentes através do modificador partial. Com ele, podemos criar uma classe Cliente, em um arquivo, inserir algumas funcionalidades, e criar o arquivo Cliente2 com outros métodos.

Listagem 25. Exemplo de Partial Class

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  
  namespace ExemplosFundamentos.PartialClass
  {
      public partial class Fornecedor
      {
          public Boolean IsCNPJValido() { 
              //Validacao de CNPJ
              return true;
          }
      }
  
      public partial class Fornecedor
      {
          public Boolean IsFornecedorEstrageiro()
          {
              //Validacao de Fornecedor Estrangeiro
              return true;
          }
      }
  
  }

Na Listagem 25 nós temos um exemplo de definição de partial class. Nela temos a definição da classe Fornecedor duas vezes. Ao compilar este código, o C# irá entender que temos apenas uma classe Fornecedor, e tanto o método IsCNPJValido quanto o método IsFornecedorEstrageiro estarão presentes na mesma pois no final das contas, a classe Fornecedor vai ser fruto da combinação de todas as definições de partial class dela. O recurso de partial class é usado pelo próprio .NET na criação de Windows Forms, para separar o código referente aos controles inseridos na tela(arquivo .designer.cs, gerado pelo Visual Studio) do código digitado pelo programador(arquivo .cs).

Existem algumas ponderações importantes com relação ao modificador partial, sendo elas:

  • Se qualquer uma das partes for definida como abstract, Sealed ou herdar de uma classe pai, toda a classe terá tais características.
  • Ele só pode ser aplicado às classes, interfaces e structs.

Conclusão

Neste artigo tivemos uma introdução a alguns conceitos de orientação a objetos, assim como algumas palavras reservadas do C# que tem alguma relação com os conceitos abordados de orientação a objetos.

A orientação a objetos é um paradigma não tão simples de ser explicado e compreendido. É preciso tempo e dedicação para que se consiga absorver este paradigma e pensar orientado a objetos. Neste artigo apresentamos alguns conceitos básicos, iniciais, porém fundamentais para novos estudos e avanços no paradigma.

Após compreender os conceitos e fundamentos da orientação a objetos, você pode começar a aplicá-los em seus projetos, mesmo que em pequenos pedaços, quanto antes você praticar, mais rápido irá dominar e evoluir no assunto.

Além disso, pode começar a pesquisar a respeito de boas práticas de desenvolvimento orientado a objetos como, por exemplo, os princípios SOLID, GRASP e o DDD.

A orientação a objetos é um pilar do desenvolvimento de software atual e as linguagens mais utilizadas hoje se baseiam neste pilar. Dominando os fundamentos da orientação a objetos ficará muito mais fácil para você aprender C#, Java e qualquer outra linguagem O.O.

Confira também