如何使用许多if语句提高方法的可读性和长度?

Mic*_*u93 26 java design-patterns if-statement

我有195个ifs的方法。这是一个简短的版本:

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

我可以将ifs更改为switch:

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

但是195个案例仍然很长。如何提高该方法的可读性和长度?在这种情况下哪种模式最好?

Era*_*ran 44

创建一个Map<String,Double>将国名映射到其相应税率的:

Map<String,Double> taxRates = new HashMap<> ();
taxRates.put("POLAND",0.23);
...
Run Code Online (Sandbox Code Playgroud)

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @BenP。如果收到意外/不支持的输入,我认为抛出已检查的异常没有问题。我可能会使用一些自定义异常,而不是基础Exception类。 (10认同)
  • 一个`IllegalArgumentException`应该没问题,但是一个普通的`Exception`是不行的... (7认同)
  • 这似乎不是抛出已检查异常的合理位置。我知道OP的代码可以做到这一点,但我还是不想传播它。 (3认同)
  • 小巧的选择:`Map.ofEntries()`可能比重复所有`put`调用更好。较大的nitpick:这仍然是太多的代码重复,恕我直言,其他一些注释者也很正确,可以将数据分解到文件或数据库中并从中填充地图或直接查询数据源。中度:是的,别人怎么说例外。:) (2认同)

EJo*_*ica 24

将数据放入XML文件或数据库中,然后使用其填充字典。这样,您可以轻松更改数据,并将数据与应用程序逻辑分离。或者,仅在您的方法中执行SQL查询。


Ale*_*ica 14

Don't do this!

As it is right now, your calculateTax method is like a container for four actual calculateTax methods, one for each of the 3 countries, and one for the invalid case. Every other method you make along these lines will be like that. Following this pattern, you'll end up with many switches (checking for the same set of cases) within many methods, where each case contains the specifics of a case. But that's exactly polymorphism does, in a much better way!

Patterns like this are a very strong indication that you're not taking advantage of object orientation, and barring any other reasons not to, you definitely should. It's Java after all, and that's kind of the whole schtick.

Create an interface like TaxPolicy:

interface TaxPolicy {
    BigDecimal calculateTaxFor(BigDecimal saleAmount);
}
Run Code Online (Sandbox Code Playgroud)

Create a class that implements it:

class NationalSalesTaxPolicy implements TaxPolicy  {
    String countryName;
    BigDecimal salesTaxRate;

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

    BigDecimal calculateTaxFor(BigDecimal saleAmount) {
        return saleAmount.multiply(salesTaxRate);         
    }
}
Run Code Online (Sandbox Code Playgroud)

Then, create objects of this class, one per country you wish to support. We can wrap this list into a new class, NationalSalesTaxCalculator, which will be our one-stop-shop for calculating sales tax for any country:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

And we can use it like:

NationalSalesTaxCalculator calculator = new NationalSalesTaxCalculator();
BigDecimal salesTax = calculator.calculateTaxFor("AUSTRIA", new BigDecimal("100"));
System.out.println(salesTax);
Run Code Online (Sandbox Code Playgroud)

Some key benefits to notice:

  1. If you add a new country you want to support, you just have to create a new object. All methods that might need that object, automatically "do the right thing", without needing to manually find them all, in order to add in new if statements.
  2. You have room to adapt functionality as necessary. For example, where I live (Ontario, Canada), sales tax isn't charged for groceries. So I could make my own subclass of NationalSalesTaxPolicy that has more nuanced logic.
  3. There's even some more room for improvement. Notice that NationalSalesTaxCalculator.calculateTaxFor() contains some code specific to handling an unsupported country. If we add in new operations to that class, every method would need the same null check and error throw.

    Instead, that could be refactored into using the null object pattern. You implement an UnsuppoertedTaxPolicy, which is a class that implements all interface methods by throwing exceptions. Like so:

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

    You can then do

    TaxPolicy countryTaxPolicy = Optional
        .ofNullable(SUPPORTED_COUNTRIES.get(countryName))
        .orElse(UNSUPPORTED_COUNTRY);
    return countryTaxPolicy.calculateTaxFor(saleAmount);
    
    Run Code Online (Sandbox Code Playgroud)

    This "centralizes" all your exceptions into one place, which makes them easier to find (thus easier to set break points on), easier to edit (in case you ever want to migrate the exception types, or change the message), and it declutters the rest of the code, so that it only needs to worry about the happy case.

Here's a working demo: https://repl.it/@alexandermomchilov/Polymorphism-over-ifswitch

  • 问题是,这实际上并不是可扩展的。要添加“不对加拿大的杂货收取销售税”(或更糟糕的是,安大略省)之类的功能,则无论如何都需要更改界面。通常,该问题需要使用OOP,但是您在这里真正拥有的唯一一件事就是添加了许多实际上没有做任何事情的代码。您没有理由将代码更改为OO,从而节省了任何开发时间。当然,*只要有这样的要求*(例如可变营业税),这就是要走的路。在此之前,一种简单的方法效果更好。 (6认同)
  • 也不要这样做。您仍然需要对许多可能在几个月后发生变化的值进行硬编码。将数据编写为JSON / Sqlite / XML将是更好的选择。 (2认同)

Gra*_*ham 8

作为框架挑战...

如果清楚他们在做什么,为什么做以及每个案例中的代码最少,那么 195个案例就不会太长。是的,它很长,但是完全可读,因为您确切知道它在做什么。长度不一定意味着无法读取。

正如其他答案所言,这可能是代码气味,表明您没有正确使用OO。但就其本身而言,它只是很长,并非难以理解。

  • 唯一真正闻到的是重复的“金额* taxRate”。这不仅很容易解决,而且很难发现一个案件有不同的作为(几乎不可避免地会在某个时刻发生)。 (2认同)

Mik*_*kov 5

如果值是恒定的并且不应该定期更改(我对此表示怀疑)。我将使用枚举介绍一个静态元模型:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后

private BigDecimal calculateTax(String country, BigDecimal amount) throws Exception {
    return CountryList.countryTaxOf(country).multiply(amount);
}
Run Code Online (Sandbox Code Playgroud)

它可读性强,编译时安全,易于扩展,每个国家/地区都可以提供更多的元数据,样板更少。