Como você evita getters e setters?

85

Estou tendo um momento difícil com o design de aulas de maneira extraordinária. Eu li que os objetos expõem seu comportamento, não seus dados; portanto, em vez de usar getter / setters para modificar dados, os métodos de uma determinada classe devem ser "verbos" ou ações que operam no objeto. Por exemplo, em um objeto 'Conta', teríamos os métodos Withdraw()e Deposit()não setAmount()etc. Veja: Por que os métodos getter e setter são maus .

Assim, por exemplo, dada uma classe Customer que mantém muitas informações sobre o cliente, por exemplo, Nome, DOB, Tel, Endereço etc., como evitar um getter / setters para obter e definir todos esses atributos? Que método do tipo 'Comportamento' pode-se escrever para preencher todos esses dados?

IntelliData
fonte
3
veja também: Quando são Getters e Setters Justified
mosquito
8
Eu apontaria que, se você precisar lidar com a especificação do Java Beans , terá getters e setters. Muitas coisas usam o Java Beans (Expression Language em jsps) e tentar evitar isso provavelmente será ... desafiador.
4
... e, da perspectiva oposta do MichaelT: se você não estiver usando a especificação JavaBeans (e eu pessoalmente acho que você deve evitá-la, a menos que esteja usando seu objeto em um contexto em que seja necessário), não haverá necessidade de o "get" verruga em getters, especialmente para propriedades que não possuem nenhum setter correspondente. Eu acho que um método chamado name()on Customeré tão clara, ou mais clara, do que um método chamado getName().
Daniel Pryden
2
@ Daniel Pryden: nome () pode significar tanto para definir ou para obter ...?
IntelliData

Respostas:

55

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( finalem 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 Coordinatefoi 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;
    }
}

( IEquatableimplementaçã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 CustomerDTO 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 Customerclasse 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 Coordinateexemplo, 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 Vector2represente um ponto na tela. Mas seria inadequado para a Coordinateturma 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 Visitmé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, _xe 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, Coordinatecujo único membro público é Equals()(na prática, seria necessária uma IEquatableimplementaçã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.

Ben Aaronson
fonte
Lindamente atendido! Gostaria de aceitar, mas primeiro alguns comentários: 1. Eu acho que o toDTO () é ótimo, porque você não acessa o get / set, o que permite alterar os campos fornecidos ao DTO sem quebrar o código existente. 2. Diga ao cliente tem um comportamento bastante para justificar tornando-se uma entidade, como é que u acessar adereços para modificá-los, por exemplo, endereço / tel mudança etc.
IntelliData
@IntelliData 1. Quando você diz "alterar os campos", quer dizer alterar a definição da classe ou alterar os dados? O último pode ser evitado removendo setters públicos, mas deixando os getters, portanto o aspecto dto é irrelevante. O primeiro não é realmente a (inteira) razão pela qual os publicitários são "maus". Consulte programmers.stackexchange.com/questions/157526/… por exemplo.
Ben Aaronson
@ IntelliData 2. Isso é difícil de responder sem conhecer o comportamento. Mas, provavelmente, a resposta é: você não faria. Que comportamento uma Customerclasse poderia exigir e poder mudar seu número de telefone? Talvez o número de telefone do cliente seja alterado e eu precise persistir com essa alteração no banco de dados, mas nada disso é responsabilidade de um objeto de domínio que fornece comportamento. Essa é uma preocupação de acesso a dados e provavelmente seria tratada com um DTO e, por exemplo, um repositório.
Ben Aaronson
@IntelliData Manter os Customerdados do objeto de domínio relativamente recentes (em sincronia com o banco de dados) é uma questão de gerenciar seu ciclo de vida, que também não é de sua responsabilidade, e provavelmente acabaria novamente vivendo em um repositório, fábrica ou container IOC ou o que instancia Customers.
Ben Aaronson
2
Eu realmente gosto do conceito Design for Behavior . Isso informa a "estrutura de dados" subjacente e ajuda a evitar as classes anêmicas muito comuns e difíceis de usar. mais um.
Radarbob #
71

A maneira mais simples de evitar setters é entregar os valores ao método construtor quando você newcria o objeto. Esse também é o padrão usual quando você deseja tornar um objeto imutável. Dito isto, as coisas nem sempre são claras no mundo real.

É verdade que os métodos devem ser sobre comportamento. No entanto, alguns objetos, como o Cliente, existem principalmente para armazenar informações. Esses são os tipos de objetos que mais se beneficiam com getters e setters; se não houvesse necessidade de tais métodos, simplesmente os eliminaríamos completamente.

Leitura adicional
Quando justificadores e setters são justificados

Robert Harvey
fonte
3
Então, por que todo o hype sobre 'Getter / Setters' é mau?
IntelliData
46
Apenas a pontificação usual de desenvolvedores de software que deveriam saber melhor. Para o artigo que você vinculou, o autor está usando a frase "getters e setters são maus" para chamar sua atenção, mas isso não significa que a afirmação seja categoricamente verdadeira.
Robert Harvey
12
@IntelliData: algumas pessoas dirão que o próprio Java é ruim.
null
18
@MetaFightsetEvil(null);
4
Certamente eval é o mal encarnado. Ou um primo próximo.
bispo
58

É perfeitamente bom ter um objeto que expõe dados em vez de comportamento. Apenas chamamos de "objeto de dados". O padrão existe sob nomes como Objeto de Transferência de Dados ou Objeto de Valor. Se o objetivo do objeto é armazenar dados, getters e setters são válidos para acessar os dados.

Então, por que alguém diria que "os métodos getter e setter são maus"? Você verá muito isso - alguém toma uma orientação que é perfeitamente válida em um contexto específico e depois remove o contexto para obter um título mais contundente. Por exemplo, " favorecer a composição sobre a herança " é um bom princípio, mas logo alguém removerá o contexto e escreverá " Por que estender é mau " (ei, mesmo autor, que coincidência!) Ou " herança é ruim e deve ser destruído ".

Se você olhar para o conteúdo do artigo, ele realmente tem alguns pontos válidos, apenas o amplia para criar um título de clique atraente. Por exemplo, o artigo afirma que os detalhes da implementação não devem ser expostos. Estes são os princípios de encapsulamento e ocultação de dados que são fundamentais no OO. No entanto, um método getter não expõe, por definição, detalhes de implementação. No caso de um objeto de dados do Cliente , as propriedades de Nome , Endereço etc. não são detalhes da implementação, mas toda a finalidade do objeto e devem fazer parte da interface pública.

Leia a continuação do artigo ao qual você vincula, para ver como ele sugere realmente definir propriedades como 'nome' e 'salário' em um objeto 'Empregado' sem o uso de criminosos. Acontece que ele usa um padrão com um 'Exportador', preenchido com métodos chamados add Name, add Salary, que por sua vez define campos com o mesmo nome ... Então, no final, ele acaba usando exatamente o padrão setter, apenas com um convenção de nomenclatura diferente.

É como pensar que você evita as armadilhas dos singletons, renomeando-os para que possam ser apenas uma coisa, mantendo a mesma implementação.

JacquesB
fonte
Além disso, os chamados especialistas parecem dizer que esse não é o caso.
IntelliData
8
Por outro lado, algumas pessoas disseram 'Nunca use OOP': prejudicial.cat-v.org/software/OO_programming #
9276 JacquesB
7
Na FTR, acho que mais "especialistas" ainda ensinam a criar getters / setters sem sentido para tudo , do que nunca a criar nenhum. OMI, este último é um conselho menos equivocado.
leftaroundabout
4
@leftaroundabout: OK, mas estou sugerindo um meio termo entre 'sempre' e 'nunca', que é 'usar quando apropriado'.
JacquesB
3
Eu acho que a questão é que muitos programadores transformam todos os objetos (ou muitos objetos) em um DTO. Às vezes, são necessárias, mas evite-as o máximo possível, pois separam os dados do comportamento. (Supondo que você é um OOPer devoto)
user949300
11

Para transformar a Customerclasse-de um objeto de dados, podemos fazer as seguintes perguntas sobre os campos de dados:

Como queremos usar o {data field}? Onde o {campo de dados} é usado? O uso de {data field} pode e deve ser movido para a classe?

Por exemplo:

  • Qual é o propósito Customer.Name?

    Possíveis respostas, exiba o nome em uma página da web de login, use o nome em correspondências para o cliente.

    O que leva a métodos:

    • Customer.FillInTemplate (…)
    • Customer.IsApplicableForMailing (…)
  • Qual é o propósito Customer.DOB?

    Validando a idade do cliente. Descontos no aniversário do cliente. Correspondências.

    • Customer.IsApplicableForProduct ()
    • Customer.GetPersonalDiscount ()
    • Customer.IsApplicableForMailing ()

Dados os comentários, o objeto de exemplo Customer- tanto como um objeto de dados quanto como um objeto "real", com suas próprias responsabilidades - é muito amplo; isto é, possui muitas propriedades / responsabilidades. O que leva a muitos componentes dependendo Customer(lendo suas propriedades) ou a Customerdependendo de muitos componentes. Talvez existam visões diferentes do cliente, talvez cada uma deva ter sua própria classe distinta 1 :

  • O cliente no contexto Accounte nas transações monetárias provavelmente é usado apenas para:

    • ajude os seres humanos a identificar que sua transferência de dinheiro vai para a pessoa correta; e
    • grupo Accounts.

    Este cliente não precisa de campos como DOB, FavouriteColour, Tele, talvez, nem mesmo Address.

  • O cliente no contexto de um usuário efetuando login em um site bancário.

    Os campos relevantes são:

    • FavouriteColour, que pode vir na forma de temas personalizados;
    • LanguagePreferencese
    • GreetingName

    Em vez de propriedades com getters e setters, eles podem ser capturados em um único método:

    • PersonaliseWebPage (página Modelo);
  • O cliente no contexto de marketing e correspondência personalizada.

    Aqui não confiando nas propriedades de um objeto de dados, mas partindo das responsabilidades do objeto; por exemplo:

    • IsCustomerInterestedInAction (); e
    • GetPersonalDiscounts ().

    O fato de esse objeto de cliente ter uma FavouriteColourpropriedade e / ou uma Addresspropriedade se tornar irrelevante: talvez a implementação use essas propriedades; mas também pode usar algumas técnicas de aprendizado de máquina e usar interações anteriores com o cliente para descobrir em quais produtos o cliente pode estar interessado.


1. É claro que as classes Customere Accountforam exemplos e, para um exemplo simples ou exercício de lição de casa, dividir esse cliente pode ser um exagero, mas com o exemplo da divisão, espero demonstrar que o método de transformar um objeto de dados em um objeto com responsabilidades funcionará.

Kasper van den Berg
fonte
4
Voto positivo, porque você está realmente respondendo à pergunta :) No entanto, também é óbvio que as soluções propostas são muito piores do que ter gettes / setters - por exemplo, FillInTemplate quebra claramente o princípio da separação de preocupações. O que apenas mostra que a premissa da pergunta é falha.
JacquesB
@ Kasper van den Berg: E quando você tem muitos atributos no Cliente, como geralmente é o caso, como você os definiria inicialmente?
IntelliData
2
@IntelliData é provável que seus valores sejam provenientes de um banco de dados, XML etc. O Cliente, ou um CustomerBuilder, os lê e os define usando (por exemplo, em Java) acesso privado / pacote / classe interna ou reflexão (ick). Não é perfeito, mas você geralmente pode evitar levantadores públicos. (Veja a minha resposta para mais alguns detalhes)
user949300
6
Eu não acho que a classe de clientes deva saber sobre qualquer uma dessas coisas.
CodesInChaos
Que tal algo assim Customer.FavoriteColor?
Gabe
8

TL; DR

  • Modelar o comportamento é bom.

  • Modelar para boas abstrações (!) É melhor.

  • Às vezes, objetos de dados são necessários.


Comportamento e Abstração

Existem várias razões para evitar getters e setters. Uma é, como você observou, para evitar a modelagem de dados. Este é realmente o menor motivo. A razão maior é fornecer abstração.

No seu exemplo com a conta bancária que é clara: um setBalance()método seria muito ruim, porque definir um saldo não é para o que uma conta deve ser usada. O comportamento da conta deve abstrair do seu saldo atual o máximo possível. Isso pode levar em consideração o saldo ao decidir falhar em uma retirada, pode dar acesso ao saldo atual, mas modificar a interação com uma conta bancária não deve exigir que o usuário calcule o novo saldo. É isso que a conta deve fazer sozinha.

Mesmo um par de métodos deposit()e withdraw()não é ideal para modelar uma conta bancária. Uma maneira melhor seria fornecer apenas um transfer()método que considere outra conta e uma quantidade como argumentos. Isso permitiria que a classe da conta garantisse trivialmente que você não crie / destrua dinheiro acidentalmente em seu sistema, forneceria uma abstração muito utilizável e forneceria aos usuários mais informações, pois forçaria o uso de contas especiais para dinheiro ganho / investido / perdido (consulte contabilidade dupla ). Obviamente, nem todo uso de uma conta precisa desse nível de abstração, mas definitivamente vale a pena considerar quanta abstração suas classes podem fornecer.

Observe que fornecer abstração e ocultar dados internos nem sempre é a mesma coisa. Quase qualquer aplicativo contém classes que são efetivamente apenas dados. Tuplas, dicionários e matrizes são exemplos frequentes. Você não deseja ocultar a coordenada x de um ponto do usuário. Há muito pouca abstração que você pode / deve estar fazendo com um ponto.


A classe do cliente

Um cliente é certamente uma entidade em seu sistema que deve tentar fornecer abstrações úteis. Por exemplo, provavelmente deve estar associado a um carrinho de compras, e a combinação do carrinho e do cliente deve permitir a compra, o que pode iniciar ações como enviar a ele os produtos solicitados, cobrar dinheiro (levando em consideração o pagamento selecionado método) etc.

O problema é que todos os dados que você mencionou não estão apenas associados a um cliente, todos esses dados também são mutáveis. O cliente pode se mudar. Eles podem mudar de empresa de cartão de crédito. Eles podem alterar o endereço de e-mail e o número de telefone. Caramba, eles podem até mudar seu nome e / ou sexo! Portanto, uma classe de cliente com todos os recursos precisa fornecer acesso completo para todos esses itens de dados.

Ainda assim, os setters podem / devem fornecer serviços não triviais: eles podem garantir o formato correto de endereços de email, verificação de endereços postais, etc. Da mesma forma, os "getters" podem fornecer serviços de alto nível, como fornecer endereços de email no Name <[email protected]>formato usando os campos de nome e o endereço de email depositado, ou forneça um endereço postal formatado corretamente, etc. É claro que o que essa funcionalidade de alto nível faz sentido depende muito do seu caso de uso. Pode ser um exagero completo ou pode exigir que outra classe faça o que é certo. A escolha do nível de abstração não é fácil.

cmaster
fonte
parece certo, embora eu discorde sobre a parte do sexo ...;)
IntelliData 19/15
6

Tentando expandir a resposta do Kasper, é mais fácil reclamar e eliminar levantadores. Em um argumento bastante vago, de ondulação manual (e esperançosamente bem-humorado):

Quando o Customer.Name mudaria?

Raramente. Talvez eles se casaram. Ou entrou em proteção de testemunha. Mas, nesse caso, você também gostaria de verificar e possivelmente mudar a residência deles, parentes próximos e outras informações.

Quando o DOB ​​mudaria?

Somente na criação inicial ou em uma falha na entrada de dados. Ou se eles são jogadores de baseball da Domincan. :-)

Esses campos não devem estar acessíveis com setters normais de rotina. Talvez você tenha um Customer.initialEntry()método ou um Customer.screwedUpHaveToChange()método que exija permissões especiais. Mas não tenha um Customer.setDOB()método público .

Geralmente, um Cliente é lido a partir de um banco de dados, uma API REST, algum XML, qualquer que seja. Tenha um método Customer.readFromDB()ou, se você for mais rigoroso quanto ao SRP / separação de preocupações, terá um construtor separado, por exemplo, um CustomerPersisterobjeto com um read()método. Internamente, eles de alguma forma definem os campos (eu prefiro usar o acesso ao pacote ou uma classe interna, YMMV). Mas, novamente, evite levantadores públicos.

(Adendo como a pergunta mudou um pouco ...)

Digamos que seu aplicativo faça uso pesado de bancos de dados relacionais. Seria tolice ter Customer.saveToMYSQL()ou Customer.readFromMYSQL()métodos. Isso cria um acoplamento indesejável a uma entidade concreta, não padronizada e com probabilidade de alteração . Por exemplo, quando você altera o esquema ou altera para Postgress ou Oracle.

No entanto, IMO, é perfeitamente aceitável para acoplar ao cliente a um padrão abstrato , ResultSet. Um objeto auxiliar separado (eu o chamarei CustomerDBHelper, que provavelmente é uma subclasse de AbstractMySQLHelper) conhece todas as conexões complexas ao seu banco de dados, conhece os detalhes complicados da otimização, conhece as tabelas, consultas, junções, etc ... (ou usa um ORM como o Hibernate) para gerar o ResultSet. Seu objeto fala com o ResultSet, que é um padrão abstrato , improvável de mudar. Quando você altera o banco de dados subjacente ou altera o esquema, o Cliente não muda , mas o CustomerDBHelper altera. Se você tiver sorte, é apenas o AbstractMySQLHelper que altera o que automaticamente faz as alterações para Cliente, Comerciante, Remessa etc ...

Dessa forma, você pode (talvez) evitar ou diminuir a necessidade de getters e setters.

E, o ponto principal do artigo Holub, compare e compare o que foi descrito acima com o que seria se você usasse getters e setters para tudo e alterasse o banco de dados.

Da mesma forma, digamos que você use muito XML. Na IMO, é bom associar seu Cliente a um padrão abstrato, como um Python xml.etree.ElementTree ou um Java org.w3c.dom.Element . O cliente obtém e se define a partir disso. Novamente, você pode (talvez) diminuir a necessidade de getters e setters.

user949300
fonte
você diria que é aconselhável usar um padrão de construtor?
IntelliData
O Builder é útil para tornar a construção de um objeto mais fácil e robusta e, se você desejar, permitir que o objeto seja imutável. No entanto, ele ainda (parcialmente) expõe o fato de que o objeto subjacente possui um campo DOB, portanto, não é o fim de tudo.
User949300
1

A questão de ter getters e setters pode ser uma questão de que uma classe pode ser usada na lógica de negócios de uma maneira, mas você também pode ter classes auxiliares para serializar / desserializar os dados de um banco de dados ou arquivo ou outro armazenamento persistente.

Devido ao fato de existirem várias maneiras de armazenar / recuperar seus dados e você desejar separar os objetos de dados da maneira como eles são armazenados, o encapsulamento pode ser "comprometido" tornando esses membros públicos ou tornando-os acessíveis através de getters e setters, que é quase tão ruim quanto torná-los públicos.

Existem várias maneiras de contornar isso. Uma maneira é disponibilizar os dados para um "amigo". Embora a amizade não seja herdada, isso pode ser superado por qualquer serializador que solicite as informações do amigo, ou seja, o serializador básico "encaminhando" as informações.

Sua classe pode ter um método genérico "fromMetadata" ou "toMetadata". From-metadata constrói um objeto, então pode muito bem ser um construtor. Se for uma linguagem de tipo dinâmico, os metadados são bastante padrão para essa linguagem e provavelmente é a principal maneira de construir esses objetos.

Se sua linguagem for C ++ especificamente, uma maneira de contornar isso é ter uma "estrutura" pública de dados e, em seguida, para sua classe ter uma instância dessa "estrutura" como membro e, de fato, todos os dados que você irá armazenar / recuperar para ser armazenado nele. Você pode escrever facilmente "wrappers" para ler / gravar seus dados em vários formatos.

Se sua linguagem é C # ou Java que não possui "structs", você pode fazer o mesmo, mas sua estrutura agora é uma classe secundária. Não existe um conceito real de "propriedade" dos dados ou da constância; portanto, se você fornecer uma instância da classe contendo seus dados e ela for pública, o que quer que seja retido poderá modificá-lo. Você pode "clonar", embora isso possa ser caro. Como alternativa, você pode fazer com que essa classe tenha dados particulares, mas use acessadores. Isso fornece aos usuários da sua classe uma maneira indireta de acessar os dados, mas não é a interface direta com a sua classe e é realmente um detalhe no armazenamento dos dados da classe, que também é um caso de uso.

CashCow
fonte
0

OOP é sobre o comportamento de encapsular e ocultar dentro de objetos. Objetos são caixas pretas. Esta é uma maneira de projetar coisas. Em muitos casos, o ativo não precisa saber o estado interno de outro componente e é melhor não precisar conhecê-lo. Você pode aplicar essa ideia principalmente a interfaces ou dentro de um objeto com visibilidade e cuidando apenas verbos / ações permitidos estão disponíveis para o chamador.

Isso funciona bem para algum tipo de problema. Por exemplo, nas interfaces do usuário para modelar o componente individual da interface do usuário. Quando você interage com uma caixa de texto, é apenas interessado em definir o texto, obtê-lo ou ouvir um evento de alteração de texto. Você normalmente não está interessado em saber onde está o cursor, a fonte usada para desenhar o texto ou como o teclado é usado. O encapsulamento fornece muito aqui.

Pelo contrário, quando você liga para um serviço de rede, fornece uma entrada explícita. Geralmente, existe uma gramática (como em JSON ou XML) e todas as opções de chamar o serviço não têm motivos para serem ocultadas. A ideia é que você possa ligar para o serviço da maneira que desejar e o formato dos dados for público e publicado.

Nesse caso, ou muitos outros (como acesso a um banco de dados), você realmente trabalha com dados compartilhados. Como tal, não há motivo para ocultá-lo, pelo contrário, você deseja disponibilizá-lo. Pode haver uma preocupação no acesso de leitura / gravação ou na consistência da verificação de dados, mas, neste núcleo, o conceito principal, se for público.

Para esse requisito de design, onde você deseja evitar o encapsulamento e tornar as coisas públicas e claro, você deseja evitar objetos. O que você realmente precisa são tuplas, estruturas C ou equivalente, não objetos.

Mas isso também acontece em linguagens como Java, as únicas coisas que você pode modelar são objetos ou matrizes de objetos. Os objetos em si podem conter alguns tipos nativos (int, float ...), mas isso é tudo. Mas os objetos também podem se comportar como uma estrutura simples, com apenas campos públicos e tudo isso.

Portanto, se você modelar dados, poderá fazê-lo apenas com campos públicos dentro de objetos, porque não precisa de mais. Você não usa o encapsulamento porque não precisa dele. Isso é feito dessa maneira em vários idiomas. Em java, historicamente, uma subida padrão em que, com o getter / setter, você poderia pelo menos ter controle de leitura / gravação (não adicionando o setter, por exemplo) e que as ferramentas e a estrutura usando a API instrospection procurariam métodos getter / setter e o usariam para preencha automaticamente o conteúdo ou exiba teses como campos modificáveis ​​na interface do usuário gerada automaticamente.

Há também o argumento de que você pode adicionar alguma lógica / verificação no método setter.

Na realidade, quase não há justificativa para os getter / setters, pois eles costumam ser usados ​​para modelar dados puros. Estruturas e desenvolvedores usando seus objetos esperam que o getter / setter não faça nada além de definir / obter os campos de qualquer maneira. Você não está efetivamente fazendo mais com getter / setter do que o que poderia ser feito com campos públicos.

Mas isso é hábitos antigos e hábitos antigos são difíceis de remover ... Você pode até ser ameaçado por seus colegas ou professor se não colocar cegos / setter cegamente em todos os lugares se eles não tiverem o histórico para entender melhor o que são e o que são não.

Você provavelmente precisaria alterar o idioma para obter o código de todas as teses getters / setters. (Como c # ou lisp). Para mim, getters / setter são apenas mais um erro de um bilhão de dólares ...

Nicolas Bousquet
fonte
6
C # propriedades realmente não implementar encapsulamento mais do que os getters e setters ...
IntelliData
1
A vantagem dos [gs] etters é que você pode fazer o que normalmente não faz (verificar valores, notificar os observadores), simular campos inexistentes etc., e principalmente que você pode alterá-lo mais tarde . Com Lombok , o clichê se foi: @Getter @Setter class MutablePoint3D {private int x, y, z;}.
Maaartinus 20/05
1
@maaartinus Certamente [gs] etter pode fazer qualquer coisa como qualquer outro método. Isso significa que qualquer código de chamada deve estar ciente de que qualquer valor definido pode gerar uma exceção, ser alterado ou potencialmente enviar uma notificação de alteração ... ou qualquer outra coisa. Que mais ou menos [gs] etter não estão fornecendo acesso a um campo, mas fazendo código arbitrário.
Nicolas Bousquet
As propriedades @IntelliData C # permitem não gravar 1 caractere inútil do clichê e não cuidar de todo esse material getter / setter ... Isso já é uma conquista melhor do que o projeto lombok. Também para mim, um POJO com apenas getters / setter não está aqui para fornecer o encapsulamento, mas para publicar um formato de dados que é livre para ler ou escrever como troca com um serviço. O encapsulamento é o oposto do requisito de design.
Nicolas Bousquet
Eu realmente não acho que as propriedades são realmente boas. Claro, você salva o prefixo e os parênteses, ou seja, 5 caracteres por chamada, mas 1. eles parecem campos, o que é confuso. 2. são algo adicional para o qual você precisa de apoio na reflexão. Não é grande coisa, mas também não há grande vantagem (quando comparado ao Java + Lombok; o Java puro está em clara perda).
Maaartinus 20/05
0

Assim, por exemplo, dada uma classe Customer que mantém muitas informações sobre o cliente, por exemplo, Nome, DOB, Tel, Endereço etc., como evitar um getter / setter para obter e definir todos esses atributos? Que método do tipo 'Comportamento' pode-se escrever para preencher todos esses dados?

Penso que esta pergunta é espinhosa porque você está preocupado com métodos de comportamento para preencher dados, mas não vejo nenhuma indicação de qual comportamento a Customerclasse de objetos pretende encapsular.

Não confunda Customercomo uma classe de objetos com 'Cliente' como usuário / ator que executa tarefas diferentes usando seu software.

Quando você diz que é dada uma classe Customer que mantém muito se as informações sobre o cliente, então, no que diz respeito ao comportamento, parece que sua classe Customer pouco a distingue de uma pedra. Um Rockpode ter uma cor, você pode dar um nome, um campo para armazenar seu endereço atual, mas não esperamos nenhum tipo de comportamento inteligente de uma rocha.

No artigo vinculado sobre getters / setters serem maus:

O processo de design do OO se concentra nos casos de uso: um usuário executa tarefas independentes que têm algum resultado útil. (O logon não é um caso de uso, porque falta um resultado útil no domínio do problema. Desenhar um cheque de pagamento é um caso de uso.) Um sistema OO, então, implementa as atividades necessárias para reproduzir os vários cenários que compõem um caso de uso.

Sem nenhum comportamento definido, referir-se a uma rocha como Customernão altera o fato de ser apenas um objeto com algumas propriedades que você gostaria de rastrear e não importa quais truques você deseja usar para se afastar de getters e setters. Uma rocha não se importa se tiver um nome válido e não se espera que uma rocha saiba se um endereço é válido ou não.

Seu sistema de pedidos pode associar um Rocka um pedido de compra e, desde que esse Rockendereço seja definido, parte do sistema pode garantir que um item seja entregue a uma rocha.

Em todos esses casos, Rocké apenas um Objeto de Dados e continuará sendo um até que definamos comportamentos específicos com resultados úteis em vez de hipotéticos.


Tente isto:

Quando você evita sobrecarregar a palavra 'Cliente' com 2 significados potencialmente diferentes, isso deve facilitar a conceitualização.

Um Rockobjeto faz um pedido ou é algo que um ser humano faz clicando nos elementos da interface do usuário para acionar ações em seu sistema?

nvuono
fonte
O cliente é um ator que faz muitas coisas, mas também possui muitas informações associadas a ele; isso justifica a criação de 2 classes separadas, 1 como ator e 1 como objeto de dados?
IntelliData
@IntelliData passando objetos ricos por camadas é onde as coisas ficam complicadas. Se você estiver enviando o objeto de um controlador para uma visualização, ela precisará entender o contrato geral do objeto (padrão JavaBeans, por exemplo). Se você estiver enviando o objeto pela rede, o JaxB ou algo semelhante precisa ser capaz de reintroduzi-lo em um objeto idiota (porque você não deu a ele o objeto completo). Objetos ricos são maravilhosos para manipular dados - eles são ruins para a transferência de estado. Por outro lado, objetos burros são ruins para manipular dados e aceitáveis ​​para a transferência de estado.
0

Eu adiciono meus 2 centavos aqui mencionando a abordagem de objetos que falam SQL .

Essa abordagem é baseada na noção de objeto independente. Possui todos os recursos necessários para implementar seu comportamento. Não precisa ser informado sobre como fazer seu trabalho - uma solicitação declarativa é suficiente. E um objeto definitivamente não precisa conter todos os seus dados como propriedades de classe. Realmente não importa - e não deveria - importar de onde eles vêm.

Falando sobre um agregado , a imutabilidade também não é um problema. Digamos que você tenha uma sequência de estados que a agregação pode conter: Raiz agregada como saga é totalmente bom implementar cada estado como objeto independente. Provavelmente você poderia ir ainda mais longe: converse com seu especialista em domínio. As chances são de que ele ou ela não veja esse agregado como uma entidade unificada. Provavelmente, cada estado tem seu próprio significado, merecendo seu próprio objeto.

Por fim, gostaria de observar que o processo de localização de objetos é muito semelhante à decomposição do sistema em subsistemas . Ambos são baseados em comportamento, e nada mais.

Zapadlo
fonte