Conforme declarado em algumas respostas e comentários, os DTOs são apropriados e úteis em algumas situações, especialmente na transferência de dados através dos limites (por exemplo, serializando para JSON para enviar através de um serviço da Web). No restante desta resposta, eu vou ignorá-lo mais ou menos e falar sobre classes de domínio e como elas podem ser projetadas para minimizar (se não eliminar) getters e setters, e ainda ser úteis em um projeto grande. Também não falarei sobre por que remover getters ou setters, ou quando fazê-lo, porque essas são perguntas próprias.
Como exemplo, imagine que seu projeto seja um jogo de tabuleiro como xadrez ou navio de guerra. Você pode ter várias maneiras de representá-lo em uma camada de apresentação (aplicativo de console, serviço da web, GUI, etc.), mas também possui um domínio principal. Uma classe que você pode ter é Coordinate
, representando uma posição no quadro. A maneira "má" de escrever seria:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Vou escrever exemplos de código em C #, em vez de Java, por questões de brevidade e porque estou mais familiarizado com isso. Espero que isso não seja um problema. Os conceitos são os mesmos e a tradução deve ser simples.)
Remoção de incubadoras: imutabilidade
Enquanto getters e setters públicos são potencialmente problemáticos, os setters são muito mais "maus" dos dois. Eles também são geralmente os mais fáceis de eliminar. O processo é simples - define o valor de dentro do construtor. Qualquer método que tenha alterado o objeto anteriormente deve retornar um novo resultado. Assim:
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
Observe que isso não protege contra outros métodos da classe que mutam X e Y. Para ser mais rigorosamente imutável, você pode usar readonly
( final
em Java). Mas de qualquer maneira, se você torna suas propriedades realmente imutáveis ou apenas evita a mutação pública direta por meio de levantadores, ele faz o truque de remover seus levantadores públicos. Na grande maioria das situações, isso funciona muito bem.
Removendo Getters, parte 1: projetando o comportamento
O exposto acima é muito bom para os levantadores, mas em termos de levantadores, nós realmente nos acertamos no pé antes mesmo de começar. Nosso processo foi pensar no que é uma coordenada - os dados que ela representa - e criar uma classe em torno disso. Em vez disso, deveríamos ter começado com o comportamento que precisamos de uma coordenada. A propósito, esse processo é auxiliado pelo TDD, onde extraímos classes como essa quando precisamos delas, então começamos com o comportamento desejado e trabalhamos a partir daí.
Então, digamos que o primeiro lugar em que você precisou de um Coordinate
foi para detecção de colisão: você queria verificar se duas peças ocupam o mesmo espaço no tabuleiro. Aqui está o caminho "maligno" (construtores omitidos por brevidade):
public class Piece
{
public Coordinate Position {get; private set;}
}
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
}
//...And then, inside some class
public bool DoPiecesCollide(Piece one, Piece two)
{
return one.X == two.X && one.Y == two.Y;
}
E aqui está o bom caminho:
public class Piece
{
private Coordinate _position;
public bool CollidesWith(Piece other)
{
return _position.Equals(other._position);
}
}
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public bool Equals(Coordinate other)
{
return _x == other._x && _y == other._y;
}
}
( IEquatable
implementação abreviada para simplificar). Ao projetar para o comportamento, em vez de modelar dados, conseguimos remover nossos getters.
Observe que isso também é relevante para o seu exemplo. Você pode estar usando um ORM ou exibir informações do cliente em um site ou algo assim; nesse caso, algum tipo de Customer
DTO provavelmente faria sentido. Mas apenas porque seu sistema inclui clientes e eles são representados no modelo de dados não significa automaticamente que você deve ter uma Customer
classe em seu domínio. Talvez, ao criar um comportamento, surja um, mas se você quiser evitar getters, não o crie preventivamente.
Removendo Getters, parte 2: comportamento externo
Assim, o acima é um bom começo, mas mais cedo ou mais tarde você provavelmente vai correr em uma situação onde você tem o comportamento que está associado com uma classe, que de alguma forma depende do estado da classe, mas que não pertence na classe. Esse tipo de comportamento é o que normalmente reside na camada de serviço do seu aplicativo.
Tomando nosso Coordinate
exemplo, eventualmente você desejará representar seu jogo para o usuário, e isso pode significar atrair a tela. Você pode, por exemplo, ter um projeto de interface do usuário que Vector2
represente um ponto na tela. Mas seria inadequado para a Coordinate
turma se encarregar da conversão de uma coordenada para um ponto na tela - isso traria todos os tipos de preocupações de apresentação para o seu domínio principal. Infelizmente, esse tipo de situação é inerente ao design do OO.
A primeira opção , que é muito comumente escolhida, é apenas expor os malditos caçadores e dizer ao inferno com ela. Isso tem a vantagem da simplicidade. Mas, como estamos falando de evitar getters, digamos, por uma questão de argumento, rejeitamos esta e vemos quais outras opções existem.
Uma segunda opção é adicionar algum tipo de .ToDTO()
método à sua classe. Isso - ou similar - pode ser necessário de qualquer maneira, por exemplo, quando você deseja salvar o jogo, precisa capturar praticamente todo o seu estado. Mas a diferença entre fazer isso pelos seus serviços e apenas acessar o getter diretamente é mais ou menos estética. Ainda tem o mesmo "mal".
Uma terceira opção - que eu vi defendida por Zoran Horvat em alguns vídeos do Pluralsight - é usar uma versão modificada do padrão de visitantes. Esse é um uso e variação bastante incomum do padrão, e acho que a milhagem das pessoas variará enormemente sobre se está adicionando complexidade para nenhum ganho real ou se é um bom compromisso para a situação. A idéia é essencialmente usar o padrão de visitante padrão, mas fazer com que os Visit
métodos tomem o estado de que precisam como parâmetros, em vez da classe que eles estão visitando. Exemplos podem ser encontrados aqui .
Para o nosso problema, uma solução usando esse padrão seria:
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public T Transform<T>(IPositionTransformer<T> transformer)
{
return transformer.Transform(_x,_y);
}
}
public interface IPositionTransformer<T>
{
T Transform(int x, int y);
}
//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
private readonly float _tileWidth;
private readonly float _tileHeight;
private readonly Vector2 _topLeft;
Vector2 Transform(int x, int y)
{
return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
}
}
Como você provavelmente pode dizer, _x
e realmente_y
não está mais encapsulado. Poderíamos extraí-los criando um que apenas os retornasse diretamente. Dependendo do gosto, você pode achar que isso torna todo o exercício inútil.IPositionTransformer<Tuple<int,int>>
No entanto, com getters públicos, é muito fácil fazer as coisas da maneira errada, basta extrair dados diretamente e usá-los em violação do Tell, Don't Ask . Enquanto que usar esse padrão é realmente mais simples fazê-lo da maneira certa: quando você deseja criar um comportamento, você começará automaticamente criando um tipo associado a ele. As violações do TDA serão obviamente muito fedorentas e provavelmente exigirão uma solução melhor e mais simples. Na prática, esses pontos tornam muito mais fácil fazê-lo da maneira correta, OO, do que a maneira "má" que os criadores encorajam.
Finalmente , mesmo que inicialmente não seja óbvio, pode haver maneiras de expor o suficiente do que você precisa como comportamento para evitar a necessidade de expor o estado. Por exemplo, usando nossa versão anterior, Coordinate
cujo único membro público é Equals()
(na prática, seria necessária uma IEquatable
implementação completa ), você poderia escrever a seguinte classe na sua camada de apresentação:
public class CoordinateToVectorTransformer
{
private Dictionary<Coordinate,Vector2> _coordinatePositions;
public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
{
for(int x=0; x<boardWidth; x++)
{
for(int y=0; y<boardWidth; y++)
{
_coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
}
}
}
private static Vector2 GetPosition(int x, int y)
{
//Some implementation goes here...
}
public Vector2 Transform(Coordinate coordinate)
{
return _coordinatePositions[coordinate];
}
}
Acontece, talvez surpreendentemente, que todo o comportamento que realmente precisávamos de uma coordenada para alcançar nosso objetivo fosse a verificação da igualdade! Obviamente, essa solução é adaptada para esse problema e faz suposições sobre o uso / desempenho aceitável da memória. É apenas um exemplo que se encaixa nesse domínio de problema específico, em vez de um plano para uma solução geral.
E, novamente, as opiniões variarão sobre se, na prática, isso é desnecessário. Em alguns casos, nenhuma solução como essa pode existir ou pode ser proibitivamente estranha ou complexa; nesse caso, você pode reverter para as três acima.