Один метод с множеством параметров против множества методов, которые должны вызываться по порядку

16

У меня есть некоторые необработанные данные, которые мне нужно сделать много (чтобы сдвинуть, повернуть, масштабировать по определенной оси, повернуть к конечной позиции), и я не уверен, что лучший способ сделать это для поддержания читабельности кода. С одной стороны, я могу сделать один метод со многими параметрами (10+), чтобы сделать то, что мне нужно, но это кошмар чтения кода. С другой стороны, я мог бы создать несколько методов с 1-3 параметрами каждый, но эти методы нужно было бы вызывать в очень конкретном порядке, чтобы получить правильный результат. Я читал, что для методов лучше всего делать одну вещь и делать это хорошо, но кажется, что наличие множества методов, которые необходимо вызывать для того, чтобы открыть код для трудно обнаруживаемых ошибок.

Существует ли парадигма программирования, которую я мог бы использовать, чтобы минимизировать количество ошибок и сделать код легче для чтения?

tomsrobots
источник
3
Самая большая проблема не в том, чтобы «не вызывать их по порядку», а в том, чтобы «не знать», что вы (или, точнее, будущий программист) должны вызывать их по порядку. Убедитесь, что любой программист по обслуживанию знает детали (это будет в значительной степени зависеть от того, как вы документируете требования, дизайн и спецификации). Используйте юнит-тесты, комментарии и предоставьте вспомогательные функции, которые принимают все параметры и вызывают другие
mattnz
Так же, как неприметное замечание, свободный интерфейс и шаблон команд могут быть полезны. Однако, вам (как владельцу) и пользователям вашей библиотеки (клиентам) решать, какой дизайн лучше. Как отмечают другие, необходимо сообщить пользователям, что операции не являются коммутативными (что они чувствительны к порядку выполнения), без чего ваши пользователи никогда не узнают, как их правильно использовать.
Руон
Примеры некоммутативных операций: преобразования изображений (вращение, масштабирование и обрезка), умножение матриц и т. Д.
rwong
Возможно, вы можете использовать каррирование: это сделает невозможным применение методов / функций в неправильном порядке.
Джорджио
Какой набор методов вы работаете здесь? Я имею в виду, я думаю, что стандартом является передача объекта преобразования (например, аффинного преобразования Java для 2D), который вы передаете в какой-то метод, который его применяет. Содержимое преобразования различается в зависимости от порядка, в котором вы вызываете начальные операции над ним, по своему замыслу (так что это «вы называете это в том порядке, в котором вам это нужно», а не «в том порядке, в котором я хочу»).
Заводная муза

Ответы:

24

Остерегайтесь временной связи . Однако это не всегда проблема.

Если вы должны выполнить шаги по порядку, из этого следует, что шаг 1 создает некоторый объект, необходимый для шага 2 (например, поток файлов или другую структуру данных). Это само по себе требует, чтобы вторая функция вызывалась после первой, даже невозможно случайно вызвать их в неправильном порядке.

Разделив вашу функциональность на куски размером с кусочек, каждую часть легче понять и, безусловно, легче тестировать отдельно. Если у вас есть огромная функция из 100 строк и что-то посередине, как ваш проваленный тест скажет вам, что не так? Если один из ваших пятистрочных методов ломается, ваш неудачный модульный тест немедленно направляет вас к одному куску кода, который требует внимания.

Вот как должен выглядеть сложный код :

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

В любой момент в процессе преобразования необработанных данных в готовый виджет каждая функция возвращает что-то необходимое для следующего шага процесса. Из шлака нельзя образовать сплав, нужно сначала его расплавить (очистить). Нельзя создать виджет без надлежащего разрешения (например, стали) в качестве входных данных.

Конкретные детали каждого шага содержатся в отдельных функциях, которые можно протестировать: вместо модульного тестирования всего процесса добычи горных пород и создания виджетов, тестируйте каждый конкретный шаг. Теперь у вас есть простой способ убедиться, что в случае сбоя процесса создания виджета вы сможете сузить конкретную причину.

Помимо преимуществ тестирования и проверки правильности, написание кода таким способом намного проще для чтения. Никто не может понять огромный список параметров . Разбейте его на мелкие кусочки и покажите, что означает каждый маленький кусочек: его можно уловить .

Сообщество
источник
2
Спасибо, я думаю, что это хороший способ решить проблему. Хотя это увеличивает количество объектов (и это может показаться ненужным), оно навязывает порядок, сохраняя разборчивость.
tomsrobots
10

Аргумент «должен быть выполнен по порядку» является спорным, поскольку практически весь ваш код должен выполняться в правильном порядке. В конце концов, вы не можете записать в файл, затем открыть его, а затем закрыть его, не так ли?

Вы должны сконцентрироваться на том, что делает ваш код наиболее удобным для сопровождения. Обычно это означает написание небольших функций, которые легко понять. Каждая функция должна иметь единственную цель и не иметь побочных эффектов, которые могут быть непредвиденными.

Дэйв Най
источник
5

Я бы создал » ImageProcesssor « (или любое другое имя, подходящее для вашего проекта) и объект конфигурации ProcessConfiguration , который содержит все необходимые параметры.

 ImageProcessor p = new ImageProcessor();

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

Внутри процессора изображений вы инкапсулируете весь процесс за один метод 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
    }
}

Этот метод вызывает трансформационные методы в правильном порядке shift(), rotate(). Каждый метод получает соответствующие параметры из переданной ProcessConfiguration .

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;

}

Я использовал жидкие интерфейсы

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

что позволяет изящную инициализацию (как видно выше).

Очевидное преимущество, заключающее в себе необходимые параметры в одном объекте. Подписи вашего метода становятся читабельными:

private void shift(Image i, ProcessConfiguration c)

Речь идет о сдвигая в изображения и подробные параметры, так или иначе настроены .

В качестве альтернативы, вы можете создать 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
    }

}

Вызов метода для processImageсоздания экземпляра такого конвейера и делает прозрачным, что и в каком порядке выполняется: сдвиг , поворот

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

Вы рассматривали возможность использования какого-либо карри ? Представьте, что у вас есть класс Processeeи класс 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
    }
}

Теперь вы можете заменить класс Processorдвумя классами Processor1и 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
    }
}

Затем вы можете вызвать операции в правильном порядке, используя:

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

Вы можете применить этот шаблон несколько раз, если у вас более двух параметров. Вы также можете группировать аргументы так, как хотите, то есть вам не нужно, чтобы каждый processметод принимал ровно один аргумент.

Джорджио
источник
У нас была почти та же идея;) Единственное отличие состоит в том, что ваш конвейер обеспечивает строгий порядок обработки.
Томас Джанк
@ThomasJunk: Насколько я понял, это требование: «эти методы должны вызываться в очень конкретном порядке, чтобы получить правильный результат». Наличие строгого порядка выполнения очень похоже на композицию функций.
Джорджио
И я тоже. Но, если меняется порядок обработки, вам придется много рефакторинга;)
Thomas Junk
@ThomasJunk: правда. Это действительно зависит от приложения. Если этапы обработки могут меняться очень часто, то, вероятно, ваш подход лучше.
Джорджио