Padrão para uma classe que faz apenas uma coisa

24

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?
Heinzi
fonte
Eu sou conhecido por fazer coisas como 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 uma if()condição sem precisar saltar através de aros. Obviamente, você quer um idioma coletado de lixo se estiver escrevendo um código assim.
um CVn
Se é independente de idioma , por que adicionar java e c # também? :)
Steven Jeuris
Estou curioso, qual é a funcionalidade deste 'método único'? Ajudaria bastante a determinar qual é a melhor abordagem no cenário específico, mas ainda assim é uma pergunta interessante!
9186 Steven Jeuris
@ StevenJeuris: Eu me deparei com esse problema em várias situações, mas no caso atual é um método que importa dados para o banco de dados local (carrega-o de um servidor da web, faz algumas manipulações e armazena-o no banco de dados).
Heinzi
11
Se você sempre chama o método ao criar a instância, por que não torná-lo uma lógica de inicialização dentro do construtor?
precisa saber é o seguinte

Respostas:

29

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.

anon
fonte
11
Isso soa exatamente como o que estou fazendo. Obrigado, pelo menos eu tenho um nome para ele agora. :-)
Heinzi 8/11
2
Link muito relevante! Cheira como um anti-padrão para mim. Se seu método for tão longo, talvez partes dele possam ser extraídas em classes reutilizáveis ou geralmente refatoradas de uma maneira melhor? Também não ouvi falar sobre esse padrão antes, nem consigo encontrar muitos outros recursos que me fazem acreditar que geralmente não é usado tanto.
Steven Jeuris
11
@StevenJeuris Eu não acho que seja um anti-padrão. Um antipadrão seria desorganizar métodos refatorados com parâmetros de lote, em vez de armazenar esse estado comum entre esses métodos em uma variável de instância.
Alfredo Osorio
@ AlfredoOsorio: Bem, isso confunde métodos refatorados com muitos parâmetros. Eles vivem no objeto, por isso é melhor do que passar por todos eles, mas é pior do que refatorar para componentes reutilizáveis ​​que precisam apenas de um subconjunto limitado dos parâmetros cada.
Jan Hudec
13

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:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);
jgauffin
fonte
11
E, claro, se você não precisar usar o mesmo doernovamente, isso reduz a var result = new StuffDoer(initialParams).Calculate(extraParams);.
um CVn
Calcular deve ser renomeado para doStuff!
shabunc
11
Eu acrescentaria que, se você não deseja inicializar (nova) sua classe em que você a usa (provavelmente você deseja usar uma Factory), você tem a opção de criar o objeto em algum outro lugar da lógica de negócios e passar os parâmetros diretamente para o método público único que você possui. A outra opção é usar um Factory e ter um método make que obtém os parâmetros e cria um objeto com todos os parâmetros através do construtor. Do que você chama seu método público sem parâmetros de onde você quiser.
Patkos Csaba
2
Esta resposta é excessivamente simplista. Uma StringComparerclasse que você usa new 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ê usasse doerseparado de result.
9118 Steven Jeuris
11
Não é uma razão para existir, é 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étodo
jgauffin
8

De 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:

  • Como você mesmo apontou, as pessoas não esperam usar a palavra-chave 'new', quando o objeto criado não gerencia nenhum estado . Seu design reflete sua intenção. As pessoas que usam sua classe podem ficar confusas quanto ao estado que gerencia e se as chamadas subseqüentes ao método podem resultar em comportamento diferente.
  • Da perspectiva da pessoa que usa a classe, o estado interno está bem oculto, mas, ao querer fazer modificações na classe ou simplesmente entendê-la, você está tornando as coisas mais complexas. Eu já escrevi muito sobre os problemas que vejo nos métodos de divisão apenas para torná-los menores , especialmente ao mover o estado para o escopo da classe. Você está modificando a maneira como sua API deve ser usada apenas para ter funções menores! Na minha opinião, isso definitivamente está levando longe demais.

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.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Como o código muito 'local' dentro do código ForEachnã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

  • Em C #, você pode usar métodos de extensão. Portanto, opere o argumento diretamente que você passa para esse método de "uma coisa".
  • Veja se essa função realmente não pertence a outra classe.
  • Torne uma função estática em uma classe estática . Essa provavelmente é a abordagem mais apropriada, como também é refletida nas APIs gerais às quais você se referiu.
Steven Jeuris
fonte
Encontrei um nome possível para esse anti-padrão, conforme descrito em outra resposta, Poltergeists .
9189 Steven Sturur,
4

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 newpara criar seu objeto auxiliar e executar o trabalho é um detalhe de implementação que deve ser oculto para quem estiver chamando.

Scott Whitlock
fonte
Uau! Você conseguiu entender o assunto de maneira muito mais concisa do que eu. :) Obrigado. (é claro que eu vou um pouco mais longe onde podemos discordar, mas eu concordo com a premissa geral)
Steven Jeuris
2
new StuffDoer().Start(initialParams);

Há um problema com os desenvolvedores sem noção que poderiam usar uma instância compartilhada e usá-la

  1. várias vezes (ok, se a execução anterior (talvez parcial) não atrapalhar a execução posterior)
  2. de vários threads (não está bem, você disse explicitamente que ele possui estado interno).

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.

user470365
fonte
Bons pontos. Espero que você não se importe com a limpeza.
9186 Steven Jeuris
2

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 .

Poltergeists são classes com funções muito limitadas e ciclos de vida efetivos. Eles geralmente iniciam processos para outros objetos. A solução refatorada inclui uma realocação de responsabilidades para objetos de vida mais longa que eliminam os Poltergeists.

Sintomas e consequências

  • Caminhos de navegação redundantes.
  • Associações transitórias.
  • Classes sem estado.
  • Objetos e classes temporários e de curta duração.
  • Classes de operação única que existem apenas para "propagar" ou "invocar" outras classes por meio de associações temporárias.
  • Classes com nomes de operação "control-like", como start_process_alpha.

Solução Refatorada

Os caça-fantasmas resolvem os poltergeists removendo-os da hierarquia de classes. Após a remoção, no entanto, a funcionalidade que foi "fornecida" pelo poltergeist deve ser substituída. Isso é fácil com um simples ajuste para corrigir a arquitetura.

Steven Jeuris
fonte