Como avisar outros programadores sobre implementação de classe


96

Estou escrevendo aulas que "devem ser usadas de uma maneira específica" (acho que todas as aulas devem ...).

Por exemplo, eu crio a fooManagerclasse, que requer uma chamada para, digamos Initialize(string,string),. E, para levar o exemplo um pouco mais longe, a classe seria inútil se não ouvirmos sua ThisHappenedação.

O que quero dizer é que a classe que estou escrevendo exige chamadas de método. Mas ele será compilado muito bem se você não chamar esses métodos e acabar com um novo FooManager vazio. Em algum momento, ele não funcionará ou poderá falhar, dependendo da classe e do que faz. O programador que implementa minha classe obviamente olhava para dentro e percebia "Ah, eu não chamei Initialize!", E tudo ficaria bem.

Mas eu não gosto disso. O que eu idealmente desejaria é o código para NÃO compilar se o método não fosse chamado; Acho que isso simplesmente não é possível. Ou algo que seria imediatamente visível e claro.

Sinto-me incomodado com a atual abordagem que tenho aqui, que é a seguinte:

Adicione um valor booleano privado na classe e verifique em todos os lugares necessários se a classe foi inicializada; caso contrário, lançarei uma exceção dizendo "A turma não foi inicializada, você tem certeza de que está ligando .Initialize(string,string)?".

Eu estou bem com essa abordagem, mas ela leva a um monte de código que é compilado e, no final, não é necessário para o usuário final.

Além disso, às vezes é ainda mais código quando há mais métodos do que apenas um Initiliazepara chamar. Eu tento manter minhas classes com não muitos métodos / ações públicos, mas isso não está resolvendo o problema, apenas mantendo-o razoável.

O que eu estou procurando aqui é:

  • Minha abordagem está correta?
  • Existe um melhor?
  • O que vocês fazem / aconselham?
  • Estou tentando resolver um problema não? Os colegas me disseram que é do programador verificar a turma antes de tentar usá-la. Eu discordo respeitosamente, mas esse é outro assunto em que acredito.

Simplificando, estou tentando descobrir uma maneira de nunca esquecer de implementar chamadas quando essa classe for reutilizada mais tarde ou por outra pessoa.

ESCLARECIMENTOS:

Para esclarecer muitas perguntas aqui:

  • Definitivamente, não estou falando apenas da parte de Inicialização de uma classe, mas de toda a vida. Evite que os colegas chamem um método duas vezes, certificando-se de chamar X antes de Y etc. Qualquer coisa que acabe sendo obrigatória e documentada, mas que eu gostaria em código, o mais simples e pequeno possível. Eu realmente gostei da ideia do Asserts, embora tenha certeza de que precisarei misturar outras idéias, pois o Asserts nem sempre será possível.

  • Estou usando a linguagem C #! Como eu não mencionei isso ?! Estou em um ambiente Xamarin e construindo aplicativos móveis geralmente usando de 6 a 9 projetos em uma solução, incluindo projetos PCL, iOS, Android e Windows. Sou desenvolvedor há cerca de um ano e meio (escola e trabalho combinados), daí minhas declarações / perguntas às vezes ridículas. Tudo isso provavelmente é irrelevante aqui, mas muita informação nem sempre é uma coisa ruim.

  • Nem sempre posso colocar tudo o que é obrigatório no construtor, devido a restrições de plataforma e ao uso de Injeção de Dependência, ter parâmetros diferentes de Interfaces está fora de questão. Ou talvez meu conhecimento não seja suficiente, o que é altamente possível. Na maioria das vezes, não é um problema de inicialização, mas mais

como posso garantir que ele se inscreveu nesse evento?

como posso ter certeza de que ele não esqueceu de "parar o processo em algum momento"

Aqui me lembro de uma aula de busca de anúncios. Desde que a visualização em que o anúncio esteja visível seja visível, a classe buscará um novo anúncio a cada minuto. Essa classe precisa de uma visualização, quando construída, onde possa exibir o anúncio, que poderia ir em um parâmetro obviamente. Mas, uma vez que a exibição se foi, StopFetching () deve ser chamado. Caso contrário, a classe continuaria buscando anúncios para uma visualização que não existe, e isso é ruim.

Além disso, essa classe tem eventos que devem ser ouvidos, como "AdClicked", por exemplo. Tudo funciona bem se não for ouvido, mas perdemos o rastreamento das análises lá se os toques não forem registrados. No entanto, o anúncio ainda funciona, para que o usuário e o desenvolvedor não vejam a diferença e a análise tenha apenas dados incorretos. Isso precisa ser evitado, mas não tenho certeza de como o desenvolvedor pode saber que deve se registrar no evento tao. Esse é um exemplo simplificado, mas a idéia está aí: "certifique-se de que ele use a ação pública que está disponível" e nos momentos certos, é claro!


13
Garantir que as chamadas para os métodos de uma classe ocorram em uma ordem específica não é possível em geral. Esse é um dos muitos, muitos problemas que são equivalentes ao problema da parada. Embora seja possível tornar a não conformidade um erro em tempo de compilação em alguns casos, não há solução geral. Provavelmente, é por isso que poucas linguagens suportam construções que permitem diagnosticar pelo menos os casos óbvios, mesmo que a análise do fluxo de dados tenha se tornado bastante sofisticada.
Kilian Foth


5
A resposta para a outra pergunta é irrelevante, a pergunta não é a mesma, portanto, são perguntas diferentes. Se alguém procurar por essa discussão, não digitará o título da outra pergunta.
Gil Areia

6
O que está impedindo mesclar o conteúdo de inicializar no ctor? A chamada deve initializeser feita depois da criação do objeto? O ctor seria muito "arriscado" no sentido em que poderia lançar exceções e quebrar a cadeia de criação?
manchado

7
Esse problema é chamado de acoplamento temporal . Se puder, tente evitar colocar o objeto em um estado inválido lançando exceções no construtor ao encontrar entradas inválidas, dessa forma você nunca passará a chamada newse o objeto não estiver pronto para ser usado. Contar com um método de inicialização provavelmente voltará para assombrá-lo e deve ser evitado, a menos que seja absolutamente necessário, pelo menos essa é a minha experiência.
Seralize

Respostas:


161

Nesses casos, é melhor usar o sistema de tipos do seu idioma para ajudá-lo na inicialização adequada. Como podemos impedir que um FooManagerseja usado sem ser inicializado? Impedindo que um FooManagerseja criado sem as informações necessárias para inicializá-lo corretamente. Em particular, toda inicialização é de responsabilidade do construtor. Você nunca deve deixar seu construtor criar um objeto em um estado ilegal.

Mas os chamadores precisam construir um FooManagerantes de poder inicializá-lo, por exemplo, porque o FooManageré passado como uma dependência.

Não crie um FooManagerse você não tiver um. Em vez disso, o que você pode fazer é passar um objeto que permita recuperar um totalmente construído FooManager, mas apenas com as informações de inicialização. (Na linguagem da programação funcional, estou sugerindo que você use um aplicativo parcial para o construtor.) Por exemplo:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

O problema é que você precisa fornecer as informações init sempre que acessar o FooManager.

Se for necessário no seu idioma, você pode agrupar a getFooManager()operação em uma classe de fábrica ou de construtor.

Eu realmente quero fazer verificações em tempo de execução de como o initialize()método foi chamado, em vez de usar uma solução de nível de sistema do tipo.

É possível encontrar um compromisso. Criamos uma classe de wrapper MaybeInitializedFooManagerque tem um get()método que retorna o FooManager, mas lança se o FooManagernão foi totalmente inicializado. Isso funciona apenas se a inicialização for feita através do wrapper ou se houver um FooManager#isInitialized()método.

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

Não quero alterar a API da minha classe.

Nesse caso, você desejará evitar as if (!initialized) throw;condicionais em cada método. Felizmente, existe um padrão simples para resolver isso.

O objeto que você fornece aos usuários é apenas um shell vazio que delega todas as chamadas para um objeto de implementação. Por padrão, o objeto de implementação gera um erro para cada método que não foi inicializado. No entanto, o initialize()método substitui o objeto de implementação por um objeto totalmente construído.

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

Isso extrai o comportamento principal da classe para o MainImpl.


9
Gosto das duas primeiras soluções (+1), mas não acho que seu último trecho com a repetição dos métodos in FooManagere in UninitialisedImplseja melhor do que repetir if (!initialized) throw;.
Bergi 14/04

3
@ Bergi Algumas pessoas adoram aulas demais. Embora FooManagertenha muitos métodos, pode ser mais fácil do que esquecer algumas das if (!initialized)verificações. Mas, nesse caso, você provavelmente deve preferir terminar a aula.
user253751

1
Um MaybeInitializedFoo também não parece melhor do que um Foo inicializado, mas +1 por fornecer algumas opções / idéias.
Matthew James Briggs

7
+1 A fábrica é a opção correta. Você não pode melhorar o design sem alterá-lo, portanto, o IMHO, a última parte da resposta sobre a metade, não vai ajudar o OP.
Nathan Cooper

6
@ Bergi Eu achei a técnica apresentada na última seção extremamente útil (veja também a técnica de refatoração “Substituir condicionais pelo polimorfismo” e o Padrão de Estado). É fácil esquecer a verificação em um método, se usarmos um if / else, é muito mais difícil esquecer a implementação de um método exigido pela interface. Mais importante, o MainImpl corresponde exatamente a um objeto em que o método initialize é mesclado com o construtor. Isso significa que a implementação do MainImpl pode usufruir de garantias mais fortes, o que simplifica o código principal. …
amon

79

A maneira mais eficaz e útil de impedir que os clientes "usurpem" um objeto é tornando-o impossível.

A solução mais simples é mesclar Initializecom o construtor. Dessa forma, o objeto nunca estará disponível para o cliente no estado não inicializado, portanto, o erro não é possível. Caso você não possa fazer a inicialização no próprio construtor, é possível criar um método de fábrica. Por exemplo, se sua classe exigir que certos eventos sejam registrados, você poderá exigir que o ouvinte de eventos seja um parâmetro no construtor ou na assinatura do método de fábrica.

Se você precisar acessar o objeto não inicializado antes da inicialização, poderá implementar os dois estados como classes separadas, para começar com uma UnitializedFooManagerinstância, que possui um Initialize(...)método que retorna InitializedFooManager. Os métodos que podem ser chamados apenas no estado inicializado existem apenas em InitializedFooManager. Essa abordagem pode ser estendida para vários estados, se necessário.

Comparado às exceções de tempo de execução, é mais trabalhoso representar estados como classes distintas, mas também oferece garantias em tempo de compilação de que você não está chamando métodos inválidos para o estado do objeto e documenta as transições de estado mais claramente no código.

Porém, de maneira mais geral, a solução ideal é projetar suas classes e métodos sem restrições, como exigir que você chame métodos em determinados momentos e em uma determinada ordem. Nem sempre é possível, mas pode ser evitado em muitos casos, usando o padrão apropriado.

Se você tiver um acoplamento temporal complexo (vários métodos devem ser chamados em uma determinada ordem), uma solução pode ser usar a inversão de controle , para criar uma classe que chame os métodos na ordem apropriada, mas use métodos ou eventos de modelo para permitir que o cliente execute operações personalizadas em estágios apropriados no fluxo. Dessa forma, a responsabilidade de executar operações na ordem correta é transferida do cliente para a própria classe.

Um exemplo simples: você tem um File-objeto que permite ler de um arquivo. No entanto, o cliente precisa chamar Openantes que o ReadLinemétodo possa ser chamado e precisa lembrar sempre de chamar Close(mesmo se ocorrer uma exceção), após o qual o ReadLinemétodo não deve mais ser chamado. Isso é acoplamento temporal. Isso pode ser evitado com um único método que aceita um retorno de chamada ou delega como argumento. O método gerencia a abertura do arquivo, chamando o retorno de chamada e, em seguida, fechando o arquivo. Ele pode transmitir uma interface distinta com um Readmétodo para o retorno de chamada. Dessa forma, não é possível ao cliente esquecer de chamar métodos na ordem correta.

Interface com acoplamento temporal:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

Sem acoplamento temporal:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

Uma solução mais pesada seria definir Filecomo uma classe abstrata que requer que você implemente um método (protegido) que será executado no arquivo aberto. Isso é chamado de padrão de método de modelo.

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

A vantagem é a mesma: o cliente não precisa se lembrar de chamar métodos em uma determinada ordem. Simplesmente não é possível chamar métodos na ordem errada.


2
Também eu me lembro casos em que simplesmente não era possível ir a partir do construtor, mas eu não tenho um exemplo para mostrar agora
Gil Areia

2
O exemplo agora descreve um padrão de fábrica - mas a primeira frase realmente descreve o paradigma RAII . Então, qual é essa resposta que você deve recomendar?
Ext3h

11
@ Ext3h Não acho que o padrão de fábrica e o RAII sejam mutuamente exclusivos.
Pieter B

@ PieterB Não, eles não são, uma fábrica pode garantir RAII. Mas apenas com a implicação adicional de que o modelo / construtor args (chamado UnitializedFooManager) não deve compartilhar um estado com as instâncias ( InitializedFooManager), como Initialize(...)na verdade é Instantiate(...)semântico. Caso contrário, este exemplo levará a novos problemas se o mesmo modelo for usado duas vezes por um desenvolvedor. E também não é possível impedir / validar estaticamente se o idioma não suportar explicitamente a movesemântica para consumir o modelo de maneira confiável.
Ext3h

2
@ Ext3h: eu recomendo a primeira solução, pois é a mais simples. Mas se o cliente precisar acessar o objeto antes de chamar Initialize, você não poderá usá-lo, mas poderá usar a segunda solução.
precisa saber é o seguinte

39

Normalmente, eu apenas verificaria a institucionalização e lançaria (digamos) um IllegalStateExceptionse você tentar usá-lo enquanto não estiver inicializado.

No entanto, se você deseja ter segurança em tempo de compilação (e isso é louvável e preferível), por que não tratar a inicialização como um método de fábrica retornando um objeto construído e inicializado, por exemplo

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

e assim você controla a criação e o ciclo de vida dos objetos, e seus clientes obtêm o Componentresultado de sua inicialização. É efetivamente uma instancia lenta.


1
Boa resposta - 2 boas opções em uma boa resposta sucinta. Outras respostas acima apresentam ginástica de sistema de tipos que é (teoricamente) válida; mas para a maioria dos casos, essas duas opções mais simples são as opções práticas ideais.
Thomas W

1
Nota: No .NET, InvalidOperationException é promovido pela Microsoft para ser o que você chamou IllegalStateException.
miroxlav

1
Não estou votando negativamente nesta resposta, mas não é realmente uma boa resposta. Simplesmente jogando IllegalStateExceptionou InvalidOperationExceptiondo nada, um usuário nunca entenderá o que fez de errado. Essas exceções não devem se tornar um desvio para corrigir a falha de design.
displayName

Você notará que a exceção é uma opção possível e eu continuarei a elaborar minha opção preferida - tornando esse tempo de compilação seguro. Eu editei a minha resposta para enfatizar isso
Brian Agnew

14

Vou me afastar um pouco das outras respostas e discordar: é impossível responder isso sem saber em que idioma você está trabalhando. Se esse é um plano que vale a pena ou não e o tipo certo de "aviso" a ser dado seus usuários dependem inteiramente dos mecanismos que seu idioma fornece e das convenções que outros desenvolvedores desse idioma tendem a seguir.

Se você tem um FooManagerno Haskell, seria criminoso permitir que seus usuários construíssem um que não seja capaz de gerenciar Foos, porque o sistema de tipos facilita e essas são as convenções que os desenvolvedores do Haskell esperam.

Por outro lado, se você estiver escrevendo C, seus colegas de trabalho terão todo o direito de levá-lo de volta e atirar em você para definir tipos struct FooManagere struct UninitializedFooManagertipos separados que suportam operações diferentes, porque isso levaria a códigos desnecessariamente complexos, com muito pouco benefício .

Se você está escrevendo Python, não há mecanismo na linguagem que permita fazer isso.

Você provavelmente não está escrevendo Haskell, Python ou C, mas são exemplos ilustrativos em termos de quanto / quão pouco trabalho o sistema de tipos é esperado e capaz de fazer.

Siga as expectativas razoáveis ​​de um desenvolvedor no seu idioma e resista ao desejo de projetar em excesso uma solução que não tenha uma implementação idiomática natural (a menos que o erro seja tão fácil de cometer e tão difícil de entender que valha a pena ir ao extremo comprimentos para torná-lo impossível). Se você não tem experiência suficiente no seu idioma para julgar o que é razoável, siga o conselho de alguém que o conhece melhor do que você.


Estou um pouco confuso com "Se você está escrevendo Python, não há mecanismo na linguagem para permitir que você faça isso". Python tem construtores, e é bastante trivial criar métodos de fábrica. Então, talvez eu não entenda o que você quer dizer com "isso"?
AL Flanagan

3
Por "isso", quis dizer um erro em tempo de compilação, em oposição a um erro de tempo de execução.
Patrick Collins

Apenas pulando para mencionar o Python 3.5, que é a versão atual, tem tipos através de um sistema de tipos conectáveis. Definitivamente, é possível obter o erro do Python antes de executar o código apenas porque ele foi importado. Até 3,5, estava totalmente correto.
Benjamin Gruenbaum

Ah ok. Com atraso +1. Mesmo confuso, isso foi muito esclarecedor.
AL Flanagan

Eu gosto do seu estilo Patrick.
Gil Sand

4

Como você parece não querer enviar a verificação de código ao cliente (mas parece adequado fazê-lo para o programador), você pode usar as funções assert , se elas estiverem disponíveis na sua linguagem de programação.

Dessa forma, você realiza as verificações no ambiente de desenvolvimento (e qualquer teste que um colega deva chamar WILL falhará previsivelmente), mas você não enviará o código ao cliente, pois as declarações (pelo menos em Java) são compiladas seletivamente.

Portanto, uma classe Java usando isso ficaria assim:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

As asserções são uma ótima ferramenta para verificar o estado de tempo de execução do seu programa que você precisa, mas não espere que alguém cometa algum erro em um ambiente real.

Além disso, eles são bastante finos e não ocupam grandes porções da sintaxe if () ... sintaxe, eles não precisam ser capturados etc.


3

Olhando para esse problema de maneira mais geral do que as respostas atuais, que se concentraram principalmente na inicialização. Considere um objeto que terá dois métodos a()e b(). O requisito é que a()é sempre chamado antes b(). Você pode criar uma verificação em tempo de compilação para que isso ocorra retornando um novo objeto a()e movendo b()-o para o novo objeto em vez do original. Exemplo de implementação:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

Agora, é impossível chamar b () sem primeiro chamar a (), pois ele é implementado em uma interface que fica oculta até que a () seja chamada. É certo que isso é um grande esforço, então eu normalmente não usaria essa abordagem, mas há situações em que ela pode ser benéfica (especialmente se sua classe for reutilizada em muitas situações por programadores que talvez não sejam familiarizado com sua implementação ou em código onde a confiabilidade é crítica). Observe também que essa é uma generalização do padrão do construtor, conforme sugerido por muitas das respostas existentes. Funciona da mesma maneira, a única diferença real é onde os dados são armazenados (no objeto original e não no retornado) e quando devem ser usados ​​(a qualquer momento e somente durante a inicialização).


2

Quando eu implemento uma classe base que requer informações extras de inicialização ou links para outros objetos antes que se torne útil, costumo tornar essa classe abstrata, depois defino vários métodos abstratos nessa classe que são usados ​​no fluxo da classe base (como abstract Collection<Information> get_extra_information(args);e abstract Collection<OtherObjects> get_other_objects(args);que, por contrato de herança, deve ser implementado pela classe concreta, forçando o usuário dessa classe base a fornecer todas as coisas exigidas pela classe base.

Assim, quando eu implemento a classe base, eu sei imediata e explicitamente o que preciso escrever para fazer com que a classe base se comporte corretamente, pois eu só preciso implementar os métodos abstratos e é isso.

EDIT: para esclarecer, isso é quase o mesmo que fornecer argumentos para o construtor da classe base, mas as implementações do método abstrato permitem que a implementação processe os argumentos passados ​​para as chamadas do método abstrato. Ou mesmo no caso de nenhum argumento ser usado, ainda pode ser útil se o valor de retorno do método abstrato depender de um estado que você possa definir no corpo do método, o que não é possível ao passar a variável como argumento para o construtor. É verdade que você ainda pode passar um argumento que terá um comportamento dinâmico com base no mesmo princípio, se você preferir usar composição sobre herança.


2

A resposta é: sim, não e às vezes. :-)

Alguns dos problemas que você descreve são facilmente resolvidos, pelo menos em muitos casos.

A verificação em tempo de compilação é certamente preferível à verificação em tempo de execução, que é preferível a nenhuma verificação.

Tempo de execução do ER:

É pelo menos conceitualmente simples forçar as funções a serem chamadas em uma determinada ordem, etc., com verificações em tempo de execução. Basta colocar sinalizadores que digam qual função foi executada e deixar que cada função comece com algo como "se não for a pré-condição-1 ou não a pré-condição-2, e então lançar a exceção". Se as verificações ficarem complexas, você poderá enviá-las para uma função privada.

Você pode fazer algumas coisas acontecerem automaticamente. Para dar um exemplo simples, os programadores costumam falar sobre a "população preguiçosa" de um objeto. Quando a instância é criada, você define um sinalizador preenchido como falso ou alguma referência de objeto principal como nula. Então, quando uma função é chamada que precisa dos dados relevantes, ela verifica o sinalizador. Se for falso, ele preenche os dados e define o sinalizador como verdadeiro. Se é verdade, continua assumindo que os dados estão lá. Você pode fazer o mesmo com outras pré-condições. Defina um sinalizador no construtor ou como um valor padrão. Então, quando você chegar a um ponto em que alguma função de pré-condição deveria ter sido chamada, se ainda não tiver sido chamada, chame-a. Obviamente, isso só funcionará se você tiver os dados para chamá-lo nesse ponto, mas em muitos casos você poderá incluir qualquer dado necessário nos parâmetros de função do "

Tempo de compilação do ER:

Como já foi dito, se você precisar inicializar antes que um objeto possa ser usado, isso colocará a inicialização no construtor. Este é um conselho bastante típico para OOP. Se possível, faça com que o construtor crie uma instância válida e utilizável do objeto.

Vejo algumas discussões sobre o que fazer se você precisar passar referências ao objeto antes que ele seja inicializado. Não consigo pensar em um exemplo real disso, mas acho que isso poderia acontecer. As soluções foram sugeridas, mas as soluções que vi aqui e todas que consigo pensar são confusas e complicam o código. Em algum momento, você deve perguntar: vale a pena criar código feio e difícil de entender para que possamos ter a verificação em tempo de compilação em vez da verificação em tempo de execução? Ou estou criando muito trabalho para mim e para os outros com um objetivo que é bom, mas não obrigatório?

Se duas funções sempre devem ser executadas juntas, a solução simples é torná-las uma função. Por exemplo, se toda vez que você adicionar um anúncio a uma página, você também deve adicionar um manipulador de métricas, coloque o código para adicionar o manipulador de métricas na função "adicionar anúncio".

Algumas coisas seriam quase impossíveis de verificar em tempo de compilação. Como um requisito de que uma determinada função não possa ser chamada mais de uma vez. (Escrevi muitas funções como essa. Recentemente, escrevi uma função que procura arquivos em um diretório mágico, processa os que encontrar e os exclui. É claro que depois que o arquivo é excluído, não é possível executar novamente. Etc.) Não conheço nenhum idioma que tenha um recurso que permita impedir que uma função seja chamada duas vezes na mesma instância no tempo de compilação. Não sei como o compilador conseguiu descobrir definitivamente o que estava acontecendo. Tudo o que você pode fazer é realizar verificações em tempo de execução. Essa verificação do tempo de execução particularmente é óbvia, não é? msgstr "se já esteve aqui = true, em seguida, jogue a exceção.

ER impossível de aplicar

Não é incomum que uma classe exija algum tipo de limpeza ao concluir: fechar arquivos ou conexões, liberar memória, gravar resultados finais no banco de dados, etc. Não há maneira fácil de forçar isso a acontecer em qualquer momento da compilação ou tempo de execução. Eu acho que a maioria das linguagens OOP tem algum tipo de provisão para uma função "finalizador" que é chamada quando uma instância é coletada de lixo, mas eu acho que a maioria também diz que não pode garantir que isso seja executado. As linguagens OOP geralmente incluem uma função "dispor" com algum tipo de provisão para definir um escopo no uso de uma instância, uma cláusula "using" ou "with" e, quando o programa sai desse escopo, a disposição é executada. Mas isso requer que o programador use o especificador de escopo apropriado. Não força o programador a fazer o certo,

Nos casos em que é impossível forçar o programador a usar a classe corretamente, tento fazer com que o programa exploda se ele fizer errado, em vez de fornecer resultados incorretos. Exemplo simples: eu já vi muitos programas que inicializam um monte de dados com valores fictícios, para que o usuário nunca receba uma exceção de ponteiro nulo, mesmo que não consiga chamar as funções para preencher os dados corretamente. E eu sempre me pergunto o porquê. Você está se esforçando para tornar os bugs mais difíceis de encontrar. Se um programador falhar em chamar a função "carregar dados" e tentar alegremente usá-los, ele obterá uma exceção de ponteiro nulo, a exceção rapidamente mostrará onde está o problema. Mas se você o ocultar deixando os dados em branco ou em zero nesses casos, o programa poderá ser executado até a conclusão, mas produzirá resultados imprecisos. Em alguns casos, o programador pode nem perceber que houve um problema. Se ele perceber, pode ser necessário seguir uma longa trilha para descobrir onde está errado. É melhor falhar cedo do que tentar corajosamente continuar.

Em geral, com certeza, é sempre bom quando você pode impossibilitar o uso incorreto de um objeto. É bom que as funções sejam estruturadas de tal maneira que simplesmente não haja como o programador fazer chamadas inválidas ou se chamadas inválidas gerarem erros em tempo de compilação.

Mas há também um ponto em que você deve dizer: O programador deve ler a documentação da função.


1
Em relação a forçar a limpeza, há um padrão que pode ser usado no qual um recurso pode ser alocado apenas chamando um método específico (por exemplo, em uma linguagem OO típica, ele pode ter um construtor privado). Esse método aloca o recurso, chama uma função (ou um método de uma interface) que é passada a ele com uma referência ao recurso e, em seguida, destrói o recurso assim que essa função retornar. Além do término abrupto do encadeamento, esse padrão garante que o recurso seja sempre destruído. É uma abordagem comum em linguagens funcionais, mas também a vi usada em sistemas OO com bons resultados.
Jules


2

Sim, seus instintos são claros. Muitos anos atrás , escrevi artigos em revistas (mais ou menos como este lugar, mas em árvores mortas e lançadas uma vez por mês) discutindo Debugging , Abugging e Antibugging .

Se você conseguir que o compilador identifique o uso indevido, essa é a melhor maneira. Quais recursos de idioma estão disponíveis dependem do idioma e você não especificou. De qualquer forma, técnicas específicas seriam detalhadas para perguntas individuais neste site.

Mas ter um teste para detectar o uso em tempo de compilação em vez de tempo de execução é realmente o mesmo tipo de teste: você ainda precisa saber para chamar as funções apropriadas primeiro e usá-las da maneira correta.

Projetar o componente para evitar que mesmo ser um problema seja muito mais sutil, e eu gosto do Buffer é como o exemplo do Element . A Parte 4 (publicada há vinte anos! Uau!) Contém um exemplo com discussão bastante semelhante ao seu caso: Essa classe deve ser usada com cuidado, pois buffer () não pode ser chamado depois que você começa a usar read (). Em vez disso, deve ser usado apenas uma vez, imediatamente após a construção. O fato de a classe gerenciar o buffer de forma automática e invisível para o chamador evitaria o problema conceitualmente.

Você pergunta, se testes podem ser feitos em tempo de execução para garantir o uso adequado, você deve fazer isso?

Eu diria que sim . Isso poupará à sua equipe bastante trabalho de depuração algum dia, quando o código for mantido e o uso específico for perturbado. O teste pode ser configurado como um conjunto formal de restrições e invariantes que podem ser testados. Isso não significa que a sobrecarga de espaço extra para rastrear o estado e o trabalho para fazer a verificação seja sempre deixada para cada chamada. Está na classe para que possa ser chamado, e o código documenta as restrições e suposições reais.

Talvez a verificação seja feita apenas em uma compilação de depuração.

Talvez a verificação seja complexa (pense em heapcheck, por exemplo), mas esteja presente e possa ser feita de vez em quando, ou chamadas adicionadas aqui e ali quando o código estiver sendo trabalhado e algum problema surgir, ou para fazer testes específicos para verifique se não há esse problema em um programa de teste de unidade ou teste de integração vinculado à mesma classe.

Você pode decidir que a sobrecarga é trivial, afinal. Se a classe arquivar E / S, um byte de estado extra e um teste para isso não serão nada. As classes comuns do iostream verificam se o fluxo está no estado incorreto.

Seus instintos são bons. Mantem!


0

O princípio de Ocultar informações (encapsulamento) é que entidades fora da classe não devem saber mais do que o necessário para usar essa classe corretamente.

Parece que seu caso está tentando fazer com que um objeto de uma classe funcione corretamente sem informar os usuários externos o suficiente sobre a classe. Você ocultou mais informações do que deveria.

  • Peça explicitamente as informações necessárias no construtor;
  • Ou mantenha os construtores intactos e modifique os métodos para que, para chamar os métodos, você precise fornecer os dados ausentes.

Palavras de sabedoria:

* De qualquer forma, você precisa reprojetar a classe e / ou construtor e / ou métodos.

* Ao não projetar adequadamente e passar fome da classe com as informações corretas, você está arriscando sua aula para quebrar em não um, mas potencialmente vários lugares.

* Se você escreveu mal as exceções e as mensagens de erro, a classe estará divulgando ainda mais sobre si mesma.

* Finalmente, se o estrangeiro deve saber que precisa inicializar a, bec, e então chamar d (), e (), f (int), então você tem um vazamento na sua abstração.


0

Como você especificou que está usando C #, uma solução viável para sua situação é aproveitar os Roslyn Code Analyzers . Isso permitirá detectar violações imediatamente e até sugerir correções de código .

Uma maneira de implementá-lo seria decorar os métodos acoplados temporalmente com um atributo que especifique a ordem em que os métodos precisam ser chamados 1 . Quando o analisador encontra esses atributos em uma classe, valida que os métodos sejam chamados em ordem. Isso faria com que sua classe se parecesse com o seguinte:

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: Eu nunca escrevi um Roslyn Code Analyzer, portanto essa pode não ser a melhor implementação. A ideia de usar o Roslyn Code Analyzer para verificar se sua API está sendo usada corretamente é 100% correta.


-1

A maneira como resolvi esse problema antes foi com um construtor privado e um MakeFooManager()método estático dentro da classe. Exemplo:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

Como um construtor é implementado, não há como alguém criar uma instância FooManagersem passar MakeFooManager().


1
este parece repetir meramente ponto feito e explicado em uma resposta antes que foi postado várias horas antes
mosquito

1
isso tem alguma vantagem em simplesmente verificar nulo no ctor? parece que você acabou de criar um método que não faz nada além de encapsular outro método. porque?
Sara #

Além disso, por que são variáveis ​​de membro foo e bar?
22616 Patrick M

-1

Existem várias abordagens:

  1. Faça com que a classe seja fácil de usar certa e difícil de usar errada. Algumas das outras respostas se concentram exclusivamente nesse ponto, portanto, simplesmente mencionarei RAII e o padrão do construtor .

  2. Lembre-se de como usar os comentários. Se a classe for autoexplicativa, não escreva comentários. * Dessa forma, as pessoas têm maior probabilidade de ler seus comentários quando a classe realmente precisar deles. E se você tiver um desses casos raros em que uma classe é difícil de usar corretamente e precisa de comentários, inclua exemplos.

  3. Use afirmações em suas compilações de depuração.

  4. Lance exceções se o uso da classe for inválido.

* Estes são os tipos de comentários que você não deve escrever:

// gets Foo
GetFoo();
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.