Os recursos de um objeto devem ser identificados exclusivamente pelas interfaces implementadas?

36

O operador is do c # e o operador instanceof do Java permitem que você se ramifique na interface (ou, mais grosseiramente, na 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 as capacidades de um objeto?

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!");

Há alguma armadilha para usar a implementação de interface para identificação de capacidade?

Este exemplo foi apenas isso, um exemplo. Eu estou querendo saber sobre uma aplicação mais geral.

    
por TheCatWhisperer 11.12.2017 / 18:28
fonte

8 respostas

7

Com a ressalva de que é melhor evitar o cancelamento, se possível, a primeira forma é preferível por dois motivos:

  1. você está permitindo que o sistema de tipos funcione para você. No primeiro exemplo, se o is for acionado, o downcast definitivamente funcionará. No segundo formulário, você precisa garantir manualmente que somente os objetos que implementam IResetsPassword retornam true para essa propriedade
  2. classe base frágil. Você precisa adicionar uma propriedade à classe / interface base para cada interface que deseja adicionar. Isso é complicado e propenso a erros.

Isso não quer dizer que você não possa amenizar um pouco essas questões. Você poderia, por exemplo, ter a classe base com um conjunto de interfaces publicadas nas quais você pode verificar a inclusão. Mas você está apenas implementando manualmente uma parte do sistema de tipos existente que é redundante.

BTW minha preferência natural é composição sobre 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ê estiver implementando a hierarquia de tipos de um homem pobre, é melhor usar o que já está lá, pois o compilador o ajudará.

    
por 11.12.2017 / 19:32
fonte
90

Should an object's capabilities be identified exclusively by the interfaces it implements?

Os recursos de objetos não devem ser identificados.

O cliente que usa um objeto não deve ser obrigado a saber nada sobre como ele funciona. O cliente deve saber apenas as coisas que ele 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

if (account is IResetsPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

ou

if (account.CanResetPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

considere

account.ResetPassword();

ou

account.ResetPassword(authority);

porque account já sabe se isso funcionará. Por que perguntar isso? Apenas diga o que você quer e deixe fazer o que for fazer.

Imagine isso feito de tal forma que o cliente não se importe se funcionou ou não, porque isso é um problema importante. O trabalho do cliente era apenas fazer a tentativa. É feito isso agora e tem outras coisas para lidar. Este estilo tem muitos nomes, mas o nome que eu mais gosto é dizer, não pergunte .

É muito tentador, ao escrever o cliente, pensar que você precisa acompanhar tudo e, assim, puxar tudo o que você acha que precisa saber. Quando você faz isso, você vira os objetos do avesso. Valorize sua ignorância. Empurre os detalhes e deixe os objetos lidarem com eles. Quanto menos você souber, melhor.

    
por 11.12.2017 / 22:41
fonte
17

Antecedentes

Herança é uma ferramenta poderosa que serve a um propósito na programação orientada a objetos. No entanto, não resolve todos os problemas elegantemente: às vezes, outras soluções são melhores.

Se você pensar em suas primeiras aulas de informática (supondo que você tenha um grau de CS), você pode se lembrar de um professor dando a você um parágrafo que declara o que o cliente quer que o software faça. Seu trabalho é ler o parágrafo, identificar os atores e as ações, e sair com um esboço do que são as classes e os métodos. Haverá algumas pistas vagas que parecem importantes, mas não são. Existe uma possibilidade muito real de interpretar mal os requisitos também.

Esta é uma habilidade importante que até mesmo os mais experientes de nós erram: identificar corretamente os requisitos e traduzi-los em linguagens de máquina.

Sua pergunta

Com base no que você escreveu, acho que você pode estar interpretando mal o caso geral de ações opcionais que as classes podem executar. Sim, sei que seu código é apenas um exemplo e você está interessado no caso geral. No entanto, parece que você quer saber como lidar com a situação em que certos subtipos de um objeto podem executar uma ação, mas outros subtipos não podem.

Só porque um objeto como account tem um tipo de conta não significa que ele se traduza em um tipo em um idioma OO. "Tipo" em uma linguagem humana nem sempre significa "classe". No contexto de uma conta, "tipo" pode correlacionar-se mais de perto 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 executada por essa conta. Em vez de usar herança, usaria um representante ou um token de segurança.

Minha solução

Considere uma classe de conta que tenha várias ações opcionais que pode executar. Em vez de definir "pode executar a ação X" via herança, por que a conta não retorna um objeto delegado (redefinidor de senha, formador de envio, etc) ou um token de acesso?

account.getPasswordResetter().doAction();
account.getFormSubmitter().doAction(view.getContents());

AccountManager.resetPassword(account, account.getAccessToken());

O benefício para a última opção existe e se eu quiser usar as credenciais da minha conta <<> para redefinir a senha de alguém else ?

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

Não apenas o sistema é mais flexível, não apenas removi o tipo de conversão, mas o design é mais expressivo . Eu posso ler isso e entender facilmente o que está fazendo e como ele pode ser usado.

TL; DR: isso parece um problema XY . Geralmente, quando confrontados com duas opções que são sub-ótimas, vale a pena dar um passo para trás e pensar "o que estou tentando realizar aqui? Devo estar realmente pensando em como tornar a typecasting menos feia?" ou devo procurar maneiras de remover completamente o typecast? "

    
por 11.12.2017 / 21:13
fonte
1

Bill Venner diz que sua abordagem é absolutamente boa ; pule para a seção intitulada 'Quando usar instanceof'. Você está fazendo downcast para um tipo / interface específico que apenas implementa esse comportamento específico, então você está absolutamente certo em ser seletivo sobre isso.

No entanto, se você quiser usar outra abordagem, há muitas maneiras de raspar um iaque.

Existe a abordagem do polimorfismo; Você poderia argumentar que todas as contas têm uma senha, portanto, todas as contas devem pelo menos tentar redefinir uma senha. Isso significa que, na classe da conta base, devemos ter um método resetPassword() que todas as contas implementam, mas de maneira própria. A questão é como esse método deve se comportar quando a capacidade não está presente.

Ele pode retornar um vazio e silenciosamente concluir se redefine a senha ou não, talvez assumindo a responsabilidade de imprimir a mensagem internamente se ela não redefinir a senha. Não é a melhor ideia.

Ele poderia retornar um booleano indicando se a redefinição foi bem-sucedida. Ao ligar esse booleano, poderíamos relatar que a redefinição da senha falhou.

Ele pode retornar uma String indicando o resultado da tentativa de redefinição de senha. A string pode fornecer mais detalhes sobre por que uma reinicialização falhou e pode ser emitida.

Ele poderia 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, lançar uma exceção se você tentar redefinir uma conta que não tenha esse recurso (não faça isso, pois usar o tratamento de exceção para controle de fluxo normal é uma prática ruim por vários motivos) .

Ter um método canResetPassword() correspondente talvez não pareça a pior coisa do mundo, já que, na teoria, é um recurso estático incorporado à classe quando foi escrito. Isso é uma pista de por que a abordagem do método é uma má idéia, embora sugira que a capacidade é dinâmica e que canResetPassword() pode mudar, o que também cria a possibilidade de que possa mudar entre pedir permissão e fazer a ligar. Como mencionado em outro lugar, diga ao invés de pedir permissão.

A composição sobre herança pode ser uma opção: você pode ter um campo passwordResetter final (ou um equivalente) e classe (s) equivalente (s) para verificar se há nulo antes de chamá-lo. Apesar de agir como pedir permissão, a finalidade pode evitar qualquer natureza dinâmica inferida.

Você pode pensar em externalizar a funcionalidade em sua própria classe, que poderia ter uma conta como um parâmetro e agir sobre ela (por exemplo, resetter.reset(account) ), embora isso também seja uma prática ruim (uma analogia comum é um lojista carteira para obter dinheiro).

Em idiomas com o recurso, você pode usar mixins ou traits, no entanto, você acaba na posição em que começou, onde você pode verificar a existência desses recursos.

    
por 13.12.2017 / 16:15
fonte
0

Para este exemplo específico de " pode redefinir uma senha ", eu recomendaria usar composição sobre herança (nesse caso, herança de uma interface / contrato) . Porque, fazendo isso:

class Foo : IResetsPassword {
    //...
}

Você está imediatamente especificando (em tempo de compilação) que sua classe ' pode redefinir uma senha '. Mas, se no seu cenário, a presença da capacidade é condicional e depende de outras coisas, então você não pode mais especificar coisas em tempo de compilação. Então, sugiro fazer isso:

class Foo {

    PasswordResetter passwordResetter;

}

Agora, no tempo de execução, você pode verificar se myFoo.passwordResetter != null antes de fazer esta operação. Se você quiser separar ainda mais as coisas (e planeja adicionar muito mais recursos), você pode:

class Foo {
    //... foo stuff
}

class PasswordResetOperation {
    bool Execute(Foo foo) { ... }
}

class SendMailOperation {
    bool Execute(Foo foo) { ... }
}

//...and you follow this pattern for each new capability...

UPDATE

Depois de ler algumas outras respostas e comentários do OP, entendi que a questão não é sobre a solução de composição. Então, acho que a questão é sobre como identificar melhor as capacidades dos objetos em geral, em um cenário como o seguinte:

class BaseAccount {
    //...
}
class GuestAccount : BaseAccount {
    //...
}
class UserAccount : BaseAccount, IMyPasswordReset, IEditPosts {
    //...
}
class AdminAccount : BaseAccount, IPasswordReset, IEditPosts, ISendMail {
    //...
}

//Capabilities

interface IMyPasswordReset {
    bool ResetPassword();
}

interface IPasswordReset {
    bool ResetPassword(UserAccount userAcc);
}

interface IEditPosts {
    bool EditPost(long postId, ...);
}

interface ISendMail {
    bool SendMail(string from, string to, ...);
}

Agora, tentarei analisar todas as opções mencionadas:

OP segundo exemplo:

if (account.CanResetPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Digamos que este código esteja recebendo alguma classe de conta base (por exemplo: BaseAccount no meu exemplo); isso é ruim, já que está inserindo booleanos na classe base, poluindo-os com código que não faz sentido algum estar lá.

Primeiro exemplo do OP:

if (account is IResetsPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Para responder à pergunta, isso é mais apropriado do que a opção anterior, mas dependendo da implementação ela quebrará o princípio L de sólido, e provavelmente verificações como essa seriam espalhadas pelo código e dificultariam ainda mais a manutenção.

Resposta do CandiedOrange:

account.ResetPassword(authority);

Se esse método ResetPassword for inserido na classe BaseAccount , ele também estará poluindo a classe base com código inadequado, como no segundo exemplo do OP

Resposta do boneco de neve:

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

Esta é 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). EX .: neste exemplo de AccountManager , a verificação de permissão seria uma consulta ao DB; na pergunta do OP, as verificações são tentativas de conjurar os objetos.

Outra sugestão minha:

Use o padrão Template Method para ramificação de alto nível. A hierarquia de classes mencionada é mantida como está; criamos apenas manipuladores mais apropriados para os objetos, a fim de evitar elencos e propriedades / métodos inadequados poluindo a classe base.

//Template method
class BaseAccountOperation {

    BaseAccount account;

    void Execute() {

        //... some processing

        TryResetPassword();

        //... some processing

        TrySendMail();

        //... some processing
    }

    void TryResetPassword() {
        Print("Not allowed to reset password with this account type!");
    }

    void TrySendMail() {
        Print("Not allowed to reset password with this account type!");
    }
}

class UserAccountOperation : BaseAccountOperation {

    UserAccount userAccount;

    void TryResetPassword() {
        account.ResetPassword(...);
    }

}

class AdminAccountOperation : BaseAccountOperation {

    AdminAccount adminAccount;

    override void TryResetPassword() {
        account.ResetPassword(...);
    }

    void TrySendMail() {
        account.SendMail(...);
    }
}

Você poderia 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 keyword ou como última opção usar somente uma conversão para passar o objeto da conta para a operação (neste caso, o número de lançamentos é apenas um, no início da operação).

    
por 11.12.2017 / 19:05
fonte
0

Nenhuma das opções que você apresentou são boas OO. Se você está escrevendo instruções if em torno do tipo de um objeto, você provavelmente está fazendo OO errado (há exceções, isso não é uma). Aqui está a resposta OO simples à sua pergunta (pode não ser válida em C #):

interface IAccount {
  bool CanResetPassword();

  void ResetPassword();

  // Other Account operations as needed
}

public class Resetable : IAccount {
  public bool CanResetPassword() {
    return true;
  }

  public void ResetPassword() {
    /* RESET PASSWORD */
  }
}

public class NotResetable : IAccount {
  public bool CanResetPassword() {
    return false;
  }

  public void ResetPassword() {
    Print("Not allowed to reset password with this account type!");}
  }

Eu modifiquei este exemplo para corresponder ao que o código original estava fazendo. Com base em alguns dos comentários, parece que as pessoas estão ficando presas em saber se esse é o código específico "correto" aqui. Esse não é o objetivo deste exemplo. Toda a sobrecarga polimórfica é essencialmente para executar condicionalmente diferentes implementações de lógica com base no tipo do objeto. O que você está fazendo em ambos os exemplos é o empastamento de mão que sua linguagem lhe dá como um recurso. Em suma, você poderia 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 dizer se esta é uma boa solução para o seu sistema em particular. É simples e se funcionar para o que você está fazendo, você provavelmente nunca precisará pensar muito sobre isso novamente, a menos que alguém não consiga verificar CanResetPassword () antes de chamar ResetPassword (). Você também pode retornar um booleano ou falhar silenciosamente (não recomendado). Isso realmente depende das especificidades do design.

    
por 11.12.2017 / 21:37
fonte
0

Resposta

Minha resposta levemente opinativa à sua pergunta é:

The capabilities of an object should be identified through themselves, not by implementing an interface. A statically, strongly typed language will limit this possibility, though (by design).

Adendo:

Branching on object type is evil.

Caminhando

Vejo que você não adicionou a tag c# , mas está perguntando em um contexto geral de object-oriented .

O que você está descrevendo é um aspecto técnico de linguagens tipificadas estáticas / strongs e não específicas 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 em tempo de compilação (por meio de definição de variável, definição de valor de parâmetro / retorno de método ou uma conversão explícita). Eu fiz ambas as suas variantes no passado, nestes tipos de idiomas, e ambos parecem feios para mim nos dias de hoje.

Em linguagens OO dinamicamente tipificadas, esse problema não ocorre devido a um conceito chamado "duck typing". Em suma, isso significa que você basicamente não declara o tipo de um objeto em qualquer lugar (exceto ao criá-lo). Você envia mensagens para objetos e os objetos manipulam essas mensagens. Você não se importa se o objeto realmente tem um método com esse nome. Algumas linguagens até possuem um método genérico, pega-tudo method_missing , o que torna isso ainda mais flexível.

Então, em tal idioma (Ruby, por exemplo), seu código se torna:

account.reset_password

período.

O account redefinirá a senha, emitirá uma exceção "negada" ou emitirá uma exceção "não sei como manipular 'reset_password'".

Se você precisar se ramificar explicitamente por qualquer motivo, você ainda faria isso na capacidade, não na classe:

account.reset_password  if account.can? :reset_password

(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 digitados dinamicamente, isso é apenas uma preferência pessoal. As linguagens estaticamente tipadas também têm coisas para elas, então, por favor, não considerem essa tagarelice como um golpe contra as linguagens estaticamente tipadas ...

    
por 12.12.2017 / 16:53
fonte
0

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. Isso demonstrou que suas abstrações estão erradas, já que elas quebram o polimorfismo. Mais provavelmente do que não, quebra O princípio Open fechado , que resulta em um acoplamento mais estreito .

Sua segunda opção pode estar bem do ponto de vista da OOP, mas o casting do tipo me confunde. Se sua conta permitir que você redefina sua senha, por que você precisa de um typecasting? Portanto, supondo que sua cláusula CanResetPassword represente algum requisito de negócios fundamental que faz parte da abstração da conta, sua segunda opção é OK.

    
por 19.12.2017 / 21:35
fonte