Método único com muitos parâmetros versus muitos métodos que devem ser chamados em ordem

16

Eu tenho alguns dados brutos que preciso fazer muitas coisas para (mudar, girar, escalar ao longo de determinado eixo, girar para uma posição final) e não tenho certeza de qual a melhor maneira de fazer isso para manter a legibilidade do código. Por um lado, posso criar um método único com muitos parâmetros (10 ou mais) para fazer o que preciso, mas esse é um pesadelo de leitura de código. Por outro lado, eu poderia criar vários métodos com 1 a 3 parâmetros cada, mas esses métodos precisariam ser chamados em uma ordem muito específica para obter o resultado correto. Eu li que é melhor que os métodos façam uma coisa e façam bem, mas parece que existem muitos métodos que precisam ser chamados para abrir o código para erros difíceis de encontrar.

Existe um paradigma de programação que eu poderia usar para minimizar erros e facilitar a leitura do código?

tomsrobots
fonte
3
O maior problema não é 'não chamá-los em ordem', é 'não saber' que você (ou mais precisamente, um futuro programador) deve chamá-los em ordem. Verifique se qualquer programador de manutenção conhece os detalhes (isso dependerá em grande parte de como você documenta os requisitos, o design e as especificações). Testes de uso da unidade, comentários e fornecer auxiliares funções que levam todos os parâmetros e chamar os outros
mattnz
Assim como uma observação imediata, interface fluente e padrão de comando podem ser úteis. No entanto, depende de você (como proprietário) e dos usuários da sua biblioteca (os clientes) decidir qual design é o melhor. Como outros apontam, é necessário comunicar aos usuários que as operações não são comutativas (que são sensíveis à ordem de execução), sem as quais seus usuários nunca descobririam como usá-las corretamente.
Rwong 01/03
Exemplos de operações não conmutativos: transformações da imagem (rotação, escamação e corte), produto de matrizes, etc.
rwong
Talvez você possa usar currying: isso tornaria impossível aplicar os métodos / funções na ordem errada.
Giorgio
Em que conjunto de métodos você está trabalhando aqui? Quero dizer, acho que o padrão é passar um objeto de transformação (como o Affine Transform para 2D de Java ) que você passa para algum método que o aplica. O conteúdo da transformação é diferente, dependendo da ordem em que você chama as operações iniciais, por design (portanto, é "você chama na ordem em que precisa", não "na ordem em que eu quero").
Clockwork-Muse

Respostas:

24

Cuidado com o acoplamento temporal . No entanto, isso nem sempre é um problema.

Se você deve executar as etapas em ordem, segue-se que a etapa 1 produz algum objeto necessário para a etapa 2 (por exemplo, um fluxo de arquivos ou outra estrutura de dados). Isso por si só exige que a segunda função deve ser chamado após a primeira, nem sequer é possível chamá-los na ordem errada acidentalmente.

Ao dividir sua funcionalidade em pedaços pequenos, cada parte fica mais fácil de entender e definitivamente mais fácil de testar isoladamente. Se você tem uma enorme função de 100 linhas e algo no meio do intervalo, como o seu teste falho diz o que está errado? Se um dos seus métodos de cinco linhas for interrompido, seu teste de unidade com falha o direciona imediatamente para o único código que precisa de atenção.

É assim que o código complexo deve parecer:

public List<Widget> process(File file) throws IOException {
  try (BufferedReader in = new BufferedReader(new FileReader(file))) {
    List<Widget> widgets = new LinkedList<>();
    String line;
    while ((line = in.readLine()) != null) {
      if (isApplicable(line)) { // Filter blank lines, comments, etc.
        Ore o = preprocess(line);
        Ingot i = smelt(o);
        Alloy a = combine(i, new Nonmetal('C'));
        Widget w = smith(a);
        widgets.add(w);
      }
    }
    return widgets;
  }
}

A qualquer momento durante o processo de conversão de dados brutos em um widget finalizado, cada função retorna algo exigido pela próxima etapa do processo. Não se pode formar uma liga a partir de escória, deve-se fundi-la (purificá-la) primeiro. Não se pode criar um widget sem a permissão adequada (por exemplo, aço) como entrada.

Os detalhes específicos de cada etapa estão contidos em funções individuais que podem ser testadas: em vez de testar a unidade todo o processo de mineração de rochas e criar widgets, teste cada etapa específica. Agora você tem uma maneira fácil de garantir que, se o processo "criar widget" falhar, você poderá restringir o motivo específico.

Além dos benefícios de testar e provar a correção, escrever código dessa maneira é muito mais fácil de ler. Ninguém pode entender uma lista enorme de parâmetros . Divida-o em pedaços pequenos e mostre o que cada pedacinho significa: isso é ótimo .

Comunidade
fonte
2
Obrigado, acho que é uma boa maneira de resolver o problema. Embora aumente o número de objetos (e isso pode parecer desnecessário), força a ordem, mantendo a legibilidade.
tomsrobots #
10

O argumento "deve ser executado em ordem" é discutível, já que praticamente todo o seu código deve ser executado na ordem correta. Afinal, você não pode gravar em um arquivo, abri-lo e fechá-lo, pode?

Você deve se concentrar no que torna seu código o mais sustentável. Isso geralmente significa funções de escrita pequenas e fáceis de entender. Cada função deve ter um único objetivo e não ter efeitos colaterais imprevistos.

Dave Nay
fonte
5

Eu criaria um » ImageProcesssor « (ou qualquer outro nome adequado ao seu projeto) e um objeto de configuração ProcessConfiguration , que contém todos os parâmetros necessários.

 ImageProcessor p = new ImageProcessor();

 ProcessConfiguration config = new processConfiguration().setTranslateX(100)
                                                         .setTranslateY(100)
                                                         .setRotationAngle(45);
 p.process(image, config);

Dentro do processador de imagem, você encapsula todo o processo por trás de um método process()

public class ImageProcessor {

    public Image process(Image i, ProcessConfiguration c){
        Image processedImage=i.getCopy();
        shift(processedImage, c);
        rotate(processedImage, c);
        return processedImage;
    }

    private void rotate(Image i, ProcessConfiguration c) {
        //rotate
    }

    private void shift(Image i, ProcessConfiguration c) {
        //shift
    }
}

Este método chama os métodos de transformação na ordem correta shift(), rotate(). Cada método obtém parâmetros apropriados do ProcessConfiguration passado .

public class ProcessConfiguration {

    private int translateX;

    private int rotationAngle;

    public int getRotationAngle() {
        return rotationAngle;
    }

    public ProcessConfiguration setRotationAngle(int rotationAngle){
        this.rotationAngle=rotationAngle;
        return this;
    }

    public int getTranslateY() {
        return translateY;
    }

    public ProcessConfiguration setTranslateY(int translateY) {
        this.translateY = translateY;
        return this;
    }

    public int getTranslateX() {
        return translateX;
    }

    public ProcessConfiguration setTranslateX(int translateX) {
        this.translateX = translateX;
        return this;
    }

    private int translateY;

}

Eu usei interfaces fluidas

public ProcessConfiguration setRotationAngle(int rotationAngle){
    this.rotationAngle=rotationAngle;
    return this;
}

que permite a inicialização bacana (como visto acima).

A vantagem óbvia, encapsulando os parâmetros necessários em um objeto. Suas assinaturas de método ficam legíveis:

private void shift(Image i, ProcessConfiguration c)

Trata-se de mudar uma imagem e os parâmetros detalhados são de alguma forma configurados .

Como alternativa, você pode criar um ProcessingPipeline :

public class ProcessingPipeLine {

    Image i;

    public ProcessingPipeLine(Image i){
        this.i=i;
    };

    public ProcessingPipeLine shift(Coordinates c){
        shiftImage(c);
        return this;
    }

    public ProcessingPipeLine rotate(int a){
        rotateImage(a);
        return this;
    }

    public Image getResultingImage(){
        return i;
    }

    private void rotateImage(int angle) {
        //shift
    }

    private void shiftImage(Coordinates c) {
        //shift
    }

}

Uma chamada de método para um método processImageinstanciaria esse pipeline e tornaria transparente o que e em que ordem é feita: shift , rotate

public Image processImage(Image i, ProcessConfiguration c){
    Image processedImage=i.getCopy();
    processedImage=new ProcessingPipeLine(processedImage)
            .shift(c.getCoordinates())
            .rotate(c.getRotationAngle())
            .getResultingImage();
    return processedImage;
}
Thomas Junk
fonte
3

Você já pensou em usar algum tipo de curry ? Imagine que você tem uma classe Processeee uma classe Processor:

class Processor
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T1 a1, T2 a2)
    {
        // Process using a1
        // then process using a2
    }
}

Agora você pode substituir a classe Processorpor duas classes Processor1e Processor2:

class Processor1
{
    private final Processee _processee;

    public Processor1(Processee p)
    {
        _processee = p;
    }

    public Processor2 process(T1 a1)
    {
        // Process using argument a1

        return new Processor2(_processee);
    }
}

class Processor2
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T2 a2)
    {
        // Process using argument a2
    }
}

Você pode chamar as operações na ordem correta usando:

new Processor1(processee).process(a1).process(a2);

Você pode aplicar esse padrão várias vezes se tiver mais de dois parâmetros. Você também pode agrupar os argumentos como quiser, ou seja, não é necessário que cada processmétodo use exatamente um argumento.

Giorgio
fonte
Tivemos quase a mesma idéia;) A única diferença é que o seu Pipeline impõe uma ordem de processamento rigorosa.
Thomas Junk
@ThomasJunk: Até onde eu entendi, este é um requisito: "esses métodos precisariam ser chamados em uma ordem muito específica para obter o resultado correto". Ter uma ordem de execução estrita parece muito com a composição da função.
Giorgio
E assim o fez I. Mas, se o processamento de alterações de ordem, você tem que fazer um monte de refatoração;)
Thomas Junk
@ThomasJunk: Verdade. Realmente depende da aplicação. Se as etapas de processamento puderem ser trocadas com muita frequência, provavelmente sua abordagem é melhor.
Giorgio