Método único con muchos parámetros frente a muchos métodos que deben llamarse en orden


16

Tengo algunos datos en bruto que necesito hacer muchas cosas (cambiarlo, rotarlo, escalarlo a lo largo de cierto eje, rotarlo a una posición final) y no estoy seguro de cuál es la mejor manera de hacerlo para mantener la legibilidad del código. Por un lado, puedo hacer un único método con muchos parámetros (10+) para hacer lo que necesito, pero esta es una pesadilla de lectura de códigos. Por otro lado, podría hacer múltiples métodos con 1-3 parámetros cada uno, pero estos métodos deberían llamarse en un orden muy específico para obtener el resultado correcto. He leído que lo mejor para los métodos es hacer una cosa y hacerlo bien, pero parece que tener muchos métodos a los que hay que llamar para abrir un código para errores difíciles de encontrar.

¿Existe un paradigma de programación que pueda usar que minimice los errores y haga que el código sea más fácil de leer?


3
El mayor problema no es 'no llamarlos en orden', es 'no saber' que usted (o más precisamente, un futuro programador) debe llamarlos en orden. Asegúrese de que cualquier programador de mantenimiento conozca los detalles (esto dependerá en gran medida de cómo documente los requisitos, el diseño y las especificaciones). Utilice las pruebas unitarias, los comentarios y proporcione funciones auxiliares que tomen todos los parámetros y llame a los demás
mattnz

Al igual que una observación fuera de lugar, la interfaz fluida y el patrón de comando pueden ser útiles. Sin embargo, depende de usted (como propietario) y de los usuarios de su biblioteca (los clientes) decidir qué diseño es el mejor. Como otros señalan, es necesario comunicar a los usuarios que las operaciones no son conmutativas (que son sensibles al orden de ejecución), sin lo cual sus usuarios nunca sabrían cómo usarlas correctamente.
rwong

Ejemplos de operaciones no conmutativas: transformaciones de imágenes (rotación, escala y recorte), multiplicaciones de matrices, etc.
rwong

Tal vez pueda usar curry: esto haría imposible aplicar los métodos / funciones en el orden incorrecto.
Giorgio

¿En qué conjunto de métodos estás trabajando aquí? Quiero decir, creo que el estándar es pasar un objeto de transformación (como Affine Transform de Java para material 2D) que pasas a algún método que lo aplique. El contenido de la transformación es diferente según el orden en que llama a las operaciones iniciales, por diseño (por lo que es "lo llamas en el orden en que lo necesitas", no "en el orden en que lo quiero").
Clockwork-Muse

Respuestas:


24

Cuidado con el acoplamiento temporal . Sin embargo, esto no siempre es un problema.

Si debe realizar los pasos en orden, se deduce que el paso 1 produce algún objeto requerido para el paso 2 (por ejemplo, una secuencia de archivos u otra estructura de datos). Esto solo requiere que la segunda función deba llamarse después de la primera, ni siquiera es posible llamarlas en el orden incorrecto accidentalmente.

Al dividir su funcionalidad en piezas del tamaño de un bocado, cada parte es más fácil de entender y definitivamente más fácil de probar de forma aislada. Si tiene una gran función de 100 líneas y algo en los saltos intermedios, ¿cómo le dice su prueba fallida qué está mal? Si uno de sus cinco métodos de línea se rompe, su prueba unitaria fallida lo dirige inmediatamente hacia el único código que necesita atención.

Así es como debería verse el código complejo :

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;
  }
}

En cualquier momento durante el proceso de convertir datos sin procesar en un widget terminado, cada función devuelve algo requerido por el siguiente paso en el proceso. Uno no puede formar una aleación a partir de la escoria, primero debe fundirla (purificarla). Uno no puede crear un widget sin el permiso adecuado (por ejemplo, acero) como entrada.

Los detalles específicos de cada paso están contenidos en funciones individuales que se pueden probar: en lugar de probar la unidad de todo el proceso de extracción de rocas y crear widgets, pruebe cada paso específico. Ahora tiene una manera fácil de asegurarse de que si su proceso de "creación de widgets" falla, puede reducir la razón específica.

Además de los beneficios de probar y probar la corrección, escribir código de esta manera es mucho más fácil de leer. Nadie puede entender una gran lista de parámetros . Divídalo en pedazos pequeños y muestre lo que significa cada pedacito: eso es increíble .


2
Gracias, creo que esta es una buena manera de abordar el problema. Aunque aumenta la cantidad de objetos (y eso puede parecer innecesario), fuerza el orden mientras mantiene la legibilidad.
Tomsrobots

10

El argumento "debe ejecutarse en orden" es discutible ya que casi todo su código debe ejecutarse en el orden correcto. Después de todo, no puedes escribir en un archivo, luego abrirlo y luego cerrarlo, ¿verdad?

Debes concentrarte en lo que hace que tu código sea más fácil de mantener. Esto generalmente significará funciones de escritura que son pequeñas y fáciles de entender. Cada función debe tener un único propósito y no tener efectos secundarios imprevistos.


5

Crearía un » ImageProcesssor « (o cualquier nombre que se adapte a su proyecto) y un objeto de configuración ProcessConfiguration , que contiene todos los parámetros necesarios.

 ImageProcessor p = new ImageProcessor();

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

Dentro del procesador de imágenes, encapsulas todo el proceso detrás de un 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 llama a los métodos de transformación en el orden correcto shift(), rotate(). Cada método obtiene los parámetros apropiados de la ProcessConfiguration pasada .

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;

}

Solía interfaces de fluidos

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

que permite una inicialización ingeniosa (como se ve arriba).

La ventaja obvia, encapsulando los parámetros necesarios en un objeto. Sus firmas de métodos se vuelven legibles:

private void shift(Image i, ProcessConfiguration c)

Se trata de cambiar una imagen y los parámetros detallados están configurados de alguna manera .

Alternativamente, puede crear una línea de procesamiento :

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
    }

}

Una llamada a un método processImagecrearía una instancia de dicha canalización y haría transparente qué y en qué orden se realiza: desplazar , rotar

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

3

¿Has considerado usar algún tipo de curry ? Imagina que tienes una clase Processeey una clase 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
    }
}

Ahora puede reemplazar la clase Processorpor dos clases Processor1y 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
    }
}

Luego puede llamar a las operaciones en el orden correcto usando:

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

Puede aplicar este patrón varias veces si tiene más de dos parámetros. También puede agrupar los argumentos como desee, es decir, no es necesario que cada processmétodo tome exactamente un argumento.


Teníamos casi la misma idea;) La única diferencia es que su Pipeline aplica un estricto orden de procesamiento.
Thomas Junk

@ThomasJunk: Según tengo entendido, este es un requisito: "estos métodos deberían llamarse en un orden muy específico para obtener el resultado correcto". Tener un orden de ejecución estricto se parece mucho a la composición de funciones.
Giorgio

Y yo también. Pero, si el orden de procesamiento cambia, tienes que refactorizar mucho;)
Thomas Junk

@ThomasJunk: Cierto. Realmente depende de la aplicación. Si los pasos de procesamiento pueden intercambiarse muy a menudo, entonces probablemente su enfoque sea mejor.
Giorgio
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.