O is
operador de C # e o instanceof
operador de Java permitem ramificar na interface (ou, mais a sério, em sua classe base) que uma instância de objeto implementou.
É apropriado usar esse recurso para ramificação de alto nível com base nos recursos que uma interface fornece?
Ou uma classe base deve fornecer variáveis booleanas para fornecer uma interface para descrever os recursos que um objeto possui?
Exemplo:
if (account is IResetsPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
vs.
if (account.CanResetPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
Existem algumas armadilhas no uso da implementação de interface para identificação de capacidade?
Este exemplo foi apenas isso, um exemplo. Estou pensando em uma aplicação mais geral.
object-oriented
TheCatWhisperer
fonte
fonte
CanResetPassword
isso só será verdade quando algo for implementadoIResetsPassword
. Agora você está fazendo uma propriedade determinar algo que o sistema de tipos deve controlar (e finalmente determinará quando você pré-forma a conversão).Respostas:
Com a ressalva de que é melhor evitar downcasting, se possível, a primeira forma é preferível por dois motivos:
is
incêndios, o downcast definitivamente funcionará. No segundo formulário, você precisa garantir manualmente que apenas os objetos que implementamIResetsPassword
retornem true para essa propriedadeIsso não quer dizer que você não possa melhorar um pouco esses problemas. Você poderia, por exemplo, ter a classe base com um conjunto de interfaces publicadas nas quais você pode verificar a inclusão. Mas, na verdade, você está apenas implementando manualmente manualmente uma parte do sistema de tipos existente que é redundante.
Aliás, minha preferência natural é a composição sobre a herança, mas principalmente no que diz respeito ao estado. Herança de interface não é tão ruim, geralmente. Além disso, se você se encontra implementando a hierarquia de tipos de pessoas pobres, é melhor usar a que já existe, pois o compilador o ajudará.
fonte
(account as IResettable)?.ResetPassword();
ouvar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Os recursos de um objeto não devem ser identificados.
O cliente que usa um objeto não deve saber nada sobre como ele funciona. O cliente deve saber apenas coisas que pode dizer ao objeto para fazer. O que o objeto faz, uma vez informado, não é o problema do cliente.
Então, ao invés de
ou
considerar
ou
porque
account
já sabe se isso vai funcionar. Por que perguntar isso? Apenas diga o que deseja e deixe fazer o que for necessário.Imagine isso feito de tal maneira que o cliente não se importe se funcionou ou não, porque isso é outro problema. O trabalho dos clientes era apenas fazer a tentativa. É feito agora e tem outras coisas para lidar. Esse estilo tem muitos nomes, mas o nome que eu mais gosto é dizer, não pergunte .
É muito tentador ao escrever para o cliente pensar que você precisa acompanhar tudo e, assim, puxar em sua direção tudo o que acha que precisa saber. Quando você faz isso, vira objetos de dentro para fora. Valorize sua ignorância. Afaste os detalhes e deixe que os objetos lidem com eles. Quanto menos você souber, melhor.
fonte
if (!account.AttemptPasswordReset()) /* attempt failed */;
Dessa forma, o cliente conhece o resultado, mas evita alguns dos problemas com a lógica de decisão na pergunta. Se a tentativa for assíncrona, você passaria um retorno de chamada.account
quando foi construído. Pop-ups, registros, impressões e alertas de áudio podem estar acontecendo neste momento. O trabalho do cliente é dizer "faça agora". Não precisa fazer mais do que isso. Você não precisa fazer tudo em um só lugar.fundo
A herança é uma ferramenta poderosa que serve a um propósito na programação orientada a objetos. No entanto, ele não resolve todos os problemas de maneira elegante: às vezes, outras soluções são melhores.
Se você se lembrar de suas primeiras aulas de ciência da computação (supondo que tenha um diploma em ciências da computação), lembre-se de um professor fornecendo um parágrafo que declara o que o cliente deseja que o software faça. Seu trabalho é ler o parágrafo, identificar os atores e ações e apresentar um esboço de quais são as classes e métodos. Haverá algumas pistas ruins que parecem importantes, mas não são. Há uma possibilidade muito real de interpretar mal os requisitos também.
Essa é uma habilidade importante que até os mais experientes cometem errado: identificar adequadamente os requisitos e traduzi-los para idiomas de máquina.
Sua pergunta
Com base no que você escreveu, acho que você pode estar entendendo mal o caso geral de ações opcionais que as classes podem executar. Sim, eu sei que seu código é apenas um exemplo e você está interessado no caso geral. No entanto, parece que você deseja saber como lidar com a situação em que certos subtipos de um objeto podem executar uma ação, mas outros não.
Só porque um objeto como um
account
tem um tipo de conta não significa que isso se traduz em um tipo em um idioma OO. "Tipo" em uma linguagem humana nem sempre significa "classe". No contexto de uma conta, "tipo" pode se correlacionar mais estreitamente com "conjunto de permissões". Você deseja usar uma conta de usuário para executar uma ação, mas essa ação pode ou não ser capaz de ser executada por essa conta. Em vez de usar herança, eu usaria um delegado ou token de segurança.Minha solução
Considere uma classe de conta que possui várias ações opcionais que pode executar. Em vez de definir "pode executar a ação X" por herança, por que a conta não retorna um objeto delegado (redefinição de senha, remetente de formulário etc.) ou um token de acesso?
O benefício da última opção existe e se eu quiser usar as credenciais da minha conta para redefinir a senha de outra pessoa ?
O sistema não só é mais flexível, como também removi os tipos de conversão, mas o design é mais expressivo . Eu posso ler isso e entender facilmente o que está fazendo e como pode ser usado.
TL; DR: isso parece um problema XY . Geralmente, quando confrontados com duas opções abaixo do ideal, vale a pena dar um passo para trás e pensar "o que estou realmente tentando realizar aqui? Devo realmente estar pensando em como tornar a produção tipográfica menos feia ou procurar maneiras de remover completamente o typecast? "
fonte
account.getPasswordResetter().resetPassword();
.The benefit to the last option there is what if I want to use my account credentials to reset someone else's password?
.. fácil. Você cria um objeto de domínio da conta para a outra conta e usa essa. As classes estáticas são problemáticas para o teste de unidade (esse método, por definição, precisa de acesso a algum armazenamento, portanto, agora você não pode testar facilmente qualquer código que toque mais nessa classe) e é melhor evitar. A opção de retornar alguma outra interface geralmente é uma boa idéia, mas realmente não lida com o problema subjacente (você acabou de mudar o problema para outra interface).Bill Venner diz que sua abordagem é absolutamente correta ; pule para a seção "Quando usar instanceof". Você está fazendo downcasting para um tipo / interface específico que apenas implementa esse comportamento específico, para que você esteja absolutamente certo em ser seletivo.
No entanto, se você quiser usar outra abordagem, existem muitas maneiras de fazer a barba com um iaque.
Existe a abordagem do polimorfismo; você pode argumentar que todas as contas têm uma senha; portanto, todas elas devem pelo menos tentar redefinir uma senha. Isso significa que, na classe de conta base, devemos ter um
resetPassword()
método que todas as contas implementem, mas à sua maneira. A questão é como esse método deve se comportar quando a capacidade não existe.Ele pode retornar um nulo e silenciosamente completo, redefinindo a senha ou não, talvez assumindo a responsabilidade de imprimir a mensagem internamente, se não redefinir a senha. Não é a melhor ideia.
Pode retornar um valor booleano indicando se a redefinição foi bem-sucedida. Ao ativar esse booleano, poderíamos relacionar que a redefinição de senha falhou.
Pode retornar uma String indicando o resultado da tentativa de redefinição de senha. A sequência poderia fornecer mais detalhes sobre o motivo da falha de uma redefinição e a saída.
Ele pode retornar um objeto ResetResult que transmite mais detalhes e combina todos os elementos de retorno anteriores.
Ele pode retornar um vazio e, em vez disso, gerar uma exceção se você tentar redefinir uma conta que não tenha esse recurso (não faça isso, usar o tratamento de exceções para o controle de fluxo normal é uma prática recomendada por vários motivos).
Ter um
canResetPassword()
método de correspondência pode não parecer a pior coisa do mundo, pois é um recurso estático embutido na classe quando foi escrito. Essa é uma pista do motivo pelo qual a abordagem do método é uma péssima idéia, pois sugere que a capacidade é dinâmica e quecanResetPassword()
pode mudar, o que também cria a possibilidade adicional de mudar entre pedir permissão e fazer a ligação. Como mencionado em outro lugar, informe em vez de pedir permissão.A composição sobre a herança pode ser uma opção: você pode ter uma
passwordResetter
campo (ou um getter equivalente) e classe (s) equivalente (s) que você pode procurar por nulo antes de chamá-lo. Embora atue como pedir permissão, a finalidade pode evitar qualquer natureza dinâmica inferida.Você pode externalizar a funcionalidade em sua própria classe, o que pode levar uma conta como parâmetro e agir sobre ela (por exemplo,
resetter.reset(account)
), embora isso também seja frequentemente uma prática ruim (uma analogia comum é um lojista que chega à sua carteira para receber dinheiro).Em idiomas com o recurso, você pode usar mixins ou características, no entanto, acaba na posição em que começou, onde pode estar verificando a existência desses recursos.
fonte
Para este exemplo específico de " pode redefinir uma senha ", eu recomendo o uso de composição sobre herança (neste caso, herança de uma interface / contrato). Porque, ao fazer isso:
Você está especificando imediatamente (em tempo de compilação) que sua classe ' pode redefinir uma senha '. Mas, se no seu cenário, a presença do recurso for condicional e depender de outras coisas, você não poderá mais especificar as coisas no tempo de compilação. Então, sugiro fazer o seguinte:
Agora, em tempo de execução, você pode verificar se
myFoo.passwordResetter != null
antes de executar esta operação. Se você deseja desacoplar ainda mais as coisas (e planeja adicionar muito mais recursos), você pode:ATUALIZAR
Depois de ler outras respostas e comentários do OP, entendi que a pergunta não é sobre a solução composicional. Então, acho que a pergunta é sobre como identificar melhor as capacidades dos objetos em geral, em um cenário como o abaixo:
Agora, tentarei analisar todas as opções mencionadas:
Segundo exemplo do OP:
Digamos que este código esteja recebendo alguma classe de conta base (por exemplo:
BaseAccount
no meu exemplo); isso é ruim, pois está inserindo booleanos na classe base, poluindo-o com código que não faz nenhum sentido estar lá.Primeiro exemplo do OP:
Para responder à pergunta, isso é mais apropriado do que a opção anterior, mas, dependendo da implementação, ele quebrará o princípio L de sólido, e provavelmente verificações como essa seriam espalhadas pelo código e dificultariam a manutenção.
O que outras pessoas estão dizendo
Se esse
ResetPassword
método for inserido naBaseAccount
classe, também estará poluindo a classe base com código inadequado, como no segundo exemplo do OPResposta do boneco de neve:
Essa é uma boa solução, mas considera que os recursos são dinâmicos (e podem mudar com o tempo). No entanto, depois de ler vários comentários do OP, acho que a conversa aqui é sobre polimorfismo e classes definidas estaticamente (embora o exemplo de contas aponte intuitivamente para o cenário dinâmico). EG: neste
AccountManager
exemplo, a verificação de permissão seria consultas ao DB; na pergunta do OP, as verificações são tentativas de converter os objetos.Outra sugestão minha:
Use o padrão Method Method para ramificações de alto nível. A hierarquia de classes mencionada é mantida como é; criamos apenas manipuladores mais apropriados para os objetos, a fim de evitar projeções e propriedades / métodos inadequados que poluem a classe base.
Você pode vincular a operação à classe de conta apropriada usando um dicionário / hashtable ou executar operações em tempo de execução usando métodos de extensão, usar
dynamic
palavra-chave ou, como última opção, usar apenas uma conversão para passar o objeto de conta para a operação (em neste caso, o número de lançamentos é apenas um, no início da operação).fonte
Nenhuma das opções que você apresentou é boa OO. Se você está escrevendo declarações if em torno do tipo de um objeto, provavelmente está fazendo OO errado (existem exceções, essa não é uma). Aqui está a resposta OO simples para sua pergunta (pode não ser C # válido):
Modifiquei este exemplo para corresponder ao que o código original estava fazendo. Com base em alguns comentários, parece que as pessoas estão se perguntando se esse é o código específico 'certo' aqui. Esse não é o objetivo deste exemplo. Toda a sobrecarga polimórfica é essencialmente executar condicionalmente diferentes implementações de lógica com base no tipo do objeto. O que você está fazendo nos dois exemplos é tocar manualmente o que o seu idioma oferece como recurso. Em poucas palavras, você pode se livrar dos subtipos e colocar a capacidade de redefinir como uma propriedade booleana do tipo de Conta (ignorando outros recursos dos subtipos).
Sem uma visão mais ampla do design, é impossível saber se essa é uma boa solução para seu sistema específico. É simples e, se funcionar para o que você está fazendo, provavelmente nunca precisará pensar muito sobre isso novamente, a menos que alguém não verifique CanResetPassword () antes de chamar ResetPassword (). Você também pode retornar um booleano ou falhar silenciosamente (não recomendado). Realmente depende das especificidades do design.
fonte
NotResetable
. "uma conta pode ter mais funcionalidade do que ser redefinível", como seria de esperar, mas isso não parece ser importante para a questão em questão.Responda
Minha resposta ligeiramente opinativa para sua pergunta é:
Termo aditivo:
Divagar
Vejo que você não adicionou a
c#
tag, mas está perguntando em umobject-oriented
contexto geral .O que você está descrevendo é um tecnicismo de linguagens estáticas / de tipos fortes, e não específico de "OO" em geral. Seu problema decorre, entre outros, do fato de que você não pode chamar métodos nesses idiomas sem ter um tipo suficientemente estreito no tempo de compilação (por definição de variável, parâmetro de método / definição de valor de retorno ou conversão explícita). Eu fiz as duas variantes no passado, nesses tipos de idiomas, e ambas me parecem feias hoje em dia.
Nas linguagens OO de tipo dinâmico, esse problema não ocorre devido a um conceito chamado "digitação de pato". Em resumo, isso significa que você basicamente não declara o tipo de um objeto em nenhum lugar (exceto ao criá-lo). Você envia mensagens para objetos e os objetos lidam com essas mensagens. Você não se importa se o objeto realmente possui um método com esse nome. Alguns idiomas ainda têm um
method_missing
método genérico e abrangente, que o torna ainda mais flexível.Portanto, nesse idioma (Ruby, por exemplo), seu código se torna:
Período.
O
account
vão quer redefinir a senha, lançar uma exceção "negado", ou jogar um "não sei como lidar com 'reset_password'" exceção.Se você precisar ramificar explicitamente por qualquer motivo, ainda o faria na capacidade, não na classe:
(Onde
can?
é apenas um método que verifica se o objeto pode executar algum método, sem chamá-lo.)Observe que, embora minha preferência pessoal esteja atualmente em idiomas de tipo dinâmico, essa é apenas uma preferência pessoal. Idiomas estaticamente tipificados também têm tudo a ver com eles, então, por favor, não tome essa divagação como uma festa contra idiomas tipicamente estatísticos ...
fonte
Parece que suas opções são derivadas de diferentes forças motivacionais. O primeiro parece ser um hack empregado devido à má modelagem de objetos. Demonstrou que suas abstrações estão erradas, pois quebram o polimorfismo. Muito provavelmente, ele quebra o princípio fechado aberto , o que resulta em um acoplamento mais apertado.
Sua segunda opção pode ser boa da perspectiva OOP, mas a conversão de tipo me confunde. Se sua conta permite que você redefina sua senha, por que você precisa de uma transmissão de texto? Portanto, supondo que sua
CanResetPassword
cláusula represente algum requisito comercial fundamental que faz parte da abstração da conta, sua segunda opção é OK.fonte