Digamos, por exemplo, que você tenha um aplicativo com uma classe amplamente compartilhada chamada User
. Esta classe expõe todas as informações sobre o usuário, seu ID, nome, níveis de acesso a cada módulo, fuso horário etc.
Os dados do usuário são obviamente amplamente referenciados em todo o sistema, mas por qualquer motivo, o sistema é configurado para que, em vez de passar esse objeto de usuário para classes que dependem dele, apenas passemos propriedades individuais dele.
Uma classe que requer o ID do usuário exigirá simplesmente o GUID userId
como parâmetro, às vezes também podemos precisar do nome de usuário, para que seja transmitido como um parâmetro separado. Em alguns casos, isso é passado para métodos individuais, portanto, os valores não são mantidos no nível da classe.
Toda vez que preciso acessar informações diferentes da classe User, preciso fazer alterações adicionando parâmetros e, onde adicionar uma nova sobrecarga não é apropriado, também preciso alterar todas as referências ao método ou construtor de classe.
O usuário é apenas um exemplo. Isso é amplamente praticado em nosso código.
Estou certo ao pensar que isso é uma violação do princípio Aberto / Fechado? Não apenas o ato de alterar as classes existentes, mas configurá-las em primeiro lugar, para que mudanças generalizadas provavelmente sejam necessárias no futuro?
Se apenas passássemos no User
objeto, eu poderia fazer uma pequena alteração na classe com a qual estou trabalhando. Se eu precisar adicionar um parâmetro, talvez seja necessário fazer dezenas de alterações nas referências à classe.
Existem outros princípios quebrados por essa prática? Inversão de dependência, talvez? Embora não façamos referência a uma abstração, existe apenas um tipo de usuário; portanto, não há necessidade real de ter uma interface de usuário.
Existem outros princípios não SOLID sendo violados, como princípios básicos de programação defensiva?
Meu construtor deve ficar assim:
MyConstructor(GUID userid, String username)
Ou isto:
MyConstructor(User theUser)
Edição da postagem:
Foi sugerido que a pergunta seja respondida em "Pass ID or Object?". Isso não responde à questão de como a decisão de ir de qualquer maneira afeta uma tentativa de seguir os princípios do SOLID, que está no cerne desta questão.
I
emSOLID
?MyConstructor
basicamente diz agora "eu preciso de umGuid
e umstring
". Então, por que não ter uma interface fornecendo aGuid
e astring
, vamosUser
implementar essa interface e vamosMyConstructor
depender de uma instância implementando essa interface? E se as necessidades deMyConstructor
mudança, altere a interface. - Isso me ajudou muito a pensar nas interfaces para "pertencer" ao consumidor e não ao provedor . Então pense "como consumidor eu preciso de algo que faça isso e aquilo" em vez de "como provedor, eu posso fazer isso e aquilo".Respostas:
Não há absolutamente nada de errado em passar um
User
objeto inteiro como parâmetro. De fato, isso pode ajudar a esclarecer seu código e tornar mais óbvio para os programadores o que um método leva se a assinatura do método exigir aUser
.Passar tipos de dados simples é bom, até que eles signifiquem algo diferente do que são. Considere este exemplo:
E um exemplo de uso:
Você consegue identificar o defeito? O compilador não pode. O "ID do usuário" sendo passado é apenas um número inteiro. Nomeamos a variável,
user
mas inicializamos seu valor a partir doblogPostRepository
objeto, que provavelmente retornaBlogPost
objetos, nãoUser
objetos - ainda assim, o código é compilado e você acaba com um erro de execução instável.Agora considere este exemplo alterado:
Talvez o
Bar
método use apenas o "ID do usuário", mas a assinatura do método exija umUser
objeto. Agora vamos voltar ao mesmo exemplo de uso de antes, mas altere-o para passar o "usuário" inteiro em:Agora temos um erro do compilador. O
blogPostRepository.Find
método retorna umBlogPost
objeto, que habilmente chamamos de "usuário". Em seguida, passamos esse "usuário" para oBar
método e obtemos imediatamente um erro do compilador, porque não podemos passar aBlogPost
para um método que aceita aUser
.O sistema de tipos do idioma está sendo aproveitado para escrever o código correto mais rapidamente e identificar defeitos no tempo de compilação, em vez do tempo de execução.
Realmente, ter que refatorar muito código, pois as alterações nas informações do usuário são apenas um sintoma de outros problemas. Ao passar um
User
objeto inteiro, você obtém os benefícios acima, além dos benefícios de não precisar refatorar todas as assinaturas de métodos que aceitam informações do usuário quando algo sobre aUser
classe é alterado.fonte
Não, não é uma violação desse princípio. Esse princípio se preocupa em não mudar
User
de maneira que afete outras partes do código que o utiliza. Suas alteraçõesUser
podem ser uma violação, mas não estão relacionadas.Não. O que você descreve - injetando apenas as partes necessárias de um objeto de usuário em cada método - é o oposto: é pura inversão de dependência.
Não. Essa abordagem é uma maneira perfeitamente válida de codificação. Não está violando esses princípios.
Mas a inversão de dependência é apenas um princípio; não é uma lei inquebrável. E o DI puro pode adicionar complexidade ao sistema. Se você achar que apenas a injeção dos valores necessários do usuário nos métodos, em vez de passar todo o objeto do usuário para o método ou construtor, cria problemas, não faça dessa maneira. Trata-se de obter um equilíbrio entre princípios e pragmatismo.
Para endereçar seu comentário:
Parte da questão aqui é que você claramente não gosta dessa abordagem, conforme o comentário "desnecessariamente [passe] ...". E isso é justo o suficiente; não há resposta certa aqui. Se você acha pesado, não faça dessa maneira.
No entanto, com relação ao princípio de aberto / fechado, se você seguir estritamente isso, "... altere todas as referências a todos os cinco desses métodos existentes ..." é uma indicação de que esses métodos foram modificados, quando deveriam ser fechado para modificação. Na realidade, porém, o princípio aberto / fechado faz sentido para APIs públicas, mas não faz muito sentido para as partes internas de um aplicativo.
Mas então você entra no território YAGNI e ainda seria ortogonal ao princípio. Se você possui um método
Foo
que usa um nome de usuário e também desejaFoo
uma data de nascimento, seguindo o princípio, você adiciona um novo método;Foo
permanece inalterado. Novamente, é uma boa prática para APIs públicas, mas é um absurdo para código interno.Como mencionado anteriormente, trata-se de equilíbrio e bom senso para qualquer situação. Se esses parâmetros frequentemente mudam, sim, use
User
diretamente. Isso o salvará das alterações em larga escala que você descreve. Mas se eles não mudam frequentemente, passar apenas o necessário também é uma boa abordagem.fonte
User
instância e depois consultar esse objeto para obter um parâmetro, estará invertendo apenas parcialmente as dependências; ainda há algumas perguntas acontecendo. A inversão de dependência verdadeira é 100% "diga, não pergunte". Mas tem um preço de complexidade.Sim, alterar uma função existente é uma violação do Princípio Aberto / Fechado. Você está modificando algo que deve ser fechado para modificação devido a alterações nos requisitos. Um design melhor (para não mudar quando os requisitos mudam) seria passar o Usuário para as coisas que deveriam funcionar nos usuários.
Mas isso pode estar em conflito com o Princípio de Segregação de Interface, já que você pode estar transmitindo muito mais informações do que a função precisa para fazer seu trabalho.
Então, como na maioria das coisas - depende .
Usando apenas um nome de usuário, a função é mais flexível, trabalhando com nomes de usuários, independentemente de onde eles vêm e sem a necessidade de criar um objeto Usuário totalmente funcional. Ele fornece resiliência à mudança se você acha que a fonte de dados mudará.
O uso de todo o usuário torna mais claro o uso e faz um contrato mais firme com os chamadores. Ele fornece resiliência à mudança se você achar que mais do usuário será necessário.
fonte
User.find()
. De fato, nem deveria haver umUser.find
. Encontrar um usuário nunca deve ser de responsabilidade deUser
.User
à função. Talvez isso faça sentido. Mas talvez a função deva se importar apenas com o nome do usuário - e transmitir informações como a data de ingresso do usuário ou endereço incorreto.User
classe-não deve ter. Pode haver umUserRepository
ou similar que lide com essas coisas.Esse design segue o padrão de objeto de parâmetro . Resolve problemas que surgem com muitos parâmetros na assinatura do método.
Não. A aplicação desse padrão habilita o princípio de abrir / fechar (OCP). Por exemplo, classes derivadas de
User
podem ser fornecidas como parâmetro que induzem um comportamento diferente na classe consumidora.Isso pode acontecer. Deixe-me explicar com base nos princípios do SOLID.
O princípio de responsabilidade única (SRP) pode ser violado se tiver o design conforme explicado:
O problema está com todas as informações . Se a
User
classe tiver muitas propriedades, ela se tornará um enorme objeto de transferência de dados que transporta informações não relacionadas da perspectiva das classes consumidoras. Exemplo: Do ponto de vista de uma classe consumirUserAuthentication
a propriedadeUser.Id
eUser.Name
são relevantes, mas nãoUser.Timezone
.O princípio de segregação de interface (ISP) também é violado com um raciocínio semelhante, mas adiciona outra perspectiva. Exemplo: Suponha que uma classe consumidora
UserManagement
exija que a propriedadeUser.Name
seja divididaUser.LastName
eUser.FirstName
a classeUserAuthentication
também seja modificada para isso.Felizmente, o ISP também oferece uma saída possível para o problema: geralmente esses objetos de parâmetro ou objetos de transporte de dados começam pequenos e crescem com o tempo. Se isso se tornar pesado, considere a seguinte abordagem: Introduza interfaces personalizadas para as necessidades das classes consumidoras. Exemplo: introduza interfaces e deixe a
User
classe derivar dela:Cada interface deve expor um subconjunto de propriedades relacionadas da
User
classe necessárias para que uma classe consumidora cumpra sua operação. Procure clusters de propriedades. Tente reutilizar as interfaces. No caso da classe consumidora,UserAuthentication
use emIUserAuthenticationInfo
vez deUser
. Se possível, divida aUser
classe em várias classes concretas usando as interfaces como "estêncil".fonte
Quando confrontado com esse problema no meu próprio código, concluí que as classes / objetos básicos do modelo são a resposta.
Um exemplo comum seria o padrão do repositório. Freqüentemente, ao consultar um banco de dados por meio de repositórios, muitos dos métodos no repositório usam muitos dos mesmos parâmetros.
Minhas regras práticas para repositórios são:
Onde mais de um método usa os mesmos 2 ou mais parâmetros, os parâmetros devem ser agrupados como um objeto de modelo.
Onde um método usa mais de 2 parâmetros, os parâmetros devem ser agrupados como um objeto de modelo.
Os modelos podem herdar de uma base comum, mas apenas quando realmente faz sentido (geralmente é melhor refatorar mais tarde do que começar com a herança em mente).
Os problemas com o uso de modelos de outras camadas / áreas não se tornam aparentes até que o projeto comece a se tornar um pouco complexo. É só então que você descobre que menos código cria mais trabalho ou mais complicações.
E sim, é totalmente bom ter 2 modelos diferentes com propriedades idênticas que atendem a diferentes camadas / finalidades (por exemplo, ViewModels vs POCOs).
fonte
Vamos apenas verificar os aspectos individuais do SOLID:
Uma coisa que tende a confundir os instintos de design é que a classe é essencialmente para objetos globais e essencialmente somente leitura. Em tal situação, violar abstrações não prejudica muito: apenas ler dados que não são modificados cria um acoplamento bastante fraco; somente quando se torna uma pilha enorme, a dor se torna perceptível.
Para restabelecer os instintos de design, basta supor que o objeto não seja muito global. Que contexto uma função precisaria se o
User
objeto pudesse ser alterado a qualquer momento? Quais componentes do objeto provavelmente seriam mutados juntos? Eles podem ser separadosUser
, seja como um subobjeto referenciado ou como uma interface que exponha apenas uma "fatia" de campos relacionados não seja tão importante.Outro princípio:
User
observe as funções que usam partes de e veja quais campos (atributos) tendem a andar juntos. Essa é uma boa lista preliminar de subobjetos - você definitivamente precisa pensar se eles realmente pertencem um ao outro.É muito trabalhoso, e é um pouco difícil de fazer, e seu código se tornará um pouco menos flexível, pois ficará um pouco mais difícil identificar o subobjeto (subinterface) que precisa ser passado para uma função, principalmente se os subobjetos tiverem sobreposições.
A divisão
User
ficará realmente feia se os subobjetos se sobreporem, então as pessoas ficarão confusas sobre qual escolher se todos os campos obrigatórios são da sobreposição. Se você se separar hierarquicamente (por exemplo, você temUserMarketSegment
qual, entre outras coisas, possuiUserLocation
), as pessoas não têm certeza de qual nível a função que estão escrevendo: está lidando com dados do usuário noLocation
nível ou noMarketSegment
nível? Isso não ajuda exatamente o fato de que isso pode mudar com o tempo, ou seja, você está novamente alterando as assinaturas das funções, às vezes em toda uma cadeia de chamadas.Em outras palavras: a menos que você realmente conheça seu domínio e tenha uma idéia bastante clara de qual módulo está lidando com quais aspectos
User
, não vale a pena melhorar a estrutura do programa.fonte
Esta é uma pergunta realmente interessante. Depende.
Se você acha que seu método pode mudar internamente no futuro para exigir parâmetros diferentes do objeto Usuário, certamente deve passar a coisa toda. A vantagem é que o código externo ao método é protegido contra alterações no método em termos de quais parâmetros ele está usando, o que, como você diz, causaria uma cascata de alterações externamente. Assim, a passagem de todo o usuário aumenta o encapsulamento.
Se você tiver certeza de que nunca precisará usar nada além de dizer o e-mail do usuário, deve passar isso. A vantagem disso é que você pode usar o método em uma ampla variedade de contextos: você pode usá-lo por exemplo com o email de uma empresa ou com um email digitado por alguém. Isso aumenta a flexibilidade.
Isso faz parte de uma gama mais ampla de perguntas sobre a criação de classes para ter um escopo amplo ou estreito, incluindo injetar dependências ou não e ter objetos disponíveis globalmente. No momento, há uma tendência infeliz de pensar que um escopo mais restrito é sempre bom. No entanto, sempre há uma troca entre encapsulamento e flexibilidade, como neste caso.
fonte
Acho melhor passar o menor número possível de parâmetros e quantos forem necessários. Isso facilita o teste e não requer a criação de objetos inteiros.
No seu exemplo, se você usará apenas o ID do usuário ou o nome do usuário, é tudo o que deve passar. Se esse padrão se repetir várias vezes e o objeto do usuário real for muito maior, meu conselho é criar uma interface menor para isso. Poderia ser
ou
Isso facilita muito o teste com zombaria e você sabe instantaneamente quais valores são realmente usados. Caso contrário, você frequentemente precisará inicializar objetos complexos com muitas outras dependências, embora no final você precise apenas de uma ou duas propriedades.
fonte
Aqui está algo que eu encontrei de tempos em tempos:
User
(ouProduct
qualquer outra coisa) que possui muitas propriedades, mesmo que o método use apenas algumas delas.User
objeto totalmente preenchido . Ele cria uma instância e inicializa apenas as propriedades que o método realmente precisa.User
argumento, encontra-se precisando encontrar chamadas para esse método para descobrir de ondeUser
vem a origem, para saber quais propriedades são preenchidas. É um usuário "real" com um endereço de email ou foi criado apenas para passar um ID de usuário e algumas permissões?Se você criar
User
e preencher apenas algumas propriedades porque essas são as que o método precisa, o chamador realmente saberá mais sobre o funcionamento interno do método do que deveria.Pior ainda, quando você tem uma instância de
User
, precisa saber de onde veio, para saber quais propriedades são preenchidas. Você não quer saber disso.Com o tempo, quando os desenvolvedores os vêem
User
como um contêiner para argumentos de método, eles podem começar a adicionar propriedades a ele para cenários de uso único. Agora está ficando feio, porque a classe está ficando cheia de propriedades que quase sempre serão nulas ou padrão.Essa corrupção não é inevitável, mas acontece repetidamente quando passamos um objeto apenas porque precisamos acessar algumas de suas propriedades. A zona de perigo é a primeira vez que você vê alguém criando uma instância
User
e apenas preenchendo algumas propriedades para que elas possam passar para um método. Coloque o pé nele porque é um caminho escuro.Sempre que possível, defina o exemplo certo para o próximo desenvolvedor passando apenas o que você precisa.
fonte