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.
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:
- 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.
- Há forte acoplamento com uma tecnologia específica de persistência. No exemplo, um banco de dados SQL.
- 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.
public class Sorter
{
// public string[] QuickSort(string[] input)
string[] Sort(string[] input)
{
// ..
}
}
No código de exemplo, QuickSort é um nome ruim para o método por revelar o algoritmo que realizaria a operação de ordenação.
Generics como recurso para abstração
Generics é um importante recurso de abstração, disponível tanto em C# quanto em Java, é a escrita de classes, estruturas e métodos com parâmetros de tipo. Esse recurso diminui a duplicidade de código permitindo que uma mesma implementação concreta atue em mais de um contexto. Coleções que utilizam generics, por exemplo, permitem que o “cliente” especifique o tipo de objetos que ela armazena.
var os = new Generic<string>();
var oi = new Generic<int>();
os.Field = "Elemar";
oi.Field = 42;
System.Console.WriteLine($"os.Field = \"{os.Field}\"", );
System.Console.WriteLine($"os.Field.GetType() = {os.Field.GetType().FullName}");
System.Console.WriteLine($"oi.Field = \"{oi.Field}\"", );
System.Console.WriteLine($"oi.Field.GetType() = {oi.Field.GetType().FullName}");
public class Generic<T>
{
public T Field;
}
Uma interface, múltiplas implementações possíveis
Desviando-nos, por agora, do código inicial desse post, considere a seguinte interface:
public interface ICache<T>
{
void Set(string key, T value);
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 e 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
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.
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.
Outro aspecto importante a considerar aqui é que, na existência de uma classe para “representar” Employee, é natural que a lógica de validação esteja, então, nessa classe.
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> e 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 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 source = EmployeeRecordEnumerable.Create(employeesStream);
foreach (var e in source)
{
uow.Add(e);
}
uow.SaveChanges();
}
}
Invertendo o controle!
Uma boa orientação para adoção de abordagens polimórficas é o princípio da injeção de dependências (DIP).
Separe o comportamento extensível por trás de uma interface e inverta as dependências. (Uncle Bob)
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<EmployeeRecord> source)
{
foreach (var e in source)
{
_uow.Add(e);
}
_uow.SaveChanges();
}
}Outro aspecto importante a observar nesse código é o suporte da linguagem C#, que também existe em Java, de sobrecarga para métodos. Ou seja, a possibilidade de expressar um mesmo comportamento, em diferentes implementações, ajustando a lista de parâmetros. Essa capacidade, torna o código mais expressivo e permite a adoção do pensamento de que um mesmo comportamento pode ser disparado por mais de um tipo de mensagem.
Repare que:
- 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.
- Não há forte acoplamento com uma tecnologia específica de persistência. Espera-se apenas um objeto que implemente uma UnitOfWork apropriadamente.
- 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.


Sobrecarga de métodos! =)
Excelente o artigo Elemar! Obrigado!
PS: Poderia por favor arrumar esse nome da variavel na interface ICache<T>?
public interface ICache<T> { void Set(string key, T valeu); // <--------------------- bool TryGet(string key, out T value); }para
public interface ICache<T> { void Set(string key, T value); bool TryGet(string key, out T value); }Elemar, em determinadas partes do código você utiliza a instrução “var” e em outras “using var”, poderia complementar o texto com uma breve explicação sobre a diferença.
Obrigado!
Ótima aula Elemar, muito obrigado por dividir o conhecimento.
PS: No exemplo há a variável linesCount que é inicializada mas não é utilizada. Poderia realizar o ajuste?
Feito!