Atribuição: isso surgiu de uma questão P.SE relacionada
Minha formação é em C / C ++, mas trabalhei bastante em Java e atualmente estou codificando C #. Por causa do meu background C, a verificação de ponteiros passados e retornados é de segunda mão, mas reconheço que isso influencia meu ponto de vista.
Vi recentemente menção do Null Object Pattern, onde a idéia é que um objeto seja sempre retornado. Caso normal retorna o objeto esperado e preenchido e o caso de erro retorna o objeto vazio em vez de um ponteiro nulo. A premissa é que a função de chamada sempre terá algum tipo de objeto para acessar e, portanto, evitará violações de memória de acesso nulo.
- Então, quais são os prós / contras de uma verificação nula versus o uso do Null Object Pattern?
Posso ver um código de chamada mais limpo com o NOP, mas também posso ver onde ele criaria falhas ocultas que, de outra forma, não seriam geradas. Prefiro que meu aplicativo falhe bastante (também conhecido como exceção) enquanto o desenvolvo do que cometer um erro silencioso escapar para a natureza.
- O padrão de objeto nulo não pode ter problemas semelhantes ao não executar uma verificação nula?
Muitos dos objetos com os quais trabalhei retêm objetos ou contêineres próprios. Parece que eu teria que ter um caso especial para garantir que todos os contêineres do objeto principal tivessem seus próprios objetos vazios. Parece que isso pode ficar feio com várias camadas de aninhamento.
fonte
Respostas:
Você não usaria um Padrão de Objeto Nulo em locais onde é retornado nulo (ou Objeto Nulo) porque houve uma falha catastrófica. Nesses lugares, continuaria retornando nulo. Em alguns casos, se não houver recuperação, você também pode travar, porque pelo menos o despejo de travamento indicará exatamente onde o problema ocorreu. Nesses casos, quando você adiciona seu próprio tratamento de erros, você ainda mata o processo (novamente, eu disse para casos em que não há recuperação), mas o tratamento de erros mascarará informações muito importantes que um despejo de memória teria fornecido.
O Padrão de Objeto Nulo é mais para locais onde há um comportamento padrão que pode ser adotado em um caso em que o objeto não é encontrado. Por exemplo, considere o seguinte:
Se você usa o NOP, você escreveria:
Observe que o comportamento desse código é "se Bob existir, quero atualizar seu endereço". Obviamente, se seu aplicativo exige que Bob esteja presente, você não deseja ter sucesso silencioso. Mas há casos em que esse tipo de comportamento seria apropriado. E nesses casos, o NOP não produz um código muito mais limpo e conciso?
Em lugares onde você realmente não pode viver sem Bob, eu teria GetUser () lançando uma exceção de aplicativo (ou seja, sem violação de acesso ou algo assim) que seria tratado em um nível mais alto e reportaria falhas gerais de operação. Nesse caso, não há necessidade de NOP, mas também não é necessário verificar explicitamente se há NULL. O IMO, essas verificações de NULL, apenas aumentam o código e diminuem a legibilidade. A verificação de NULL ainda é a escolha certa de design para algumas interfaces, mas não tanto quanto muitas pessoas pensam.
fonte
Prós
Contras
Este último "profissional" é o principal diferenciador (na minha experiência) de quando cada um deve ser aplicado. "A falha deve ser barulhenta?" Em alguns casos, você deseja que uma falha seja difícil e imediata; se algum cenário que nunca deveria acontecer de alguma forma acontecer. Se um recurso vital não foi encontrado ... etc. Em alguns casos, você concorda com um padrão sensato, pois não é realmente um erro : obter um valor de um dicionário, mas a chave está ausente, por exemplo.
Como qualquer outra decisão de design, existem vantagens e desvantagens dependendo de suas necessidades.
fonte
Não sou uma boa fonte de informações objetivas, mas subjetivamente:
Em ambientes como Java ou C #, isso não oferece o principal benefício da explicitação - a segurança da solução estar completa, pois você ainda pode receber nulo no lugar de qualquer objeto no sistema e o Java não tem meios (exceto anotações personalizadas para o Beans , etc.) para protegê-lo, exceto ifs explícitos. Porém, no sistema livre de implementações nulas, abordar a solução de problemas dessa maneira (com objetos representando casos excepcionais) oferece todos os benefícios mencionados. Portanto, tente ler sobre algo que não seja a linguagem convencional e você se verá mudando sua maneira de codificar.
Em geral, os objetos Nulo / Erro / tipos de retorno são considerados uma estratégia de tratamento de erros muito sólida e bastante matematicamente correta, enquanto as exceções que tratam a estratégia de Java ou C são apenas um passo acima da estratégia "die ASAP" - então basicamente deixam todo o ônus da instabilidade e imprevisibilidade do sistema no desenvolvedor ou mantenedor.
Também existem mônadas que podem ser consideradas superiores a essa estratégia (também superiores em complexidade de entendimento), mas essas são mais sobre os casos em que você precisa modelar aspectos externos do tipo, enquanto Null Object modela aspectos internos. Portanto, NonExistingUser provavelmente é melhor modelado como mônada talvez_existindo.
E, finalmente, não acho que exista nenhuma conexão entre o padrão Null Object e o tratamento silencioso de situações de erro. Deve ser o contrário - deve forçar você a modelar explicitamente todos os casos de erro e manipulá-lo adequadamente. Agora, é claro que você pode decidir ser desleixado (por qualquer motivo) e pular o tratamento explícito do caso de erro, mas tratá-lo genericamente - bem, essa foi sua escolha explícita.
fonte
Acho interessante notar que a viabilidade de um Objeto Nulo parece ser um pouco diferente, dependendo se o seu idioma é ou não estaticamente digitado. Ruby é uma linguagem que implementa um objeto para Nulo (ou
Nil
na terminologia da linguagem).Em vez de retornar um ponteiro para a memória não inicializada, o idioma retornará o objeto
Nil
. Isso é facilitado pelo fato de o idioma ser digitado dinamicamente. Não é garantido que uma função sempre retorne umint
. Pode retornarNil
se é isso que precisa fazer. Torna-se mais complicado e difícil fazer isso em uma linguagem estaticamente tipada, porque você naturalmente espera que nulo seja aplicável a qualquer objeto que possa ser chamado por referência. Você teria que começar a implementar versões nulas de qualquer objeto que pudesse ser nulo (ou seguir a rota Scala / Haskell e ter algum tipo de wrapper "Talvez").MrLister trouxe à tona o fato de que, em C ++, não é garantido que você tenha uma referência / ponteiro de inicialização ao criá-lo. Por ter um objeto nulo dedicado em vez de um ponteiro nulo bruto, você pode obter alguns recursos adicionais interessantes. Ruby
Nil
inclui umato_s
função (toString) que resulta em texto em branco. Isso é ótimo se você não se importa em obter um retorno nulo em uma string e deseja pular a geração de relatórios sem travar. As Classes abertas também permitem substituir manualmente a saída,Nil
se necessário.Concedido isso tudo pode ser tomado com um grão de sal. Acredito que em uma linguagem dinâmica, o objeto nulo pode ser uma grande conveniência quando usado corretamente. Infelizmente, tirar o máximo proveito disso pode exigir que você edite sua funcionalidade básica, o que pode dificultar a compreensão e a manutenção do programa para as pessoas acostumadas a trabalhar com seus formulários mais padrão.
fonte
Para alguns pontos de vista adicionais, dê uma olhada no Objective-C, onde, em vez de ter um objeto Nulo, o valor nulo funciona como um objeto. Qualquer mensagem enviada para um objeto nulo não tem efeito e retorna um valor de 0 / 0.0 / NO (falso) / NULL / nulo, qualquer que seja o tipo de valor de retorno do método. Isso é usado como um padrão muito difundido na programação do MacOS X e iOS, e geralmente os métodos são projetados para que um objeto nulo retornando 0 seja um resultado razoável.
Por exemplo, se você quiser verificar se uma sequência possui um ou mais caracteres, você pode escrever
e obtenha um resultado razoável se aString == nil, porque uma sequência inexistente não contém caracteres. As pessoas tendem a demorar um pouco para se acostumar, mas provavelmente levaria um tempo para me acostumar a verificar valores nulos em qualquer lugar em outros idiomas.
(Há também um objeto singleton da classe NSNull que se comporta bem diferente e não é muito utilizado na prática).
fonte
nil
obtém#define
NULL e se comporta como um objeto nulo. Esse é um recurso de tempo de execução enquanto em ponteiros nulos em C # / Java emite erros.Não use se você puder evitá-lo. Use uma função de fábrica retornando um tipo de opção, uma tupla ou um tipo de soma dedicado. Em outras palavras, represente um valor potencialmente padrão com um tipo diferente de um valor garantido para não ser padronizado.
Primeiro, vamos listar os desiderata e depois pensar em como podemos expressar isso em algumas linguagens (C ++, OCaml, Python)
grep
em busca de possíveis erros.Eu acho que a tensão entre o Padrão de Objeto Nulo e os ponteiros nulos vem de (5). Se pudermos detectar erros cedo o suficiente, (5) se torna discutível.
Vamos considerar esse idioma por idioma:
C ++
Na minha opinião, uma classe C ++ geralmente deve ser construtiva por padrão, pois facilita as interações com as bibliotecas e facilita o uso da classe em contêineres. Isso também simplifica a herança, pois você não precisa pensar em qual construtor de superclasse chamar.
No entanto, isso significa que você não pode ter certeza se um valor do tipo
MyClass
está no "estado padrão" ou não. Além de inserir umbool nonempty
campo ou um campo semelhante à superfície padrão em tempo de execução, o melhor que você pode fazer é produzir novas instâncias deMyClass
uma maneira que obrigue o usuário a verificá-la .Eu recomendo usar uma função de fábrica que retorne uma
std::optional<MyClass>
,std::pair<bool, MyClass>
ou uma referência de valor r para a,std::unique_ptr<MyClass>
se possível.Se você deseja que sua função de fábrica retorne algum tipo de estado de "espaço reservado" que seja diferente de um construído por padrão
MyClass
, use astd::pair
e certifique-se de documentar que sua função faz isso.Se a função de fábrica tiver um nome exclusivo, será fácil
grep
identificá-lo e procurar desleixo. No entanto, é difícilgrep
para casos em que o programador deveria ter usado a função de fábrica, mas não o fez .OCaml
Se você estiver usando um idioma como OCaml, poderá usar apenas um
option
tipo (na biblioteca padrão do OCaml) ou umeither
tipo (não na biblioteca padrão, mas fácil de criar). Ou umdefaultable
tipo (estou inventando o termodefaultable
).A,
defaultable
como mostrado acima, é melhor que um par, porque o usuário deve corresponder ao padrão para extrair o'a
e não pode simplesmente ignorar o primeiro elemento do par.O equivalente do
defaultable
tipo mostrado acima pode ser usado em C ++ usando astd::variant
com duas instâncias do mesmo tipo, mas partes dastd::variant
API não podem ser usadas se os dois tipos forem iguais. Também é um uso estranho,std::variant
já que seus construtores de tipo não têm nome.Python
Você não recebe nenhuma verificação em tempo de compilação para o Python. Mas, sendo digitado dinamicamente, geralmente não há circunstâncias em que você precise de uma instância de espaço reservado de algum tipo para aplacar o compilador.
Eu recomendaria apenas lançar uma exceção quando você fosse forçado a criar uma instância padrão.
Se isso não for aceitável, eu recomendaria criar um
DefaultedMyClass
que herdaMyClass
e retorne esse da sua função de fábrica. Isso oferece flexibilidade em termos de funcionalidade "stubbing out" em instâncias padrão, se necessário.fonte