Digamos que eu tenha um procedimento que faça coisas :
void doStuff(initalParams) {
...
}
Agora descubro que "fazer coisas" é uma operação bastante compex. O procedimento se torna grande, divido-o em vários procedimentos menores e logo percebo que ter algum tipo de estado seria útil ao fazer coisas, de modo que preciso passar menos parâmetros entre os procedimentos pequenos. Então, eu fatoro isso em sua própria classe:
class StuffDoer {
private someInternalState;
public Start(initalParams) {
...
}
// some private helper procedures here
...
}
E então eu chamo assim:
new StuffDoer().Start(initialParams);
ou assim:
new StuffDoer(initialParams).Start();
E é isso que parece errado. Ao usar a API .NET ou Java, eu nunca ligo new SomeApiClass().Start(...);
, o que me faz suspeitar que estou fazendo errado. Claro, eu poderia tornar o construtor do StuffDoer privado e adicionar um método auxiliar estático:
public static DoStuff(initalParams) {
new StuffDoer().Start(initialParams);
}
Mas então eu teria uma classe cuja interface externa consiste em apenas um método estático, que também parece estranho.
Daí a minha pergunta: existe um padrão bem estabelecido para esse tipo de classe que
- tem apenas um ponto de entrada e
- não possui um estado "reconhecível externamente", ou seja, o estado da instância é necessário apenas durante a execução desse único ponto de entrada?
bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");
quando tudo o que importava era aquela informação específica e os métodos de extensão LINQ não estão disponíveis. Funciona bem e se encaixa dentro de umaif()
condição sem precisar saltar através de aros. Obviamente, você quer um idioma coletado de lixo se estiver escrevendo um código assim.Respostas:
Há um padrão chamado Objeto do Método, no qual você fatora um método grande e único com muitas variáveis / argumentos temporários em uma classe separada. Você faz isso em vez de apenas extrair partes do método em métodos separados porque eles precisam acessar o estado local (os parâmetros e variáveis temporárias) e não pode compartilhar o estado local usando variáveis de instância porque elas seriam locais para esse método (e os métodos extraídos dele) apenas e não seriam utilizados pelo restante do objeto.
Então, em vez disso, o método se torna sua própria classe, os parâmetros e variáveis temporárias se tornam variáveis de instância dessa nova classe e, em seguida, o método é dividido em métodos menores da nova classe. A classe resultante geralmente possui apenas um único método de instância pública que executa a tarefa que a classe encapsula.
fonte
Eu diria que a classe em questão (usando um único ponto de entrada) é bem projetada. É fácil de usar e estender. Bastante sólido.
A mesma coisa para
no "externally recognizable" state
. É um bom encapsulamento.Não vejo nenhum problema com código como:
fonte
doer
novamente, isso reduz avar result = new StuffDoer(initialParams).Calculate(extraParams);
.StringComparer
classe que você usanew StringComparer().Compare( "bleh", "blah" )
também é projetada? Que tal criar um estado quando não há necessidade? Ok, o comportamento está bem encapsulado (bem, pelo menos para o usuário da classe, mais sobre isso na minha resposta ), mas isso não faz com que seja 'bom design' por padrão. Quanto ao seu exemplo 'doer-result'. Isso só faria sentido se você usassedoer
separado deresult
.one reason to change
. A diferença pode parecer sutil. Você tem que entender o que isso significa. Isso nunca vai criar um aulas métodoDe uma perspectiva dos princípios do SOLID , a resposta de jgauffin faz sentido. No entanto, você não deve esquecer os princípios gerais de design, por exemplo, ocultação de informações .
Vejo vários problemas com a abordagem fornecida:
Algumas referências relacionadas
Possivelmente, um ponto principal de argumentação está em como esticar o Princípio da Responsabilidade Única . "Se você levar isso ao extremo e criar classes que tenham um motivo para existir, poderá acabar com apenas um método por classe. Isso causaria uma grande expansão de classes até nos processos mais simples, fazendo com que o sistema fosse difícil de entender e difícil de mudar ".
Outra referência relevante relacionada a este tópico: "Divida seus programas em métodos que executam uma tarefa identificável. Mantenha todas as operações em um método no mesmo nível de abstração ". - Kent Beck Key aqui é "o mesmo nível de abstração". Isso não significa "uma coisa", como muitas vezes é interpretado. Esse nível de abstração é totalmente de acordo com o contexto para o qual você está projetando.
Então, qual é a abordagem adequada?
Sem conhecer seu caso de uso concreto, é difícil dizer. Há um cenário em que às vezes (não frequentemente) uso uma abordagem semelhante. Quando quero processar um conjunto de dados, sem querer disponibilizar essa funcionalidade para todo o escopo da classe. Eu escrevi um post sobre isso, como lambdas podem melhorar ainda mais o encapsulamento . Eu também comecei a uma pergunta sobre o tema aqui na programadores . A seguir, é apresentado um exemplo mais recente de onde eu usei essa técnica.
Como o código muito 'local' dentro do código
ForEach
não é reutilizado em nenhum outro lugar, posso simplesmente mantê-lo no local exato em que é relevante. Esboçar o código de tal maneira que o código que depende um do outro seja fortemente agrupado o torna mais legível na minha opinião.Alternativas possíveis
fonte
Eu diria que é muito bom, mas que sua API ainda deve ser um método estático em uma classe estática, já que é isso que o usuário espera. O fato de o método estático usar
new
para criar seu objeto auxiliar e executar o trabalho é um detalhe de implementação que deve ser oculto para quem estiver chamando.fonte
Há um problema com os desenvolvedores sem noção que poderiam usar uma instância compartilhada e usá-la
Portanto, isso precisa de documentação explícita de que não é seguro para threads e se é leve (criando rápido, não vincula recursos externos nem muita memória), não há problema em instanciar rapidamente.
Escondê-lo em um método estático ajuda com isso, porque a reutilização da instância nunca acontece.
Se houver alguma inicialização cara, pode (nem sempre) ser vantajoso preparar a inicialização uma vez e usá-la várias vezes, criando outra classe que contenha apenas estado e a torne clonável. A inicialização criaria um estado no qual seria armazenado
doer
.start()
clonaria e passaria para métodos internos.Isso também permitiria outras coisas, como estado persistente de execução parcial. (Se forem necessários fatores longos e externos, por exemplo, falha no fornecimento de eletricidade, poderá interromper a execução.) Mas essas coisas extravagantes adicionais geralmente não são necessárias, portanto não valem a pena.
fonte
Além da minha outra resposta mais elaborada e personalizada , sinto que a observação a seguir merece uma resposta separada.
Há indicações de que você pode estar seguindo o anti-padrão Poltergeists .
fonte
Existem vários padrões no catálogo P de EAA de Martin Fowler ;
fonte