É um anti-padrão se uma propriedade de classe criar e retornar uma nova instância de uma classe?


24

Eu tenho uma classe chamada Headingque faz algumas coisas, mas também deve ser capaz de retornar o oposto do valor atual do cabeçalho, que finalmente deve ser usado através da criação de uma nova instância da Headingprópria classe.

Posso ter uma propriedade simples chamada reciprocalpara retornar o cabeçalho oposto do valor atual e, em seguida, criar manualmente uma nova instância da classe Heading, ou posso criar um método createReciprocalHeading()para criar automaticamente uma nova instância da classe Heading e retorná-la ao diretório do utilizador.

No entanto, um de meus colegas recomendou que eu apenas criasse uma propriedade de classe chamada reciprocalque retornasse uma nova instância da própria classe por meio do método getter.

Minha pergunta é: não é um anti-padrão que uma propriedade de uma classe se comporte dessa maneira?

Eu particularmente acho isso menos intuitivo porque:

  1. Na minha opinião, uma propriedade de uma classe não deve retornar uma nova instância de uma classe e
  2. O nome da propriedade, ou seja reciprocal, não ajuda o desenvolvedor a entender completamente seu comportamento, sem obter ajuda do IDE ou verificar a assinatura do getter.

Estou sendo muito rigoroso sobre o que uma propriedade de classe deve fazer ou é uma preocupação válida? Eu sempre tentei gerenciar o estado da classe através de seus campos e propriedades e seu comportamento através de seus métodos, e não consigo ver como isso se encaixa na definição da propriedade da classe.


6
Como @Ewan diz no último parágrafo de sua resposta, longe de ser um antipadrão, ter Headingum tipo imutável e reciprocalretornar um novo Headingé uma boa prática "poço de sucesso". (com a ressalva para as duas chamadas para reciprocaldeve retornar "a mesma coisa", ou seja, eles devem passar o teste de igualdade.)
David Arno

20
"minha turma não é imutável". Aqui está o seu verdadeiro anti-padrão. ;)
David Arno

3
@DavidArno Bem, às vezes há uma enorme penalidade de desempenho por tornar certas coisas imutáveis, especialmente se o idioma não a suportar nativamente, mas caso contrário, concordo que a imutabilidade é uma boa prática em muitos casos.
precisa saber é o seguinte

2
@dcorking a classe é implementada em C # e TypeScript, mas prefiro manter essa linguagem independente.
precisa saber é o seguinte

2
Sons @Kaiserludi como uma resposta (uma grande resposta) - Os comentários são para o pedido de clareza
dcorking

Respostas:


22

Não é desconhecido ter coisas como Copy () ou Clone (), mas sim, acho que você está certo em se preocupar com isso.

considere por exemplo:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

Seria bom ter algum aviso de que a propriedade era um novo objeto a cada vez.

você também pode assumir que:

h2.reciprocal == h1

Contudo. Se sua classe de cabeçalho fosse um tipo de valor imutável, você seria capaz de implementar esses relacionamentos e recíproco pode ser um bom nome para a operação


11
Apenas observe que Copy()e Clone()são métodos, não propriedades. Acredito que o OP esteja se referindo a getters de propriedades retornando novas instâncias.
Lynn

6
eu sei. mas as propriedades são (tipo de) também métodos em c #
Ewan

4
Nesse caso, C # tem muito mais para oferecer: String.Substring(), Array.Reverse(), Int32.GetHashCode(), etc.
Lynn

If your heading class was an immutable value type- Acho que você deve adicionar que os setters de propriedades em objetos imutáveis ​​sempre retornem as novas instâncias (ou pelo menos se o novo valor não for o mesmo que o antigo), porque essa é a definição de objetos imutáveis. :)
Ivan Kolmychek 8/16

17

Uma interface de uma classe leva o usuário da classe a fazer suposições sobre como ela funciona.

Se muitas dessas suposições estão corretas e poucas estão erradas, a interface é boa.

Se muitas dessas suposições estão erradas e poucas estão certas, a interface é um lixo.

Uma suposição comum sobre Propriedades é que chamar a função get é barato. Outra suposição comum sobre propriedades é que chamar a função get duas vezes seguidas retornará a mesma coisa.


Você pode contornar isso usando consistência, a fim de alterar as expectativas. Por exemplo, para uma pequena biblioteca 3D em que você precisa Vector, Ray Matrixetc, você pode fazer com que getters como Vector.normale Matrix.inversesempre sejam caros. Infelizmente, mesmo que suas interfaces usem consistentemente propriedades caras, as interfaces serão, na melhor das hipóteses, tão intuitivas quanto aquelas que usam Vector.create_normal()e Matrix.create_inverse()- ainda não conheço nenhum argumento forte que possa ser feito de que o uso de propriedades crie uma interface mais intuitiva, mesmo depois de alterar as expectativas.


10

As propriedades devem retornar muito rapidamente, retornar o mesmo valor com chamadas repetidas e obter seu valor não deve ter efeitos colaterais. O que você descreve aqui não deve ser implementado como uma propriedade.

Sua intuição está correta. Um método de fábrica não retorna o mesmo valor com chamadas repetidas. Ele retorna uma nova instância a cada vez. Isso praticamente o desqualifica como propriedade. Também não é rápido se o desenvolvimento posterior adicionar peso à criação da instância, por exemplo, dependências de rede.

As propriedades geralmente devem ser operações muito simples que obtêm / configuram parte do estado atual da classe. As operações de fábrica não atendem a esse critério.


11
A DateTime.Nowpropriedade é comumente usada e refere-se a um novo objeto. Eu acho que o nome de uma propriedade pode ajudar a remover a ambiguidade sobre se o mesmo objeto é retornado ou não a cada vez. Está tudo no nome e como é usado.
Greg Burghardt

3
DateTime.Now é considerado um erro. Veja stackoverflow.com/questions/5437972/…
Brad Thomas

@GregBurghardt O efeito colateral de um novo objeto pode não ser um problema por si só, porque DateTimeé um tipo de valor e não tem conceito de identidade do objeto. Se bem entendi, o problema é que não é determinístico e está retornando um novo valor a cada vez.
Zev Spitz #

11
@GregBurghardt DateTime.Newé estático e, portanto, você não pode esperar que ele represente o estado de um objeto.
Tsahi Asher

5

Eu não acho que haja uma resposta independente de idioma para isso, já que o que constitui uma "propriedade" é uma pergunta específica do idioma, e o que um chamador de uma "propriedade" espera também é uma pergunta específica do idioma. Eu acho que a maneira mais proveitosa de pensar sobre isso é pensar no que parece do ponto de vista do chamador.

Em C #, as propriedades são distintas por serem capitalizadas (convencionalmente) (como métodos), mas não possuem parênteses (como variáveis ​​de instância pública). Se você vir o código a seguir, documentação ausente, o que você espera?

var reciprocalHeading = myHeading.Reciprocal;

Como um novato em C # relativo, mas que leu as Diretrizes de uso de propriedades da Microsoft , eu esperaria Reciprocal, entre outras coisas:

  1. ser um membro de dados lógicos da Headingclasse
  2. ser barato para ligar, de modo que não seja necessário armazenar em cache o valor
  3. falta efeitos colaterais observáveis
  4. produz o mesmo resultado se chamado duas vezes consecutivas
  5. (talvez) ofereça um ReciprocalChangedevento

Dessas suposições, (3) e (4) provavelmente estão corretas (supondo que Headingseja um tipo de valor imutável, como na resposta de Ewan ), (1) é discutível, (2) é desconhecido, mas também discutível e (5) é improvável que faça sentido semântico (embora o que quer que tenha um cabeçalho talvez deva ter um HeadingChangedevento). Isso me sugere que em uma API C #, "obter ou calcular o recíproco" não deve ser implementado como uma propriedade, mas principalmente se o cálculo for barato e Headingimutável, é um caso limítrofe.

(Observe, porém, que nenhuma dessas preocupações tem nada a ver com a possibilidade de chamar a propriedade criar uma nova instância , nem mesmo (2). Criar objetos no CLR, por si só, é bastante barato.)

Em Java, as propriedades são uma convenção de nomenclatura de método. Se eu ver

Heading reciprocalHeading = myHeading.getReciprocal();

minhas expectativas são semelhantes às acima (se menos explícitas): espero que a ligação seja barata, idempotente e sem efeitos colaterais. No entanto, fora da estrutura do JavaBeans, o conceito de "propriedade" não é tão significativo em Java, e particularmente ao considerar uma propriedade imutável sem correspondência setReciprocal(), a getXXX()convenção agora é um tanto antiquada. Do Effective Java , segunda edição (já com mais de oito anos):

Os métodos que retornam uma não- booleanfunção ou atributo do objeto no qual são chamados geralmente são nomeados com um substantivo, frase substantiva ou uma frase verbal que começa com o verbo get…. Existe um contingente vocal que afirma que apenas a terceira forma (começando com get) é aceitável, mas há pouca base para essa afirmação. As duas primeiras formas geralmente levam a um código mais legível ... (p. 239)

Em uma API contemporânea e mais fluente, então, eu esperaria ver

Heading reciprocalHeading = myHeading.reciprocal();

- o que sugere novamente que a chamada é barata, idempotente e sem efeitos colaterais, mas não diz nada sobre a realização de um novo cálculo ou a criação de um novo objeto. Isto é bom; em uma boa API, eu não deveria me importar.

Em Ruby, não existe uma propriedade. Existem "atributos", mas se eu vir

reciprocalHeading = my_heading.reciprocal

Não tenho como saber imediatamente se estou acessando uma variável de instância @reciprocalpor meio de um attr_readermétodo ou simples de acessador ou se estou chamando um método que executa um cálculo caro. O fato de o nome do método ser um substantivo simples, no entanto, em vez de dizer calcReciprocal, sugere novamente que a chamada é pelo menos barata e provavelmente não tem efeitos colaterais.

Em Scala, a convenção de nomenclatura é que métodos com efeitos colaterais recebem parênteses e métodos sem eles, mas

val reciprocal = heading.reciprocal

pode ser um dos seguintes:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Observe que o Scala permite várias coisas que, no entanto, são desencorajadas pelo guia de estilo . Esse é um dos meus principais aborrecimentos com o Scala.)

A falta de parênteses me diz que a ligação não tem efeitos colaterais; o nome, novamente, sugere que a ligação deve ser relativamente barata. Além disso, eu não me importo como isso me dá valor.

Em resumo: conheça o idioma que você está usando e saiba quais expectativas outros programadores trarão para sua API. Tudo o resto é um detalhe de implementação.


11
Marque uma das minhas respostas favoritas com +1, obrigado por dedicar seu tempo a escrever isso.
precisa saber é o seguinte

2

Como outros já disseram, é um padrão bastante comum retornar instâncias da mesma classe.

A nomeação deve andar de mãos dadas com as convenções de nomeação do idioma.

Por exemplo, em Java, eu provavelmente esperaria que fosse chamado getReciprocal();

Dito isto, eu consideraria objetos mutáveis ​​vs imutáveis .

Com os imutáveis , as coisas são bem fáceis e não machucam se você devolver o mesmo objeto ou não.

Com os mutáveis , isso pode ser muito assustador.

b = a.reciprocal
a += 1
b = what?

Agora, a que b está se referindo? O recíproco do valor original de a? Ou o recíproco do que mudou? Pode ser um exemplo muito curto, mas você entendeu.

Nesses casos, procure um nome melhor que comunique o que acontece, por exemplo, createReciprocal()pode ser uma escolha melhor.

Mas isso realmente depende do contexto também.


Você quis dizer mutável ?
Carsten S 8/16

@CarstenS Sim, claro, obrigado. Não faço ideia onde estava minha cabeça. :)
Eiko

Discordo um pouco getReciprocal(), pois isso "soa" como um getter de estilo JavaBeans "normal". Prefere "createXXX ()" ou possivelmente "calculateXXX ()" que, indicar ou dica para outros programadores que algo diferente está acontecendo
user949300

@ user949300 Concordo um pouco com suas preocupações - e mencionei o nome create ... mais abaixo. Existem desvantagens também. create ... implica novas instâncias que nem sempre podem ser o caso (pense em objetos dedicados singulares para alguns casos especiais), calcule ... tipos de detalhes de implementação de vazamentos - o que pode incentivar suposições erradas no preço.
Eiko

1

No interesse da responsabilidade única e da clareza, eu teria um ReverseHeadingFactory que pegasse o objeto e retornasse seu reverso. Isso deixaria mais claro que um novo objeto estava sendo retornado e significaria que o código para produzir o inverso foi encapsualizado para longe de outro código.


11
+1 para a clareza do nome, mas o que há de errado com o SRP em outras soluções?
precisa saber é o seguinte

3
Por que você recomenda uma classe de fábrica em vez de um método de fábrica? (Na minha opinião, uma responsabilidade útil de um objeto é muitas vezes a objetos emitem que são como a própria, como iterator.next Será que essa violação leve de SRP causar outros problemas de engenharia de software.?)
dcorking

2
@dcorking Uma classe de fábrica versus método (na minha opinião) geralmente é o mesmo tipo de pergunta que "O objeto é comparável ou devo fazer um comparador?" É sempre bom fazer um comparador, mas só é bom compará-lo se for uma maneira bastante universal de compará-lo. (Por exemplo, números maiores são maiores que números menores, isso é bom para comparáveis, mas você poderia dizer que todos os pares são maiores que todas as probabilidades, eu transformaria isso em um comparador) - Então, para isso, eu acho que existe apenas um maneira de reverter um cabeçalho, então eu me inclinaria para criar um método para evitar uma classe extra.
Captain Man

0

Depende. Normalmente, espero que as propriedades retornem coisas que fazem parte de uma instância, portanto, retornar uma instância diferente da mesma classe seria um pouco incomum.

Mas, por exemplo, uma classe de string pode ter propriedades "firstWord", "lastWord" ou se manipular Unicode "firstLetter", "lastLetter", que seriam objetos de string completos - apenas geralmente menores.


0

Como as outras respostas abrangem a parte Propriedade, abordarei seu número 2 sobre reciprocal:

Não use outra coisa senão reciprocal. É o único termo correto para o que você está descrevendo (e é o termo formal). Não escreva software incorreto para evitar que seus desenvolvedores aprendam o domínio em que estão trabalhando. Na navegação, os termos têm significados muito específicos e usam algo aparentemente inócuo reverseou oppositepode causar confusão em determinados contextos.


0

Logicamente, não, não é propriedade de um cabeçalho. Se fosse, você também poderia dizer que um número negativo é propriedade desse número e, francamente, faria com que "propriedade" perdesse todo o significado, quase tudo seria uma propriedade.

Recíproca é uma função pura de um cabeçalho, o mesmo que negativo de um número é uma função pura desse número.

Como regra geral, se a configuração não fizer sentido, provavelmente não deve ser uma propriedade. Defini-lo ainda pode não ser permitido, é claro, mas adicionar um levantador deve ser teoricamente possível e significativo.

Agora, em alguns idiomas, pode fazer sentido tê-lo como uma propriedade, conforme especificado no contexto desse idioma, caso traga algumas vantagens devido à mecânica desse idioma. Por exemplo, se você deseja armazená-lo em um banco de dados, provavelmente é sensato torná-lo propriedade, se permitir o manuseio automático da estrutura do ORM. Ou talvez seja uma linguagem em que não haja distinção entre uma propriedade e uma função-membro sem parâmetros (não conheço essa linguagem, mas tenho certeza de que há algumas). Cabe ao designer e documentador da API distinguir entre funções e propriedades.


Edit: Para C # especificamente, eu examinaria as classes existentes, especialmente as da Biblioteca Padrão.

  • Existem BigIntegere Complexestruturas. Mas são estruturas e têm apenas funções estáticas, e todas as propriedades são de tipos diferentes. Portanto, não há muita ajuda de design para sua Headingclasse.
  • O Math.NET Numerics possui Vectorclasse , que possui muito poucas propriedades e parece bem próxima da sua Heading. Se você passar por isso, terá uma função recíproca, não uma propriedade.

Você pode querer olhar para as bibliotecas realmente usadas pelo seu código ou para as classes similares existentes. Tente manter a consistência, geralmente é o melhor, se um caminho não estiver claramente certo e o outro errado.


-1

Pergunta muito interessante mesmo. Não vejo violação do SOLID + DRY + KISS no que você está propondo, mas, ainda assim, cheira mal.

Um método que retorna uma instância da classe é chamado de construtor , certo? então você está criando uma nova instância a partir de um método não construtor. Não é o fim do mundo, mas como cliente, eu não esperava isso. Isso geralmente é feito em circunstâncias específicas: uma fábrica ou, às vezes, um método estático da mesma classe (útil para singletons e para ... programadores não tão orientados a objetos?)

Plus: o que acontece se você invocar getReciprocal()o objeto retornado? você obtém outra instância da classe, que pode ser uma cópia exata da primeira! E se você invocar getReciprocal()naquele? Objetos espalhando alguém?

Novamente: e se o invocador precisar do valor recíproco (como escalar, quero dizer)? getReciprocal()->getValue()? estamos violando a Lei de Deméter por pequenos ganhos.


Eu aceitaria de bom grado contra-argumentos do downvoter, obrigado!
Dr. Gianluigi Zane Zanettini 8/16

11
Não diminuí o voto, mas suspeito que o motivo pode ser o fato de que realmente não adiciona muito ao restante das respostas, mas posso estar errado.
precisa saber é o seguinte

2
O SOLID e o KISS são frequentemente opostos um ao outro.
Whatsisname
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.