Полностью исключить вложенность if-else

Шаблоны проектирования

1. Предпосылки

1.1 Отрицательные учебные материалы

Не знаю, сталкивался ли кто-нибудь из васгоризонтальная пирамидато жеif-elseВложенный:

if (true) {
    if (true) {
        if (true) {
            if (true) {
                if (true) {
                    if (true) {
                        
                    }
                }
            }
        }
    }
}

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

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

2.2 Опытная реконструкция

Рефакторинг некоторое время назадПравила взимания платы за услуги, до реконструкцииif-elseВложено следующим образом.

public Double commonMethod(Integer type, Double amount) {
    if (3 == type) {
        // 计算费用
        if (true) {
            // 此处省略200行代码,包含n个if-else,下同。。。
        }
        return 0.00;
    } else if (2 == type) {
        // 计算费用
        return 6.66;
    }else if (1 == type) {
        // 计算费用
        return 8.88;
    }else if (0 == type){
        return 9.99;
    }
    throw new IllegalArgumentException("please input right value");
}

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

2.3 Возвращение к первоисточнику

  • Разберем причины многоветвистости кода
  1. деловое суждение
  2. Нулевое оценочное суждение
  3. государственный приговор
  • Как с этим бороться?
  1. Когда есть несколько похожих алгоритмов, используйте режим стратегии, чтобы исключить бизнес-суждения, каждый подкласс реализует один и тот же интерфейс и обращает внимание только на свою собственную реализацию (Суть этой статьи);
  2. Постарайтесь завершить все суждения о нулевых значениях извне, и внешний интерфейс гарантирует, что внутренние переменные не будут нулевыми, тем самым уменьшая суждения о нулевых значениях (см.Как освободиться от проверки параметра if-else?);
  3. Предварительно кэшировать информацию о состоянии ветки вMapв, непосредственноgetПолучите конкретные значения, устраните ответвления (также отражено в этой статье).
  • Давайте посмотрим на упрощенный деловой звонок
CalculationUtil.getFee(type, amount)

или

serviceFeeHolder.getFee(type, amount)

Супер просто? Ниже описаны два способа реализации (пример кода приложен в конце статьи).

Во-вторых, общая часть

2.1 Сводка требований

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

2.2 Перечисление членов

Используется для поддержки типов членства.

public enum MemberEnum {

    ORDINARY_MEMBER(0, "普通会员"),
    JUNIOR_MEMBER(1, "初级会员"),
    INTERMEDIATE_MEMBER(2, "中级会员"),
    SENIOR_MEMBER(3, "高级会员"),

    ;

    int code;
    String desc;

    MemberEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }


    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

}

2.3 Определение интерфейса политики

Интерфейс содержит два метода:

  1. compute(Double amount): Абстракция каждого правила выставления счетов
  2. getType(): Получите уровень членства, поддерживаемый в перечислении.
public interface FeeService {

    /**
     * 计费规则
     * @param amount 会员的交易金额
     * @return
     */
    Double compute(Double amount);

    /**
     * 获取会员级别
     * @return
     */
    Integer getType();
}

3. Внерамочная реализация

3.1 Зависимости проекта

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

3.2 Реализация различных правил начисления платы

Здесь четыре подкласса реализуют интерфейс стратегии, среди которыхcompute()Метод реализует логику выставления счетов для каждого уровня членства,getType()Указывает уровень членства, к которому принадлежит этот класс.

  • Правила выставления счетов для обычных участников
public class OrdinaryMember implements FeeService {

    /**
     * 计算普通会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 9.99;
    }

    @Override
    public Integer getType() {
        return MemberEnum.ORDINARY_MEMBER.getCode();
    }
}
  • Правила выставления счетов для младших участников
public class JuniorMember implements FeeService {

    /**
     * 计算初级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 8.88;
    }

    @Override
    public Integer getType() {
        return MemberEnum.JUNIOR_MEMBER.getCode();
    }
}
  • Правила выставления счетов для промежуточных участников
public class IntermediateMember implements FeeService {

    /**
     * 计算中级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 6.66;
    }

    @Override
    public Integer getType() {
        return MemberEnum.INTERMEDIATE_MEMBER.getCode();
    }
}
  • Правила выставления счетов за премиум-членство
public class SeniorMember implements FeeService {

    /**
     * 计算高级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 0.01;
    }

    @Override
    public Integer getType() {
        return MemberEnum.SENIOR_MEMBER.getCode();
    }
}

3.3 Основная фабрика

Создайте фабричный классServiceFeeFactory.java, фабричный класс управляет всеми классами реализации интерфейса стратегии. Подробности смотрите в комментариях к коду.

public class ServiceFeeFactory {

    private Map<Integer, FeeService> map;

    public ServiceFeeFactory() {

        // 该工厂管理所有的策略接口实现类
        List<FeeService> feeServices = new ArrayList<>();

        feeServices.add(new OrdinaryMember());
        feeServices.add(new JuniorMember());
        feeServices.add(new IntermediateMember());
        feeServices.add(new SeniorMember());

        // 把所有策略实现的集合List转为Map
        map = new ConcurrentHashMap<>();
        for (FeeService feeService : feeServices) {
            map.put(feeService.getType(), feeService);
        }
    }

    /**
     * 静态内部类单例
     */
    public static class Holder {
        public static ServiceFeeFactory instance = new ServiceFeeFactory();
    }

    /**
     * 在构造方法的时候,初始化好 需要的 ServiceFeeFactory
     * @return
     */
    public static ServiceFeeFactory getInstance() {
        return Holder.instance;
    }

    /**
     * 根据会员的级别type 从map获取相应的策略实现类
     * @param type
     * @return
     */
    public FeeService get(Integer type) {
        return map.get(type);
    }
}

3.4 Инструменты

Создайте вызов для управления правилами выставления счетов с помощью класса инструментов и отправьте входные данные на уровне компании, которые не соответствуют правилам.IllegalArgumentException.

public class CalculationUtil {

    /**
     * 暴露给用户的的计算方法
     * @param type 会员级别标示(参见 MemberEnum)
     * @param money 当前交易金额
     * @return 该级别会员所需缴纳的费用
     * @throws IllegalArgumentException 会员级别输入错误
     */
    public static Double getFee(int type, Double money) {
        FeeService strategy = ServiceFeeFactory.getInstance().get(type);
        if (strategy == null) {
            throw new IllegalArgumentException("please input right value");
        }
        return strategy.compute(money);
    }
}

ядропройти черезMapизget()метод, в зависимости от поступающихtype, вы можете получить реализацию соответствующих правил выставления счетов типа членства, тем самым уменьшивif-elseделовое суждение.

3.5 Тестирование

public class DemoTest {

    @Test
    public void test() {
        Double fees = upMethod(1,20000.00);
        System.out.println(fees);
        // 会员级别超范围,抛 IllegalArgumentException
        Double feee = upMethod(5, 20000.00);
    }

    public Double upMethod(Integer type, Double amount) {
        // getFee()是暴露给用户的的计算方法
        return CalculationUtil.getFee(type, amount);
    }
}
  • Результаты
8.88
java.lang.IllegalArgumentException: please input right value

Четыре,Spring Bootвыполнить

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

4.1 Зависимости проекта

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

4.2 Реализация различных правил начисления платы

Эта часть отличается от предыдущей тем, что:Передать класс реализации стратегии управлению контейнером Spring

  • Правила выставления счетов для обычных участников
@Component
public class OrdinaryMember implements FeeService {

    /**
     * 计算普通会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 9.99;
    }

    @Override
    public Integer getType() {
        return MemberEnum.ORDINARY_MEMBER.getCode();
    }
}
  • Правила выставления счетов для младших участников
@Component
public class JuniorMember implements FeeService {

    /**
     * 计算初级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 8.88;
    }

    @Override
    public Integer getType() {
        return MemberEnum.JUNIOR_MEMBER.getCode();
    }
}
  • Правила выставления счетов для промежуточных участников
@Component
public class IntermediateMember implements FeeService {

    /**
     * 计算中级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 6.66;
    }

    @Override
    public Integer getType() {
        return MemberEnum.INTERMEDIATE_MEMBER.getCode();
    }
}
  • Правила выставления счетов за премиум-членство
@Component
public class SeniorMember implements FeeService {

    /**
     * 计算高级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 0.01;
    }

    @Override
    public Integer getType() {
        return MemberEnum.SENIOR_MEMBER.getCode();
    }
}

4.3 Преобразование псевдонимов

Мысль: Как программе передать идентификатор, как идентифицировать и разобрать этот идентификатор, найти соответствующий класс реализации стратегии?

Мое решение: сделать это в файле конфигурации для удобства обслуживания.

  • application.yml
alias:
  aliasMap:
    first: ordinaryMember
    second: juniorMember
    third: intermediateMember
    fourth: seniorMember
  • AliasEntity.java
@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "alias")
public class AliasEntity {

    private HashMap<String, String> aliasMap;

    public HashMap<String, String> getAliasMap() {
        return aliasMap;
    }

    public void setAliasMap(HashMap<String, String> aliasMap) {
        this.aliasMap = aliasMap;
    }

    /**
     * 根据描述获取该会员对应的别名
     * @param desc
     * @return
     */
    public String getEntity(String desc) {
        return aliasMap.get(desc);
    }
}

Конфигурация этого класса легко читается, потому что хранимаяMapизkey-valueценность,keyЕсть описание,valueУчастник всех уровнейBeanпсевдоним.

4.4 Фабрика политик

@Component
public class ServiceFeeHolder {

    /**
     * 将 Spring 中所有实现 ServiceFee 的接口类注入到这个Map中
     */
    @Resource
    private Map<String, FeeService> serviceFeeMap;

    @Resource
    private AliasEntity aliasEntity;

    /**
     * 获取该会员应当缴纳的费用
     * @param desc 会员标志
     * @param money 交易金额
     * @return
     * @throws IllegalArgumentException 会员级别输入错误
     */
    public Double getFee(String desc, Double money) {
        return getBean(desc).compute(money);
    }

    /**
     * 获取会员标志(枚举中的数字)
     * @param desc 会员标志
     * @return
     * @throws IllegalArgumentException 会员级别输入错误
     */
    public Integer getType(String desc) {
        return getBean(desc).getType();
    }

    private FeeService getBean(String type) {
        // 根据配置中的别名获取该策略的实现类
        FeeService entStrategy = serviceFeeMap.get(aliasEntity.getEntity(type));
        if (entStrategy == null) {
            // 找不到对应的策略的实现类,抛出异常
            throw new IllegalArgumentException("please input right value");
        }
        return entStrategy;
    }
}

Особенности:

  1. будетSpringвсе вServiceFee.javaКласс реализации внедряется вMap, различные стратегии через их различныеkeyПолучить его класс реализации;
  2. Не удается найти класс реализации соответствующей стратегии, броситьIllegalArgumentExceptionаномальный.

4.5 Тестирование

@SpringBootTest
@RunWith(SpringRunner.class)
public class DemoTest {

    @Resource
    ServiceFeeHolder serviceFeeHolder;

    @Test
    public void test() {
         // 计算应缴纳费用
        System.out.println(serviceFeeHolder.getFee("second", 1.333));
        // 获取会员标志
        System.out.println(serviceFeeHolder.getType("second"));
        // 会员描述错误,抛 IllegalArgumentException
        System.out.println(serviceFeeHolder.getType("zero"));
    }
}
  • Результаты
8.88
1
java.lang.IllegalArgumentException: please input right value

V. Резюме

Две схемы в основном относятся к шаблонам проектирования врежим стратегии, потому что шаблон стратегии как раз соответствует этому сценарию:

  1. Классов в системе много, и отличаются они только своим поведением.
  2. Системе необходимо динамически выбирать один из нескольких алгоритмов.

5.1 Роли в режиме стратегии

风尘博客

  • Context: Класс среды

ContextЭто называется ролью контекста, которая играет роль инкапсуляции связи между предыдущим и последующим, экранирования прямого доступа модуля высокого уровня к стратегии и алгоритму и инкапсуляции возможных изменений.ServiceFeeFactory.java.

  • Strategy: класс абстрактной стратегии

Определить интерфейс алгоритма, соответствующийFeeService.java.

  • ConcreteStrategy: конкретный класс стратегии

Интерфейс, реализующий определенную стратегию, соответствующуюOrdinaryMember.java/JuniorMember.java/IntermediateMember.java/SeniorMember.java.

5.2 Образец кода и справочные статьи

  1. Версия без рамы
  2. Выпуск Spring Boot Framework
  3. Как освободиться от проверки параметра if-else?

5.3 Технический обмен

  1. Пыль Блог
  2. Блог Пыли - Самородки
  3. Пыль Блог - Блог Парк
  4. Github
  5. публика
    风尘博客