Quase todas as linguagens de programação suportam, em maior ou menor escala, tipos primitivos. Em C#, por exemplo, temos tipos como string
, long
, double
, int
, bool
, etc. Esses tipos servem como “blocos fundamentais” para a construção de representações mais complexas.
Python e tipos primitivos
Python não tem tipos primitivos, pelo menos, implementados da mesma forma que outras linguagens de programação. Afinal, na linguagem, mesmo tipos como int
, bool
são objetos. Entretanto, para os fins do propósito desse capítulo, podemos entender esses tipos “fundamentais” para Python, como primitivos, sem prejuízo de entendimento (apenas de rigor)
Em linguagens procedurais, é comum criar “rotinas no entorno” dos tipos primitivos. Por exemplo, a representação de um “funcionário” em sistemas de gestão, desenvolvido em linguagem procedural, não é mais que um conjunto de variáveis com tipos primitivos, eventualmente agrupados em alguma “estrutura”.
typedef struct { float Weight; int Age; float Height; } Person;
Os valores destas variáveis, ainda falando de programação procedural, são manipulados e validados por rotinas distintas (funções), não raro, “acopladas” com uma funcionalidade do software – um CPF, assim, geralmente é armazenado em uma variável string
, validada em funções acionadas nas rotinas que manipulem essa informação.
Já no paradigma orientado a objetos, há o encapsulamento – dados e comportamento não devem estar separados, mas combinados em objetos. Infelizmente, entretanto, há uma certa “obsessão” por continuar usando tipos primitivos de maneira semelhante a que acontece no paradigma procedural, levando a códigos que pecam por baixa coesão.
Outro problema comum de implementação, característico da obsessão de tipos primitivos, é a utilização de valores constantes numéricos como “códigos de retorno”. Muito mais interessante é a utilização de enumeradores.
Bons designs orientados a objetos evitam a obsessão por tipos primitivos.
Um exemplo de obsessão por primitivos
No exemplo que segue, temos uma implementação, comum, obsessiva por tipos primitivos.
public class Employee { public string Id { get; set; } public string Name { get; set; } string _cpf; public string Cpf { get { return _cpf; } set { if (Utils.CheckCpf(value)) { throw new ArgumentException(“...”); } _cpf = value; } } public decimal Salary { get; set; } }
No código de exemplo, o “tipo” associado a propriedade CPF é string
. Além disso, podemos observar a utilização de uma função, CheckCpf
, estática, implementada em uma “biblioteca” de propósito geral, nos mesmos moldes que ocorreria em um linguagem adequada ao paradigma procedural.
Substituindo tipos primitivos
A superação da obsessão por tipos primitivos começa pelo reconhecimento de elementos mais “sutis” na modelagem, como a representação de valores simples, também como objetos. Em DDD, esses objetos são identificados como value objects.
Criando tipos mais expressivos (que tipos primitivos)
A superação da “obsessão” por tipos primitivos começa pelo exercício de combinar estado e comportamento (por exemplo validações), em um objeto apropriado.
public struct Cpf { private readonly string _value; public Cpf(string value) => _value = value; public override string ToString() => _value; public static implicit operator Cpf(string value) => Parse(value); public bool IsValid { get { // validation logic; } } public bool IsEmpty => (_value != null); }
Na implementação de exemplo, deve chamar a atenção a utilização do modificador readonly
no atributo que representa o estado. A ideia é que, uma vez atribuído um valor a este atributo, ele não possa ser modificado. Objetos para representar value objects são naturalmente imutáveis, afinal, eles são “expressão de um valor”.
Ainda no código de exemplo, também deve chamar a atenção o fato de construirmos o tipo como uma struct
. As características de objetos definidos como struct
, em C#, tais como comparação por valor e alocação default na stack (em oposição a class
que define objetos que serão alocados na heap), eliminam potenciais impactos de performance e legibilidade. Também é interessante verificar a implementação de conversores implícitos para tipos primitivos, facilitando atualização das bases de código.
Finalmente, é interessante observar o encapsulamento em cena. Aliás, o estado (valor do Cpf), fica escondido no código de exemplo. A “visão externa”, quando acontece, é determinada pelo método ToString()
. Com alguma criatividade, podemos ampliar essa “visão externa”, por exemplo, criando métodos distintos para retornar um Cpf formatado ou não formatado. Afinal, o valor é o mesmo, exceto pela representação.
Adoção de Parse/TryParse
Uma vez que resolvemos criar objetos, é interessante os modelar seguindo alguns padrões vigentes. Em .NET, por exemplo, é comum a utilização do padrão Parse/TryParse
.
public struct Cpf { private readonly string _value; private Cpf(string value) => _value = value; public override string ToString() => _value; public static implicit operator Cpf(string value) => Parse(value); public static Cpf Parse(string value) => (TryParse(value, out var result)) ? result : throw new System.ArgumentException(nameof(value)); public static bool TryParse(string value, out Cpf result) { if (string.IsNullOrEmpty(value) || value.Length != 11) { result = default(Cpf); return false; } result = new Cpf(value); return true; } public bool IsEmpty => (_value != null); }
A classe Employee
, agora, pode ser muito mais enxuta.
public class Employee { public string Id { get; set; } public string Name { get; set; } public Cpf Cpf {get; set;} }
Evitar “obsessão” por tipos primitivos colabora para OCP
Aplicando a mesma ideia que usamos com Cpf
em Name
temos ainda mais possibilidades.
struct PersonName { public string FirstName {get; private set;} public string LastName {get; private set;} public PersonName(string value) { if (string.IsNullOrEmpty(value)) throw new ArgumentException(nameof(value)); int p = value.IndexOf(" "); if (p == -1) { throw new ArgumentException(nameof(value)); } FirstName = value.Substring(0, p); LastName = value.Substring(p + 1); } public override string ToString() => $"{FirstName} {LastName}"; public string ToLastFirstNameString() => $"{LastName}, {FirstName}"; public static implicit operator PersonName(string value) => new PersonName(value); }
O que fizemos, aqui, foi modificar a representação interna, para separar nome e sobrenome – bem mais poderoso que uma única string
. Essa visão, abre espaço para que novos comportamentos sejam adicionados com o tempo, sem mudar código cliente.
Indo “mais longe” com sobrecarga de operadores
Algumas linguagens com suporte ao paradigma orientado a objetos, como C#, oferecem suporte a sobrecarga de operadores.
public readonly struct Fraction { private readonly int _numerator; private readonly int _denominator; public Fraction(int numerator, int denominator) { if (denominator == 0) { throw new ArgumentException("Denominator cannot be zero.", nameof(denominator)); } _numerator = numerator; _denominator = denominator; } public static Fraction operator +(Fraction a) => a; public static Fraction operator -(Fraction a) => new Fraction(-a.num, a.den); public static Fraction operator +(Fraction a, Fraction b) => new Fraction( a._numerator * b._denominator + b._numerator * a._denominator, a._denominator * b._denominator ); public static Fraction operator -(Fraction a, Fraction b) => a + (-b); // ... public static implicit operator Fraction(int value) => new Fraction(value, 1); public override string ToString() => $"{_numerator} / {_denominator}"; public double ToDouble() => _numerator / (double) _denominator; }
Essa característica poderosa permite a construção de objetos ainda mais expressivos, sobretudo para tipos matemáticos.
var p1 = new Point3(2, 4, 5); var p2 = new Point3(1, 2, 3); System.Console.WriteLine(p1 * 3); struct Point3 { public double X { get; private set; } public double Y { get; private set; } public double Z { get; private set; } public Point3(double x, double y, double z) => (X, Y, Z) = (x, y, z); public static Point3 operator +(Point3 a, Point3 b) => new Point3(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static Point3 operator *(Point3 a, int m) => new Point3(a.X * m, a.Y * m, a.Z * m); public override string ToString() => $"{X}; {Y}; {Z}"; }
Java não tem suporte a sobrecarga de operadores.
Tornando o código mais expressivo com enumerações
É comum, em determinados contextos, utilizarmos numéricos representando valores constantes significativos. Por exemplo, dias da semana.
const int SUNDAY = 0; const int MONDAY = 1; const int TUESDAY = 2; const int WEDNESDAY = 3; const int THURSDAY = 4; const int FRIDAY = 5; const int SATURDAY = 6;
A adoção de constantes é um recurso poderoso, adotado por programadores utilizando linguagens procedurais, para substituir “valores mágicos” (valores adicionados diretamente no código, sem significado evidente).
Adotando enumerações
Em linguagens orientadas a objetos, uma abordagem ainda mais “expressiva” é consolidar todos esses valores em uma enumeração.
System.Console.WriteLine(DaysOfWeek.Sunday); enum DaysOfWeek { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }
Enumerações são tipos especiais de classes que são fáceis de definir e dão a ideia de “conjunto”.
Evenutalmente, uma enumeração poderá ter conversão facilitada através da especificação de valores numéricos de um determinado tipo.
enum DaysOfWeek : int { Sunday = 0, Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5 , Saturday = 6 }
Suportando combinações de valores com enumeradores de bits
Eventualmente, enumeradores podem ser utilizados como “mapas de bits” de forma a permitir atribuições de valores múltiplos.
[Flags]
enum DaysOfWeek : int { Sunday = 0b_0000_0001, // 1 Monday = 0b_0000_0010, // 2, Tuesday = 0b_0000_0100, // 4, Wednesday = 0b_0000_1000, // 8, Thursday = 0b_0001_0000, // 16, Friday = 0b_0010_0000, // 32, Saturday = 0b_0100_0000, // 64, Weekend = Saturday | Sunday }
A utilização de mapas de bits é um artifício poderoso para suportar “combinação de valores” e, potencialmente, reduzir carga de armazenamento e transporte de dados.
Antes de avançar…
Neste capítulo curto identificamos e mostramos alternativas para “superar” a obsessão por tipos primitivos.
A adoção das técnicas indicadas aqui, permite a construção de códigos mais flexívei, fáceis de entender. e organizar. Afinal, toda as operações relacionadas a um determinado dado ficam em um único lugar.Na prática, a adoção de encapsulamento dificulta a duplicação. A utilização de enumerações torna o código mais expressivo.
Recomendo a você considerar e implementar outros exemplos de tipos, para valores que, no paradigma procedural, costumam ficar “dispersos” em atributos e funções desacrupadas.
Excelente texto!
Uma duvida: o método IsEmpty da classe CPF não está invertido?
O correto seria: public bool IsEmpty => (_value == null);