Как я могу улучшить читаемость и длину метода со многими операторами if?

У меня есть метод с 195, если это. Вот более короткая версия:

private BigDecimal calculateTax(String country, BigDecimal amount) throws Exception {
    if(country.equals("POLAND")){
        return new BigDecimal(0.23).multiply(amount);
    }
    else if(country.equals("AUSTRIA")) {
        return new BigDecimal(0.20).multiply(amount);
    }
    else if(country.equals("CYPRUS")) {
        return new BigDecimal(0.19).multiply(amount);
    }
    else {
        throw new Exception("Country not supported");
    }
}

Я могу изменить, если на переключатели:

private BigDecimal calculateTax(String country, BigDecimal amount) throws Exception {
    switch (country) {
        case "POLAND":
            return new BigDecimal(0.23).multiply(amount);
        case "AUSTRIA":
            return new BigDecimal(0.20).multiply(amount);
        case "CYPRUS":
            return new BigDecimal(0.19).multiply(amount);
        default:
            throw new Exception("Country not supported");
    }
}

но 195 случаев все еще так долго. Как я мог улучшить читаемость и длину этого метода? Какой шаблон будет лучшим в этом случае?

Ответ 1

Создайте Map<String,Double> которая сопоставляет названия стран с соответствующими налоговыми ставками:

Map<String,Double> taxRates = new HashMap<> ();
taxRates.put("POLAND",0.23);
...

Используйте эту Map следующим образом:

private BigDecimal calculateTax(String country, BigDecimal amount) throws Exception {
    if (taxRates.containsKey(country)) {
        return new BigDecimal(taxRates.get(country)).multiply(amount);
    } else {
        throw new Exception("Country not supported");
    }
}

Ответ 2

Поместите данные в файл XML или базу данных, а затем используйте их для заполнения словаря. Таким образом, вы можете легко изменять данные и отделять данные от логики вашего приложения. Или просто выполните SQL-запрос в вашем методе.

Ответ 3

Не делай этого!

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

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

Создайте интерфейс как TaxPolicy:

interface TaxPolicy {
    BigDecimal calculateTaxFor(BigDecimal saleAmount);
}

Создайте класс, который его реализует:

class NationalSalesTaxPolicy implements TaxPolicy  {
    String countryName;
    BigDecimal salesTaxRate;

    // Insert constructor, getters, setters, etc. here

    BigDecimal calculateTaxFor(BigDecimal saleAmount) {
        return saleAmount.multiply(salesTaxRate);         
    }
}

Затем создайте объекты этого класса, по одному на страну, которую вы хотите поддерживать. Мы можем заключить этот список в новый класс, NationalSalesTaxCalculator, который станет нашим универсальным средством расчета налога с продаж для любой страны:

class NationalSalesTaxCalculator {
    static Map<String, NationalSalesTaxPolicy> SUPPORTED_COUNTRIES = Stream.of(
        new NationalSalesTaxPolicy("POLAND", "0.23"),
        new NationalSalesTaxPolicy("AUSTRIA", "0.20"),
        new NationalSalesTaxPolicy("CYPRUS", "0.19")
    ).collect(Collectors.toMap(NationalSalesTaxPolicy::getCountryName, c -> c));

    BigDecimal calculateTaxFor(String countryName, BigDecimal saleAmount) {
        NationalSalesTaxPolicy country = SUPPORTED_COUNTRIES.get(countryName);
        if (country == null) throw new UnsupportedOperationException("Country not supported");

        return country.calculateTaxFor(saleAmount);
    }
}

И мы можем использовать это как:

NationalSalesTaxCalculator calculator = new NationalSalesTaxCalculator();
BigDecimal salesTax = calculator.calculateTaxFor("AUSTRIA", new BigDecimal("100"));
System.out.println(salesTax);

Некоторые ключевые преимущества, на которые следует обратить внимание:

  1. Если вы добавляете новую страну, которую хотите поддерживать, вам просто нужно создать новый объект. Все методы, которые могут нуждаться в этом объекте, автоматически "делают правильные вещи", без необходимости вручную находить их все, чтобы добавить новые операторы if.
  2. У вас есть возможность адаптировать функциональность по мере необходимости. Например, где я живу (Онтарио, Канада), налог с продаж не взимается за продукты. Таким образом, я мог бы сделать свой собственный подкласс NationalSalesTaxPolicy который имеет более тонкую логику.
  3. Там еще немного места для улучшения. Обратите внимание, что NationalSalesTaxCalculator.calculateTaxFor() содержит некоторый код, специфичный для обработки неподдерживаемой страны. Если мы добавим новые операции в этот класс, каждому методу потребуются одинаковые проверки на нуль и сброс ошибок.

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

    class UnsuppoertedTaxPolicy implements TaxPolicy {
        public BigDecimal calculateTaxFor(BigDecimal saleAmount) {
            throw new UnsupportedOperationException("Country not supported");
        }
    }
    

    Вы можете тогда сделать

    TaxPolicy countryTaxPolicy = Optional
        .ofNullable(SUPPORTED_COUNTRIES.get(countryName))
        .orElse(UNSUPPORTED_COUNTRY);
    return countryTaxPolicy.calculateTaxFor(saleAmount);
    

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

Вот рабочая демонстрация: https://repl.it/@alexandermomchilov/Polymorphism-over-ifswitch

Ответ 4

Как вызов кадра...

195 случаев - это не слишком долго, если понятно, что они делают и почему, и если код внутри каждого случая минимален. Да, это долго, но это прекрасно читается, потому что вы точно знаете, что он делает. Длина не обязательно означает нечитаемость.

Как и другие ответы, конечно, это может быть запах кода, указывающий, что вы не используете OO должным образом. Но сам по себе он просто длинный, не читаемый.

Ответ 5

Если значения постоянны и не предназначены для регулярного изменения (в чем я сомневаюсь). Я бы представил статическую метамодель с использованием Enum:

public enum CountryList {

    AUSTRIA(BigDecimal.valueOf(0.20)),
    CYPRUS(BigDecimal.valueOf(0.19)),
    POLAND(BigDecimal.valueOf(0.23));

    private final BigDecimal countryTax;

    CountryList(BigDecimal countryTax) {
        this.countryTax = countryTax;
    }

    public BigDecimal getCountryTax() {
        return countryTax;
    }

    public static BigDecimal countryTaxOf(String countryName) {
        CountryList country = Arrays.stream(CountryList.values())
                .filter(c -> c.name().equalsIgnoreCase(countryName))
                .findAny()
                .orElseThrow(() -> new IllegalArgumentException("Country is not found in the dictionary: " + countryName));

        return country.getCountryTax();
    }
}

затем

private BigDecimal calculateTax(String country, BigDecimal amount) throws Exception {
    return CountryList.countryTaxOf(country).multiply(amount);
}

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

Ответ 6

РЕДАКТИРОВАТЬ: пропустил @Александр ответ; это может быть немного излишним, но он также затрагивает главное: используйте ООП.
РЕДАКТИРОВАТЬ 2: реализованы предложения @Luaan

Я, вероятно, упускаю что-то очевидное, и это может быть немного трудно реализовать на этом позднем этапе, но это выглядит для меня как идеальный случай для объектно-ориентированного программирования:

Вы создаете класс Country который содержит все, что относится к стране, например, name метод calcTax calculateTax() и еще много чего, а затем ваш вызывающий объект calculateTax() convertTotalAmount calculateTotalAmount() или любой другой) будет вызывать country.calculateTax(amount) вместо calculateTax(country, amount), и вся конструкция if/switch просто исчезла.

Кроме того, когда вы добавляете поддержку для новой страны (скажем, где-то еще одна гражданская война, и страна распадается), вы просто добавляете все для новой страны в одном месте вместо того, чтобы выискивать тысячи методов с гигантскими цепочками if() или switch()...

Ответ 7

Вы можете рассмотреть возможность создания двух массивов. Это легко реализовать.
Например, в этом случае вы можете использовать:

String[] countries = {"Poland", "India", "Mexico",....};  

И еще один массив для соответствующего значения BigDecimal:

BigDecimal[] taxrates = {0.23,0.5,0.75};    

Обратите внимание, что, поскольку оба массива параллельны, значение индекса для обоих массивов будет одинаковым для любого из выбранных. Например, индекс = 1 всегда будет указывать на Индию и 0,5.

Если вам нужно удалить или добавить элемент из массива, вы можете использовать временный массив и скопировать значения. Затем создайте новый массив и скопируйте необходимые значения. Вы также можете редактировать значения, используя индекс, например, используя идентификатор SQL. В случае динамических и радикальных изменений я предлагаю использовать ArrayList, где мы можем легко получить доступ к методам get() и indexOf ("") или remove ("").

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

Ответ 8

Пожалуйста, используйте плагины jQuery и jqMap.js, которые позволяют комбинировать отображение и перебирать данные с помощью функционального программирования.

Ответ 9

Мой текущий проект использует этот шаблон, я опубликую его здесь, чтобы помочь кому-то найти более масштабное решение для более сложной проблемы, тогда мы сможем удалить большую часть if/else из исходного кода.

ServiceInterface.java

public interface ServiceInterface {
    String handle(String input);
}

FirstServiceImpl.java

public class FirstServiceImpl implements ServiceInterface{
    @Override
    public String handle(String input) {
        // TODO Auto-generated method stub
        return "";
    }
}

SecondServiceImpl.java

public class SecondServiceImpl implements ServiceInterface{
    @Override
    public String handle(String input) {
        // TODO Auto-generated method stub
        return "";
    }
}

ServiceChooser.java

public enum ServiceChooser {
    SECOND_SERVICE(new SecondServiceImpl()) {
        @Override
        boolean isUse(Object country) {
            // TODO put logic to dertermine this service will be use or not
            return false;
        }
    },
    FIRST_SERVICE(new FirstServiceImpl()) {
        @Override
        boolean isUse(Object country) {
          // TODO put logic to dertermine this service will be use or not
          return false;
        }
    };

    private ServiceInterface service;

    public ServiceInterface getService() {
        return service;
    }

    private ServiceChooser(ServiceInterface service) {
        this.service = service;
    }

    abstract boolean isUse(Object inputCondition);
}

КАК ПОЛЬЗОВАТЬСЯ

final String determine = ""; // This will be String or an Object to help choose which Service is use
String input = ""; // This will be String or an Object to the input
List<String> multipleResult = Stream.of(ServiceChooser.values())
    .filter(item -> item.isUse(determine))
    .map(ServiceChooser::getService)
    .map(item -> item.handle(input))
    .collect(Collectors.toList());