Estou escrevendo aulas que "devem ser usadas de uma maneira específica" (acho que todas as aulas devem ...).
Por exemplo, eu crio a fooManager
classe, 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 ThisHappened
açã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 Initiliaze
para 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!
initialize
ser 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?new
se 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.Respostas:
Nesses casos, é melhor usar o sistema de tipos do seu idioma para ajudá-lo na inicialização adequada. Como podemos impedir que um
FooManager
seja usado sem ser inicializado? Impedindo que umFooManager
seja 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
FooManager
antes de poder inicializá-lo, por exemplo, porque oFooManager
é passado como uma dependência.Não crie um
FooManager
se você não tiver um. Em vez disso, o que você pode fazer é passar um objeto que permita recuperar um totalmente construídoFooManager
, 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: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
MaybeInitializedFooManager
que tem umget()
método que retorna oFooManager
, mas lança se oFooManager
não foi totalmente inicializado. Isso funciona apenas se a inicialização for feita através do wrapper ou se houver umFooManager#isInitialized()
método.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.Isso extrai o comportamento principal da classe para o
MainImpl
.fonte
FooManager
e inUninitialisedImpl
seja melhor do que repetirif (!initialized) throw;
.FooManager
tenha muitos métodos, pode ser mais fácil do que esquecer algumas dasif (!initialized)
verificações. Mas, nesse caso, você provavelmente deve preferir terminar a aula.A maneira mais eficaz e útil de impedir que os clientes "usurpem" um objeto é tornando-o impossível.
A solução mais simples é mesclar
Initialize
com 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
UnitializedFooManager
instância, que possui umInitialize(...)
método que retornaInitializedFooManager
. Os métodos que podem ser chamados apenas no estado inicializado existem apenas emInitializedFooManager
. 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 chamarOpen
antes que oReadLine
método possa ser chamado e precisa lembrar sempre de chamarClose
(mesmo se ocorrer uma exceção), após o qual oReadLine
mé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 umRead
mé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:
Sem acoplamento temporal:
Uma solução mais pesada seria definir
File
como 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.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.
fonte
UnitializedFooManager
) não deve compartilhar um estado com as instâncias (InitializedFooManager
), comoInitialize(...)
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 amove
semântica para consumir o modelo de maneira confiável.Normalmente, eu apenas verificaria a institucionalização e lançaria (digamos) um
IllegalStateException
se 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
e assim você controla a criação e o ciclo de vida dos objetos, e seus clientes obtêm o
Component
resultado de sua inicialização. É efetivamente uma instancia lenta.fonte
IllegalStateException
.IllegalStateException
ouInvalidOperationException
do 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.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
FooManager
no Haskell, seria criminoso permitir que seus usuários construíssem um que não seja capaz de gerenciarFoo
s, 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 FooManager
estruct UninitializedFooManager
tipos 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ê.
fonte
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:
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.
fonte
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()
eb()
. O requisito é quea()
é sempre chamado antesb()
. Você pode criar uma verificação em tempo de compilação para que isso ocorra retornando um novo objetoa()
e movendob()
-o para o novo objeto em vez do original. Exemplo de implementação: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).
fonte
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);
eabstract 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.
fonte
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.
fonte
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!
fonte
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.
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.
fonte
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:
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.
fonte
A maneira como resolvi esse problema antes foi com um construtor privado e um
MakeFooManager()
método estático dentro da classe. Exemplo:Como um construtor é implementado, não há como alguém criar uma instância
FooManager
sem passarMakeFooManager()
.fonte
Existem várias abordagens:
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 .
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.
Use afirmações em suas compilações de depuração.
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:
fonte