Este capítulo possui uma versão mais recente:

People think that computer science is the art of geniuses but the actual reality is the opposite, just many people doing things that build on each other, like a wall of mini stones.
Donald Knuth
Não é difícil encontrar programas escritos em linguagens que favorecem orientação a objetos que, embora compostos por classes, não são mais do que código procedural escrito de maneira diferente. Orientação a objetos implica em mais do que adotar técnicas de codificação, exige mudanças na forma como pensamos a solução de problemas.
0
Considerações?x

Para começar, o bom design orientado a objetos distingue o projeto das interfaces e das implementações. Pensar em interfaces implica em pensamento abstrato, enquanto a implementação é mais concreta.

Código orientado a objetos bem-feito geralmente é mais simples de manter, testar e evoluir. Entretanto, código orientado a objetos não significa, necessariamente, menos código. 
0
Por favor, deixe seu feedback aquix

Um código procedural escrito com classes

O que segue é um bom exemplo de código que, embora esteja escrito com classes é procedural. Seu propósito é “importar” para dentro do banco de dados uma relação de funcionários, com os atributos id, nome salário.

using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;

public class EmployeesImporter
{
  public void Import(Stream employeesStream)
  {
    using var reader = StreamReader(employeeStream);
    string line;
    var linesCount = 0;
	
    using var dbconn = new SqlConnection(“Data Source=(local), InitialCatalog, Integrated Security = True”); 
    dbconn.Open();
		
    using var transaction = dbconn.BeginTransaction();
		
    while ((line = reader.ReadLine()) != null)
    {
      linesCount ++;
      var fields = line.Split(new [] {‘,’});
		
      if (fields.Length != 3)
      {
        Console.WriteLine($“Line {linesCount}: invalid record”);
        continue;
      } 
			
      if (fields[0].Length != 6) // id should be 6 chars long
      {
        Console.WriteLine($”Line {linesCount}: invalid record.”);
        continue;
      }
			
      decimal salary; 
      if (@decimal.TryParse(fields[2], out salary))
      {
        Console.WriteLine($”Line {linesCount}: invalid record”);
        continue;
      }
		  
      var e = new EmployeeRecord(fields[0], fields[1], salary);

      var command = dbconn.CreateCommand();
      command.Transaction = transaction;
      command.CommandType = CommandType.StoredProcedure;
      command.CommandText = “dbo.InsertEmployee”;
      command.Parameters.AddWithValue(“@id”, e.Id);
      command.Parameters.AddWithValue(“@name”, e.Name);
      command.Parameters.AddWithValue(“@salary”, e.Salary);
      command.ExecuteNonQuery();
    }

    transaction.Commit();
    dbconn.Close();  
  }
}

public struct EmployeeRecord
{
  public EmployeeRecord(string id, string name, decimal salary)
    => (Id, Name, Salary) = (id, name, salary);
		
  public string Id { get; } 
  public string Name { get; } 
  public decimal Salary { get; }	
}

Este código, embora correto, tem, sob o ponto de vista orientado a objetos uma série de problemas de design, revelados em dificuldades para o manter e evoluir:

  1. Há forte acoplamento com uma forma e formato de receber dados. Espera-se um stream e que o conteúdo sejam linhas em formato CSV.
  2. Há forte acoplamento com uma tecnologia específica de persistência. No exemplo, um banco de dados SQL.
  3. Há forte acoplamento com uma base de dados específica, identificada por uma string de conexão.

A combinação desses diversos “acoplamentos” compromete a testabilidade e, em consequência, o evolvabilty.

Interessante observar que a interface dessa classe é mínima (há apenas um método). Mas, esta interface acaba escondendo “coisas demais” em uma única implementação que está, perceptivelmente, sobrecarregada. Essas características tornam esse código um ótimo candidato a refatoração.

Refactoring

Refatoração é um processo sistemático de melhoria de código sem criar novas funcionalidades que podem transformar uma bagunça em código limpo e design simples.

Distinguindo interface e implementação

Antes de conseguirmos melhorar o design do código anterior, precisamos afirmar alguns conceitos. Para começar, precisamos entender com mais clareza a distinção entre interfaces e implementações.

Em OO, uma interface, abstrata, constitui o conjunto de métodos (comportamento) e atributos (estado)  expostos (públicos) de um objeto. É através dela que ocorrem as interações com outros objetos através de mensagens. Trata-se de uma espécie de “contrato”. No exemplo, anterior, a interface da classe EmployeesImporter é composta apenas por um método: Import

Muitas linguagens de programação, incluindo C# e Java, oferecem recursos para planejamento de interfaces especificamente, permitindo referência a elas, o que contribui para redução do acoplamento aferente para implementações concretas.

public interface IEmployeesImporter
{
  void Import(Stream source);
}

O prefixo 'I' no nome de interfaces

Uncle Bob, no clássico Clean Code, condena a utilização de adornos como o prefixo ‘I’ no nome de interfaces e apresenta bons argumentos para sua posição. Entretanto, em C#, a utilização do prefixo tornou-se prática mais do que comum, principalmente por influência do próprio framework.

A implementação, concreta, é o código que “realiza” a interface além de todos os métodos (comportamentos) e atributos (estado) privados. Ou seja, que são invisíveis para os “clientes” do objeto.

Identificando os 'clientes' de um objeto


Há duas interpretações distintas para “cliente”. A primeira, versa sobre todo código que interage com os objetos de uma determinada classe. A segunda, indica os programadores que escrevem esses códigos.

Boas interfaces são concisas e fáceis de entender. Elas comunicam com clareza a intencionalidade, mitigando riscos de interpretação e, consequentemente, erros em sistemas.

Uma boa interface oculta de maneira eficaz detalhes ou indicações de implementação evitando que eventuais mudanças concretas não impliquem em necessidade de mudança ou trabalho para os clientes.
0
Considerações?x

Uma interface, múltiplas implementações possíveis

Dependendo da natureza da interface, ela poderá contar com muitas implementações.
0
Considerações?x

Desviando-nos, por agora, do código inicial desse post, considere a seguinte interface:

public interface ICache<T>
{
  void Set(string key, T valeu);
  bool TryGet(string key, out T value);
}

Note que essa interface é genérica ao ponto de permitir várias implementações. Abaixo um exemplo de implementação in-memory.

using System;
using System.Collections.Generic;

public class InMemoryCache<T>
  : ICache<T>
{
  Dictionary<string, T> _data = new 
     Dictionary<string, T>();
		   
  public void Set(string key, T value) 
  {
    _data[key] = value;
  }
	
  public bool TryGet(string key, out T value)
    => _data.TryGetValue(key, out value);
}

Note que a preservação de uma interface permite diferentes implementações concretas sem que exista necessidade de mudanças  ou adaptações nos clientes. A implementação de cache in-memory in-process acima, por exemplo, poderia ser substituída  facilmente por outra, distribuída, como Redis.

using System;
using StackExchange.Redis;

public class RedisCache<T>
  : ICache<T>
{
  ConnectionMultiplexer _redis = 
    ConnectionMultiplexer.Connect("server1:6379,server2:6379");
		   
  public void Set(string key, T value) 
  {
    var o = JsonConvert.SerializeObject(value)
    var db = redis.GetDatabase();
    o.StringSet(key, o);
  }
	
  public bool TryGet(string key, out T value)
  {
     var db = redis.GetDatabase();
     var o = redis.StringGet(key);
     value = (o == null) ? default (T) : JsonConvert.DeserializeObject<T>(o);
     return o == null;
  }
}

Apartar interface (abstrata) de implementação (concreta) autoriza o desenvolvimento de soluções polimórficas, com mínimo impacto de acoplamento. Essa flexibilidade permite a construção de sistemas mais adaptáveis e úteis.

Uma interface, uma responsabilidade

Interfaces menores são, pelo menos em teoria, modificadas com menor frequência, colaborando para a estabilidade do código. Classes que implementam interfaces pequenas tendem a ser mais coesas.
0
Considerações?x

Uma boa orientação para elaboração de interfaces pequenas é o princípio da responsabilidade única (SRP), proposto por Uncle Bob na década de 1990.

Um módulo de software deve ter apenas uma única responsabilidade.

Adaptada para programação orientada a objetos, a proposição é que uma classe deve ter uma única responsabilidade, seja lá o que isso signifique.

Há um bocado de debate para definir o nível de granularidade para uma responsabilidade. Eu, pessoalmente, gosto de associar responsabilidade por “forças” que me levam a mudança do código. Quanto mais dessas “forças”, mais responsabilidades.
0
Considerações?x

O exemplo de código estruturado que inicia este capítulo mostra um método que assume muitas responsabilidades. Parte do pensamento da programação orientada a objetos consiste em distribuir responsabilidades por operações em conjuntos de objetos que cooperem efetivamente. Por exemplo, o parsing de cada linha CSV, parece mais natural a um método especialista de EmployeeRecord:

public struct EmployeeRecord
{
  public EmployeeRecord(string id, string name, decimal salary)
    => (Id, Name, Salary) = (id, name, salary);
		
  public string Id { get; } 
  public string Name { get; } 
  public decimal Salary { get; }
	
  public static bool TryParse(string data, out EmployeeRecord result)
  {
    var fields = data.Split(new [] {‘,’});
			
    if (fields.Length == 3 && fields[0].Length == 6 && decimal.TryParse(fields[2], out decimal salary))
    {
      result = new EmployeeRecord(fields[0], fields[1], salary);
      return true;
    }
	
    result = default(EmployeeRecord);
    return false;
  }
}

Um dos principais atributos dessa refatoração está na possibilidade de testar o parsing de dados de employees de maneira isolada. É consideravelmente mais fácil escrever rotinas automatizadas de teste para esse comportamento, afinal não há mais efeitos colaterais.

Interfaces consagradas consolidam boas práticas e padrões

Eventualmente, interfaces consagradas revelam boas práticas e padrões. Bons exemplos, são as interfaces IEnumerable<T> e IEnumerator<T>. Essas classes permitem a implementação de lazyness evaluation em .NET.

Lazyness Evaluation

Avaliação preguiçosa (também conhecida por avaliação atrasada) é uma técnica usada em programação para atrasar a computação até um ponto em que o resultado da computação é considerado necessário.

O código apresentado no início desse capítulo “lê” um stream identificando linhas CSV, interpretando-as, para, posteriormente, armazená-las no banco de dados. Poderíamos, ingenuamente, considerar uma refatoração onde processamos todo o stream para depois dispara a persistência. Entretanto, tal implementação seria uma mal ideia por materializar na memória, sem necessidade, todos os employees que precisam ser importados.

public class EmployeeRecordsEnumerableLoader
{
  public static IEnumerable<EmployeeRecord> Load(Stream stream)
  {
    var result = new List<EmployeeRecord>();
    using StreamReader _reader = new StreamReader(stream);
    int _linesCount = 0;
    string line;
	  
    while ((line = _reader.ReadLine()) != null)
    {
      _linesCount++; 
      if (!EmployeeRecord.TryParse(line, out var current)) 
      { 
        Console.WriteLine($"Line {_linesCount}: invalid record."); 
      }
      else
      {
        result.Add(current);
      }
    }
	  
    return result;
  }
}

Empregando as abstrações propostas nas interfaces IEnumerable<T> IEnumerator<T> podemos evitar tamanha carga. Como você poderá constatar, é muito mais código (e complexidade) para evitar desperdício de recursos computacionais.

public class EmployeeRecordsEnumerable
	: IEnumerable<EmployeeRecord>
{
  Stream _stream;
  public EmployeeRecordsEnumerable(Stream stream)
  	=> _stream = stream;
  
  public IEnumerator<EmployeeRecord> GetEnumerator()
  	=> new EmployeeRecordsEnumerator(_stream);
	
  IEnumerator IEnumerable.GetEnumerator()
  	=> new EmployeeRecordsEnumerator(_stream);
}

public class EmployeeRecordsEnumerator 
	: IEnumerator<EmployeeRecord>
{

  StreamReader _reader;
  int _linesCount;
  
  public EmployeeRecordsEnumerator(Stream stream)
  	=> _reader = new StreamReader(stream); 
  
  private EmployeeRecord _current;
  public EmployeeRecord Current
  {
    get => _current;
  }
  
  object IEnumerator.Current
  {
    get => _current;
  }
  
  public bool MoveNext()
  {
    string line;
    while ((line = _reader.ReadLine()) != null)
    {
      _linesCount++; 
      if (!EmployeeRecord.TryParse(line, out var _current)) 
      { 
        Console.WriteLine($"Line {_linesCount}: invalid record."); 
      }
      else
      {
        return true;
      }
    }
    return false;
  }

  public void Reset()
  {
    _reader.DiscardBufferedData();
    _reader.BaseStream.Seek(0, SeekOrigin.Begin);
    _current = default(EmployeeRecord);
  }

  public void Dispose()
  {
    _reader.Dispose();
  }
}

Essa implementação, agora, permite simplificação considerável da lógica de EmployeesImporter. Repare como o foreach entende e consegue lidar com as interfaces IEnumerable!

public class EmployeesImporter
{
  public void Import(Stream employeesStream)
  {
    using var dbconn = new SqlConnection(“Data Source=(local), InitialCatalog, Integrated Security = True”); 
    dbconn.Open();
    using var transaction = dbconn.BeginTransaction();
	
    var linesCount = 0;
    var source = new EmployeeRecordEnumerable(employeesStream);
		
    foreach (var e in source)
    {
      var command = dbconn.CreateCommand();
      command.Transaction = transaction;
      command.CommandType = CommandType.StoredProcedure;
      command.CommandText = “dbo.InsertEmployee”;
      command.Parameters.AddWithValue(“@id”, e.Id);
      command.Parameters.AddWithValue(“@name”, e.Name);
      command.Parameters.AddWithValue(“@salary”, e.Salary);
      command.ExecuteNonQuery();		
    }
    transaction.Commit();
    dbconn.Close();
  }
}

Concordemos, porém, que as implementações de IEnumerable<T> e IEnumerator<T> não são, evidentemente, fáceis. Felizmente, C#, oferece um “açúcar sintático” para simplificar essa escrita: yield return.

public class EmployeeRecordsEnumerableFactory
{
  public static IEnumerable<EmployeeRecord> Create(Stream stream)
  {
    using StreamReader _reader = new StreamReader(stream);
    int _linesCount = 0;
    string line;
    while ((line = _reader.ReadLine()) != null)
    {
      _linesCount++; 
      if (!EmployeeRecord.TryParse(line, out var current)) 
      { 
        Console.WriteLine($"Line {_linesCount}: invalid record."); 
      }
      else
      {
        yield return current;
      }
    }
  }
}

Problemas comuns geralmente tem implementações simples! O código acima faz com que o compilador gere “por baixo dos panos” implementações concretas para IEnumerable<T> e IEnumerator<T> que se comportam conforme a implementação indicada.

Implementações concretas devem ser uniformes no “nível de abstração”

Quando “apartamos” a leitura das linhas do stream e o parsing dos dados, simplificamos consideravelmente a implementação do método Importer tornado-o uma espécie de “orquestrador abstrato” para atividades de nível mais baixo (mais concretas). Entretanto, o acesso ao banco de dados continua sendo feito diretamente. A separação dessa “responsabilidade” pode ser feita através da implementação do padrão Unit of Work.

Unit of Work

Unit of Work é um padrão de projeto para aplicações empresariais. A ideia desse padrão é manter uma lista de objetos relacionados a uma transação e coordenar sua persistência.

public class UnitOfWorkSql : IDisposable
{
  SqlConnection _dbconn;
  SqlTransaction _transaction;
  public UnitOfWorkSql()
  {
    _dbconn = new SqlConnection(“Data Source=(local), InitialCatalog, Integrated Security = True”); 
    _dbconn.Open();
    _transaction = _dbconn.BeginTransaction();
  }

  public void Add(EmployeeRecord record)
  {
    var command = _dbconn.CreateCommand();
    command.Transaction = _transaction;
    command.CommandType = CommandType.StoredProcedure;
    command.CommandText = “dbo.InsertEmployee”;
    command.Parameters.AddWithValue(“@id”, record.Id);
    command.Parameters.AddWithValue(“@name”, record.Name);
    command.Parameters.AddWithValue(“@salary”, record.Salary);
    command.ExecuteNonQuery();
  }

  public void SaveChanges()
  {
    _transaction.Commit();
    _transaction.Dispose();
    _transaction = _dbconn.BeginTransaction();
  }

  public void Dispose()
  {
    _transaction.Dispose();
    _dbconn.Close();
    _dbconn.Dispose();
  }
}

A adoção desse padrão permite retirar o código que lida com banco de dados da classe Importer tornando-a menos abstrata.

public class EmployeesImporter
{
  public void Import(Stream employeesStream)
  {
    using var uow = new UnitOfWorkSql();
    	
    var linesCount = 0;
    var source = EmployeeRecordEnumerable.Create(employeesStream);
		
    foreach (var e in source)
    {
      uow.Add(e);		
    }
    uow.SaveChanges();
  }
}

Invertendo o controle!

Se bem implementado, um código orientado a objetos terá, sempre menos dependência de implementações concretas e mais de abstrações (interfaces). Dessa forma, converte-se em excelente candidato para adoção de abordagens polimórficas.
0
Considerações?x

Considerando o exemplo que estamos trabalhando neste capítulo, a extração da interface do UnitOfWork, por exemplo, torna muito fácil gravar dados em outros formatos e lugares, além do banco de dados proposto originalmente. O “segredo” está em substituir a alocação dessa dependência pela possibilidade de recebê-la por parâmetro na implementação concreta da classe Importer.

public class EmployeesImporter 
{
  IUnitOfWork _uow;
  public EmployeesImporter(IUnitOfWork uow) 
    => _uow = uow; 
  
  public void Import(Stream employeesStream)
    => Import(EmployeeRecordEnumerable.Create(employeesStream));
  public void Import(IEnumerable source) 
  { 
    var linesCount = 0; 
    
    foreach (var e in source) 
    { 
      _uow.Add(e); 
    } 
    _uow.SaveChanges(); 
  } 
}

Repare que:

  1. Não há forte acoplamento com uma forma e formato de receber dados. Em nível mais alto, espera-se uma enumeração de EmployeeRecords.
  2. Não há forte acoplamento com uma tecnologia específica de persistência. Espera-se apenas um objeto que implemente uma UnitOfWork apropriadamente.
  3. Não há forte acoplamento com uma base de dados específica, ela ficou localizada na UnitOfWork podendo ser, ainda, abstraída.

Antes de avançar…

Design orientado a objetos de qualidade demanda experiência que só é obtida pela prática.

Caso tenha alguma experiência utilizando linguagens que favorecem orientação a objetos, submeta seus códigos a uma análise crítica para determinar se são, de fato OO ou, apenas código com classes. Se tiver tempo, busque implementar algumas refatorações.

Referências bibliográficas

ALBAHARI, Joseph. C# 9.0 in a Nutshell: the definitive reference. San Francisco, Ca: O’Reilly, 2021.

FOWLER, Martin. Refactoring: Improving the design of existing code. 2 ed. Boston, Ma: Addison-Wesley Professional, 2019.

FOWLER, Martin. Patterns of Enterprise Application Architecture. Boston, Ma: Addison-Wesley Professional, 2012.

MARTIN, Robert. Clean Architecture: a craftsman’s guide to software structure and design. Boston, Ma: Pearson Education, 2018.

MARTIN, Robert. Clean Code: A Handbook of Agile Software Craftsmanship. Boston, Ma: Pearson Education, 209.

WEISFELD, Matt. The Object-Oriented Thought Process. 5. ed. San Francisco, Ca: Addison-Wesley, 2019.

Compartilhe este capítulo:

Compartilhe:

Comentários

Participe da construção deste capítulo deixando seu comentário:

Inscrever-se
Notify of
guest
1 Comentário
Oldest
Newest Most Voted
Feedbacks interativos
Ver todos os comentários
Jerson Brito
Jerson Brito
3 anos atrás

Muito obrigado por mais um conteúdo de excelente qualidade.

Elemar Júnior

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mentorias

para arquitetos de software

Imersão, em grupo, supervisionada por Elemar Júnior, onde serão discutidos tópicos avançados de arquitetura de software, extraídos de cenários reais.

ElemarJúnior

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

TECH

&

BIZ

-   Insights e provocações sobre tecnologia e negócios   -   

55 51 9 9942 0609  |  me@elemarjr.com

55 51 9 9942 0609  |  me@elemarjr.com

bullet-1.png

55 51 9 9942 0609  |  me@elemarjr.com

1
0
Quero saber a sua opinião, deixe seu comentáriox
()
x