Você geralmente envia objetos ou suas variáveis ​​de membro para funções?

31

Qual é a prática geralmente aceita entre esses dois casos:

function insertIntoDatabase(Account account, Otherthing thing) {
    database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}

ou

function insertIntoDatabase(long accountId, long thingId, double someValue) {
    database.insertMethod(accountId, thingId, someValue);
}

Em outras palavras, geralmente é melhor passar objetos inteiros ou apenas os campos que você precisa?

AJJ
fonte
5
Isso dependeria inteiramente do objetivo da função e de como ela se relaciona (ou não) ao objeto em questão.
MetaFight 24/05
Esse é o problema. Não sei dizer quando usaria um ou outro. Sinto que sempre poderia mudar o código para acomodar qualquer abordagem.
AJJ 24/05
11
Em termos de API (e sem considerar as implementações), o primeiro é abstrato e orientado ao domínio (o que é bom), enquanto o último não é (o que é ruim).
Erik Eidt
11
A primeira abordagem seria mais OO de três camadas. Mas deve ser ainda mais eliminado o banco de dados de palavras do método. Deve ser "Armazenar" ou "Persistir" e fazer Conta ou Coisa (não as duas). Como cliente desta camada, você não deve estar ciente da mídia de armazenamento. Ao recuperar uma conta, você precisará passar o ID ou uma combinação de valores de propriedade (não valores de campo) para identificar o objeto desejado. Ou / e implenente, um método de enumeração que passa em todas as contas.
Martin Maat 24/05
11
Normalmente, ambos estariam errados (ou melhor, menos que o ideal). Como um objeto deve ser serializado no banco de dados deve ser uma propriedade (uma função de membro) do objeto, porque normalmente depende diretamente das variáveis ​​de membro do objeto. Caso você altere membros do objeto, também será necessário alterar o método de serialização. Isso funciona melhor se ele faz parte do objeto
tofro

Respostas:

24

Nem geralmente é melhor que o outro. É uma decisão que você deve fazer caso a caso.

Mas, na prática, quando você está em uma posição em que pode realmente tomar essa decisão, é porque você decide qual camada da arquitetura geral do programa deve dividir o objeto em primitivas, portanto, pense em toda a chamada pilha , não apenas esse método em que você está atualmente. Presumivelmente, a ruptura deve ser feita em algum lugar e não faria sentido (ou seria desnecessariamente propenso a erros) fazê-lo mais de uma vez. A questão é onde deveria estar aquele lugar.

A maneira mais fácil de tomar essa decisão é pensar em qual código deve ou não ser alterado se o objeto for alterado . Vamos expandir um pouco o seu exemplo:

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

vs

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

Na primeira versão, o código da interface do usuário está passando cegamente o dataobjeto e cabe ao código do banco de dados extrair os campos úteis dele. Na segunda versão, o código da interface do usuário está dividindo o dataobjeto em seus campos úteis, e o código do banco de dados os recebe diretamente sem saber de onde eles vieram. A principal implicação é que, se a estrutura do dataobjeto fosse alterada de alguma maneira, a primeira versão exigiria apenas a alteração do código do banco de dados, enquanto a segunda versão exigiria a alteração apenas do código da interface do usuário . Qual desses dois está correto depende muito do tipo de dados que o dataobjeto contém, mas geralmente é muito óbvio. Por exemplo, sedataé uma string fornecida pelo usuário como "20/05/1999", deve ser o código da interface do usuário para convertê-la em um Datetipo adequado antes de transmiti-la.

Ixrec
fonte
8

Esta não é uma lista exaustiva, mas considere alguns dos seguintes fatores ao decidir se um objeto deve ser passado para um método como argumento:

O objeto é imutável? A função é 'pura'?

Os efeitos colaterais são uma consideração importante para a manutenção do seu código. Quando você vê código com muitos objetos com estado mutáveis ​​sendo passados ​​por todo o lugar, esse código geralmente é menos intuitivo (da mesma maneira que variáveis ​​de estado globais podem ser menos intuitivas), e a depuração se torna mais difícil e com o tempo. consumindo.

Como regra geral, procure garantir, tanto quanto razoavelmente possível, que todos os objetos que você passar para um método sejam claramente imutáveis.

Evite (novamente, na medida do possível) qualquer projeto em que se espere que o estado de um argumento seja alterado como resultado de uma chamada de função - um dos argumentos mais fortes para essa abordagem é o Princípio da menor surpresa ; ou seja, alguém lendo seu código e vendo um argumento passado para uma função tem "menos probabilidade" de esperar que seu estado mude após o retorno da função.

Quantos argumentos o método já possui?

Métodos com listas de argumentos excessivamente longas (mesmo que a maioria desses argumentos tenham valores "padrão") começam a parecer um cheiro de código. Às vezes, essas funções são necessárias, no entanto, e você pode considerar criar uma classe cujo único objetivo é agir como um Objeto de Parâmetro .

Essa abordagem pode envolver uma pequena quantidade de mapeamento de código padrão adicional do seu objeto 'fonte' para o seu objeto de parâmetro, mas esse é um custo bastante baixo em termos de desempenho e complexidade, no entanto, e existem vários benefícios em termos de dissociação e imutabilidade de objetos.

O objeto transmitido pertence exclusivamente a uma "camada" dentro do seu aplicativo (por exemplo, um ViewModel ou uma entidade ORM?)

Pense em Separação de Preocupações (SoC) . Às vezes, perguntar-se se o objeto "pertence" à mesma camada ou módulo em que seu método existe (por exemplo, uma biblioteca de wrapper de API rolada manualmente ou sua camada lógica de negócios principal etc.) pode informar se esse objeto realmente deve ser passado para aquele método.

O SoC é uma boa base para escrever código modular limpo, com pouco acoplamento. por exemplo, um objeto de entidade ORM (mapeamento entre o código e o esquema do banco de dados) idealmente não deve ser transmitido na sua camada de negócios ou pior na sua camada de apresentação / interface do usuário.

No caso de passar dados entre 'camadas', ter parâmetros de dados simples passados ​​para um método é geralmente preferível à passagem de um objeto da camada 'errada' em um objeto. Embora seja provavelmente uma boa idéia ter modelos separados, que existem na camada 'certa', na qual você pode mapear.

A função em si é muito grande e / ou complexa?

Quando uma função precisa de muitos itens de dados, pode valer a pena considerar se essa função está assumindo muitas responsabilidades; procure oportunidades em potencial para refatorar usando objetos menores e funções mais curtas e simples.

A função deve ser um objeto de comando / consulta?

Em alguns casos, o relacionamento entre os dados e a função pode estar próximo; nesses casos, considere se um objeto de comando ou um objeto de consulta seria apropriado.

Adicionar um parâmetro de objeto a um método força a classe que contém a adotar novas dependências?

Às vezes, o argumento mais forte para os argumentos "Dados antigos simples" é simplesmente que a classe de recebimento já é autocontida e a adição de um parâmetro de objeto a um de seus métodos poluiria a classe (ou, se a classe já estiver poluída, ela será piorar a entropia existente)

Você realmente precisa passar por um objeto completo ou precisa apenas de uma pequena parte da interface desse objeto?

Considere o Princípio de Segregação de Interface com relação às suas funções - ou seja, ao passar um objeto, ele deve depender apenas de partes da interface desse argumento que ele (a função) realmente precisa.

Ben Cottrell
fonte
5

Portanto, quando você cria uma função, declara implicitamente algum contrato com o código que está chamando. "Esta função pega essas informações e as transforma em outra coisa (possivelmente com efeitos colaterais)".

Portanto, seu contrato deve estar logicamente com os objetos (no entanto, eles são implementados) ou com os campos que por acaso fazem parte desses outros objetos. Você está adicionando acoplamentos de qualquer maneira, mas, como programador, você decide a que lugar pertence.

Em geral , se não estiver claro, favoreça os menores dados necessários para que a função funcione. Isso geralmente significa passar apenas nos campos, pois a função não precisa das outras coisas encontradas nos objetos. Mas às vezes pegar o objeto inteiro é mais correto, pois resulta em menos impacto quando as coisas inevitavelmente mudam no futuro.

Telastyn
fonte
Por que você não altera o nome do método para insertAccountIntoDatabase ou passa outro tipo? Em um certo número de argumentos para usar obj, é fácil ler o código. No seu caso, eu prefiro pensar se o nome do método esclarece o que vou inserir, em vez de como vou fazê-lo.
Laiv
3

Depende.

Para elaborar, os parâmetros aceitos pelo método devem corresponder semanticamente ao que você está tentando fazer. Considere uma EmailInvitere estas três implementações possíveis de um invitemétodo:

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

Passar para Stringonde você deve passar EmailAddressé defeituoso, porque nem todas as strings são endereços de email. A EmailAddressclasse combina melhor semanticamente o comportamento do método. No entanto, a transmissão de um Userarquivo também é falha, porque, por que razão a terra deveria EmailInviterse limitar a convidar usuários? E as empresas? E se você estiver lendo endereços de email de um arquivo ou linha de comando e eles não estiverem associados aos usuários? Listas de discussão? A lista continua.

Existem alguns sinais de aviso que você pode usar para obter orientação aqui. Se você estiver usando um tipo de valor simples como Stringou intmas nem todas as strings ou ints são válidas ou há algo de "especial" nelas, você deve usar um tipo mais significativo. Se você estiver usando um objeto e a única coisa a fazer é chamar um getter, deve passar o objeto diretamente no getter. Essas diretrizes não são difíceis nem rápidas, mas poucas são.

Jack
fonte
0

O Clean Code recomenda ter o mínimo de argumentos possível, o que significa que o Object seria normalmente a melhor abordagem e acho que faz algum sentido. Porque

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

é uma chamada mais legível do que

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );
someguy
fonte
não posso concordar lá. Os dois não são sinônimos. Criar 2 novas instâncias de objetos apenas para passá-las para um método não é bom. Eu usaria insertIntoDatabase (myAccount, myOtherthing) em vez de qualquer uma das suas opções.
jwenting
0

Passe ao redor do objeto, não seu estado constituinte. Isso suporta os princípios orientados a objetos de encapsulamento e ocultação de dados. Expor as entranhas de um objeto em várias interfaces de método em que não é necessário viola os principais princípios de POO.

O que acontece se você alterar os campos Otherthing? Talvez você altere um tipo, adicione um campo ou remova um campo. Agora, todos os métodos como o que você mencionou na sua pergunta precisam ser atualizados. Se você passar pelo objeto, não haverá alterações na interface.

O único momento em que você deve escrever um método para aceitar campos em um objeto é ao escrever um método para recuperar o objeto:

public User getUser(String primaryKey) {
  return ...;
}

No momento de fazer essa chamada, o código de chamada ainda não tem uma referência ao objeto porque o ponto de chamar esse método é obter o objeto.


fonte
11
"O que acontece se você alterar os campos Otherthing?" (1) Isso seria uma violação do princípio aberto / fechado. (2) mesmo se você passar o objeto inteiro, o código dentro dele acessa os membros desse objeto (e se não o faz, por que passar o objeto?) Ainda quebraria ...
David Arno
@ David David O ponto da minha resposta não é que nada iria quebrar, mas menos iria quebrar. Não esqueça também o primeiro parágrafo: independentemente de quais quebras, o estado interno de um objeto deve ser abstraído usando a interface do objeto. Passar pelo seu estado interno é uma violação dos princípios da POO.
0

Do ponto de vista da manutenção, os argumentos devem ser claramente distinguíveis entre si, de preferência no nível do compilador.

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

O primeiro design leva à detecção precoce de bugs. O segundo design pode levar a problemas sutis de tempo de execução que não aparecem nos testes. Portanto, o primeiro desenho deve ser preferido.

Joeri Sebrechts
fonte
0

Dos dois, minha preferência é o primeiro método:

function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }

O motivo é que as alterações feitas em qualquer objeto no caminho, desde que as alterações preservem esses getters para que a mudança seja transparente fora do objeto, você terá menos código para alterar e testar e menos chance de interromper o aplicativo.

Este é apenas o meu processo de pensamento, baseado principalmente em como gosto de trabalhar e estruturar coisas dessa natureza e que provam ser bastante gerenciáveis ​​e sustentáveis ​​a longo prazo.

Não vou entrar em convenções de nomenclatura, mas gostaria de salientar que, embora esse método contenha a palavra "banco de dados", esse mecanismo de armazenamento pode mudar no futuro. Pelo código mostrado, não há nada que vincule a função à plataforma de armazenamento de banco de dados que está sendo usada - ou mesmo se for um banco de dados. Apenas assumimos porque está no nome. Novamente, supondo que esses getters sejam sempre preservados, será fácil alterar como / onde esses objetos são armazenados.

Eu repensaria a função e os dois objetos, porque você tem uma função que depende de duas estruturas de objetos e, especificamente, dos getters empregados. Também parece que essa função está vinculando esses dois objetos em uma coisa cumulativa que é mantida. Meu intestino está me dizendo que um terceiro objeto pode fazer sentido. Eu precisaria saber mais sobre esses objetos e como eles se relacionam na realidade e no roteiro previsto. Mas meu intestino está inclinado nessa direção.

Como o código está agora, a pergunta é: "Onde seria ou deveria estar essa função?" Faz parte da conta ou do OtherThing? Onde isso vai?

Eu acho que já existe um terceiro objeto "banco de dados", e estou inclinado a colocar essa função nesse objeto, e então torna-se esse trabalho de objetos ser capaz de lidar com uma conta e um OtherThing, transformar e também persistir com o resultado .

Se você fosse tão longe quanto tornar esse terceiro objeto conforme um padrão ORM (Object-Relational Mapping), melhor. Isso tornaria muito óbvio para quem trabalha com o código entender "Ah, é aqui que o Account e o OtherThing se juntam e persistem".

Mas também poderia fazer sentido introduzir um quarto objeto, que lida com o trabalho de combinar e transformar uma Conta e um OtherThing, mas não com a mecânica da persistência. Eu faria isso se você antecipar muito mais interações com ou entre esses dois objetos, porque, com isso, eu gostaria que os bits de persistência fossem fatorados em um objeto que gerencia apenas a persistência.

Eu gostaria de manter o design de forma que qualquer um dos objetos Account, OtherThing ou terceiro ORM possa ser alterado sem precisar alterar também os outros três. A menos que haja uma boa razão para não fazê-lo, eu gostaria que o Account e o OtherThing fossem independentes e não tivessem que conhecer o funcionamento interno e as estruturas um do outro.

É claro que, se eu conhecesse todo o contexto, isso poderia mudar minhas idéias completamente. Novamente, é assim que penso quando vejo coisas assim e como é enxuto.

Thomas Carlisle
fonte
0

Ambas as abordagens têm seus próprios prós e contras. O que é melhor em um cenário depende muito do caso de uso em questão.


Parâmetros Pro Multiple, referência ao objeto Con:

  • O chamador não está vinculado a uma classe específica ; ele pode transmitir valores de diferentes fontes por completo
  • O estado do objeto está protegido contra alterações inesperadas na execução do método.

Referência do Pro Object:

  • Interface clara de que o método está vinculado ao tipo de referência Objeto, dificultando a passagem acidental de valores não relacionados / inválidos
  • Renomear um campo / getter requer alterações em todas as invocações do método e não apenas em sua implementação
  • Se uma nova propriedade for incluída e precisar ser aprovada, nenhuma alteração será necessária na assinatura do método
  • O método pode alterar o estado do objeto
  • Passar muitas variáveis ​​de tipos primitivos semelhantes torna confuso para o chamador em relação à ordem (problema de padrão do Construtor)

Então, o que precisa ser usado e quando depende muito dos casos de uso

  1. Passar parâmetros individuais: em geral, se o método não tem nada a ver com o tipo de objeto, é melhor passar a lista de parâmetros individuais para que seja aplicável a um público mais amplo.
  2. Introduzir novo objeto de modelo: se a lista de parâmetros aumentar, será maior (mais de 3), é melhor introduzir um novo objeto de modelo pertencente à API chamada (padrão do construtor preferido)
  3. Passar referência de objeto: Se o método estiver relacionado aos objetos de domínio, é melhor do ponto de vista de capacidade de manutenção e legibilidade passar as referências de objeto.
Rahul
fonte
0

Por um lado, você tem uma conta e um objeto Otherthing. Por outro lado, você tem a capacidade de inserir um valor em um banco de dados, dado o ID de uma conta e o ID de um Otherthing. Essas são as duas coisas.

Você pode escrever um método considerando Conta e Outras coisas como argumentos. No lado profissional, o chamador não precisa saber detalhes sobre a conta e outras coisas. No lado negativo, o destinatário precisa saber sobre os métodos de conta e outras coisas. Além disso, não há como inserir mais nada em um banco de dados além do valor de um objeto Otherthing e não há como usar esse método se você tiver o ID de um objeto de conta, mas não o próprio objeto.

Ou você pode escrever um método usando dois IDs e um valor como argumentos. No lado negativo, o chamador precisa conhecer os detalhes de Conta e outras coisas. E pode haver uma situação em que você realmente precise de mais detalhes de uma conta ou outra coisa além do ID a ser inserido no banco de dados; nesse caso, essa solução é totalmente inútil. Por outro lado, esperamos que nenhum conhecimento de conta e outras coisas seja necessário no chamado e haja mais flexibilidade.

Sua decisão: é necessária mais flexibilidade? Geralmente, isso não é uma questão de apenas uma ligação, mas seria consistente em todo o seu software: você usa IDs da conta na maioria das vezes ou usa os objetos. Misturando você recebe o pior dos dois mundos.

No C ++, você pode ter um método com dois IDs mais valor e um método embutido com Account e Otherthing, para que você tenha os dois lados com zero de sobrecarga.

gnasher729
fonte