предисловие
Недавно я занимался рефакторингом кода и обнаружил много неприятных запахов в коде. Больше нечего сказать, сегодня я в основном говорю о том, как рефакторить эти вонючие и длинные if...else.
Прежде чем представить более элегантное программирование, давайте рассмотрим плохой код if...else.
Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение
1. Вонючий и длинный, если...иначе
Без лишних слов давайте взглянем на код ниже.
public interface IPay {
void pay();
}
@Service
public class AliaPay implements IPay {
@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}
@Service
public class WeixinPay implements IPay {
@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}
@Service
public class JingDongPay implements IPay {
@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}
@Service
public class PayService {
@Autowired
private AliaPay aliaPay;
@Autowired
private WeixinPay weixinPay;
@Autowired
private JingDongPay jingDongPay;
public void toPay(String code) {
if ("alia".equals(code)) {
aliaPay.pay();
} elseif ("weixin".equals(code)) {
weixinPay.pay();
} elseif ("jingdong".equals(code)) {
jingDongPay.pay();
} else {
System.out.println("找不到支付方式");
}
}
}
Метод toPay класса PayService предназначен в основном для инициации платежа.В соответствии с разными кодами для оплаты принято вызывать метод pay разных классов платежей (таких как: aliaPay).
Что не так с этим кодом? Может быть, так делают некоторые люди.
Только представьте, если способов оплаты становится все больше и больше, таких как оплата Baidu, оплата Meituan, оплата UnionPay и т. д., вам нужно изменить код метода toPay и добавить новый еще... если судить, если есть слишком много суждений, Приведет ли это ко все большей и большей логике?
Очевидно, что это нарушает шесть принципов шаблонов проектирования:开闭原则
и单一职责原则
.
Принцип открыт-закрыт: открыт для расширения, закрыт для модификации. То есть для добавления новых функций нужно как можно меньше менять существующий код.
Принцип единой ответственности: как следует из названия, логика должна быть максимально простой, не слишком сложной и легкой для повторного использования.
Итак, каково решение этой проблемы?
2. Уловка, чтобы исключить if...else
1. Используйте аннотации
Причина, по которой код используется для определения того, какой класс оплаты использовать, заключается в том, что между кодом и классом оплаты нет связывающей связи.Если связывающая связь существует, нет необходимости судить.
Давайте сначала определим аннотацию.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PayCode {
String value();
String name();
}
Добавьте эту аннотацию ко всем классам платежей
@PayCode(value = "alia", name = "支付宝支付")
@Service
public class AliaPay implements IPay {
@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}
@PayCode(value = "weixin", name = "微信支付")
@Service
public class WeixinPay implements IPay {
@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}
@PayCode(value = "jingdong", name = "京东支付")
@Service
public class JingDongPay implements IPay {
@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}
Затем добавьте самый важный класс:
@Service
public class PayService2 implements ApplicationListener<ContextRefreshedEvent> {
private static Map<String, IPay> payMap = null;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PayCode.class);
if (beansWithAnnotation != null) {
payMap = new HashMap<>();
beansWithAnnotation.forEach((key, value) ->{
String bizType = value.getClass().getAnnotation(PayCode.class).value();
payMap.put(bizType, (IPay) value);
});
}
}
public void pay(String code) {
payMap.get(code).pay();
}
}
Класс PayService2 реализуетApplicationListener
интерфейс, так что вonApplicationEvent
метод, вы можете получитьApplicationContext
пример. Затем мы получаем класс с аннотацией PayCode и помещаем его в карту.Ключом в карте является значение, определенное в аннотации PayCode, которое согласуется с параметром кода, а значение является экземпляром класса оплаты.
Таким образом, экземпляр платежного класса можно каждый раз получать напрямую через код, без суждения if...else. Если вы хотите добавить новый способ оплаты, просто отметьте аннотацию PayCode в классе оплаты, чтобы определить новый код.
Примечание: Код таким образом может не иметь никакого делового значения, может быть чистым числом, если он не повторяется.
2. Название динамического сшивания
Этот метод в основном предназначен для сценариев, в которых код имеет значение для бизнеса.
@Service
public class PayService3 implements ApplicationContextAware {
private ApplicationContext applicationContext;
private static final String SUFFIX = "Pay";
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void toPay(String payCode) {
((IPay) applicationContext.getBean(getBeanName(payCode))).pay();
}
public String getBeanName(String payCode) {
return payCode + SUFFIX;
}
}
Мы видим, что имя bean-компонента класса оплаты объединено кодом и суффиксом, например: aliaPay, weixinPay и jingDongPay. Это требует особого внимания при именовании класса платежа, и предыдущий абзац должен соответствовать коду. Экземпляр вызываемого платежного класса получается непосредственно из экземпляра ApplicationContext, по умолчанию бин является синглтоном и помещается в карту в памяти, поэтому проблем с производительностью не будет.
В частности, этим методом достигаетсяApplicationContextAware
интерфейс с вышеуказаннымApplicationListener
Интерфейс другой, я хочу сказать всем, чтобы получитьApplicationContext
Существует более одного способа экземпляра.
3. Оценка шаблонного метода
Конечно, в дополнение к двум методам, описанным выше, реализация исходного кода Spring также подсказывает нам другой способ решения проблемы if...else.
Давайте сначала взглянем на исходный код Spring AOP, взглянемDefaultAdvisorAdapterRegistry
изwrap
метод
public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
if (adviceObject instanceof Advisor) {
return (Advisor) adviceObject;
}
if (!(adviceObject instanceof Advice)) {
thrownew UnknownAdviceTypeException(adviceObject);
}
Advice advice = (Advice) adviceObject;
if (advice instanceof MethodInterceptor) {
returnnew DefaultPointcutAdvisor(advice);
}
for (AdvisorAdapter adapter : this.adapters) {
if (adapter.supportsAdvice(advice)) {
returnnew DefaultPointcutAdvisor(advice);
}
}
thrownew UnknownAdviceTypeException(advice);
}
Сосредоточьтесь на методе supportAdvice, существует три класса, которые реализуют этот метод. Возьмем случайный класс и посмотрим
class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable {
@Override
public boolean supportsAdvice(Advice advice) {
return (advice instanceof AfterReturningAdvice);
}
@Override
public MethodInterceptor getInterceptor(Advisor advisor) {
AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice();
returnnew AfterReturningAdviceInterceptor(advice);
}
}
такого родаsupportsAdvice
Метод очень простой, просто оценитеadvice
ТипAfterReturningAdvice
.
Мы видим, что здесь должно быть поучительно.
На самом деле, мы можем сделать это, определить интерфейс или абстрактный класс, который имеетsupport
Метод определяет, может ли код, переданный параметром, быть обработан сам по себе, и если он может быть обработан, то используется платежная логика.
public interface IPay {
boolean support(String code);
void pay();
}
@Service
public class AliaPay implements IPay {
@Override
public boolean support(String code) {
return"alia".equals(code);
}
@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}
@Service
public class WeixinPay implements IPay {
@Override
public boolean support(String code) {
return"weixin".equals(code);
}
@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}
@Service
public class JingDongPay implements IPay {
@Override
public boolean support(String code) {
return"jingdong".equals(code);
}
@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}
Каждый класс оплаты имеет одинsupport
метод, чтобы определить, равен ли переданный код коду, определенному вами.
@Service
public class PayService4 implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private List<IPay> payList = null;
@Override
public void afterPropertiesSet() throws Exception {
if (payList == null) {
payList = new ArrayList<>();
Map<String, IPay> beansOfType = applicationContext.getBeansOfType(IPay.class);
beansOfType.forEach((key, value) -> payList.add(value));
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void toPay(String code) {
for (IPay iPay : payList) {
if (iPay.support(code)) {
iPay.pay();
}
}
}
}
В этом коде экземпляр класса оплаты, который реализует интерфейс IPay, инициализируется в коллекции списков и возвращается в цикл по коллекции списков при вызове интерфейса оплаты. pay текущего экземпляра класса оплаты.метод.
4. Стратегия + заводской шаблон
Этот подход также используется в сценариях, где код имеет значение для бизнеса.
Шаблон стратегии определяет набор алгоритмов, инкапсулирует их один за другим и делает взаимозаменяемыми. Фабричный шаблон используется для инкапсуляции и управления созданием объектов и является порождающим шаблоном.
Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение
public interface IPay {
void pay();
}
@Service
public class AliaPay implements IPay {
@PostConstruct
public void init() {
PayStrategyFactory.register("aliaPay", this);
}
@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}
@Service
public class WeixinPay implements IPay {
@PostConstruct
public void init() {
PayStrategyFactory.register("weixinPay", this);
}
@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}
@Service
public class JingDongPay implements IPay {
@PostConstruct
public void init() {
PayStrategyFactory.register("jingDongPay", this);
}
@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}
public class PayStrategyFactory {
private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();
public static void register(String code, IPay iPay) {
if (null != code && !"".equals(code)) {
PAY_REGISTERS.put(code, iPay);
}
}
public static IPay get(String code) {
return PAY_REGISTERS.get(code);
}
}
@Service
public class PayService3 {
public void toPay(String code) {
PayStrategyFactory.get(code).pay();
}
}
Ключом к этому коду является класс PayStrategyFactory, представляющий собой фабрику стратегий, которая определяет глобальную карту, регистрирует текущий экземпляр на карте во всех классах реализации IPay, а затем получает его из карты через класс PayStrategyFactory в вызывающем месте. в соответствии с кодом Достаточно экземпляра платежного класса.
5. Модель цепочки ответственности
Этот метод очень полезен для устранения if...else при рефакторинге кода.
Шаблон цепочки ответственности: объедините запрошенные объекты обработки, как длинную цепочку, чтобы сформировать цепочку объектов. Запрос не знает, какой объект выполняет запрос, что реализует развязку между запросом и объектом обработки.
общийfilter
,spring aop
Это использование модели цепочки ответственности. Здесь я немного улучшил ее. Конкретный код выглядит следующим образом:
public abstract class PayHandler {
@Getter
@Setter
protected PayHandler next;
public abstract void pay(String pay);
}
@Service
public class AliaPayHandler extends PayHandler {
@Override
public void pay(String code) {
if ("alia".equals(code)) {
System.out.println("===发起支付宝支付===");
} else {
getNext().pay(code);
}
}
}
@Service
public class WeixinPayHandler extends PayHandler {
@Override
public void pay(String code) {
if ("weixin".equals(code)) {
System.out.println("===发起微信支付===");
} else {
getNext().pay(code);
}
}
}
@Service
public class JingDongPayHandler extends PayHandler {
@Override
public void pay(String code) {
if ("jingdong".equals(code)) {
System.out.println("===发起京东支付===");
} else {
getNext().pay(code);
}
}
}
@Service
public class PayHandlerChain implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private PayHandler header;
public void handlePay(String code) {
header.pay(code);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, PayHandler> beansOfTypeMap = applicationContext.getBeansOfType(PayHandler.class);
if (beansOfTypeMap == null || beansOfTypeMap.size() == 0) {
return;
}
List<PayHandler> handlers = beansOfTypeMap.values().stream().collect(Collectors.toList());
for (int i = 0; i < handlers.size(); i++) {
PayHandler payHandler = handlers.get(i);
if (i != handlers.size() - 1) {
payHandler.setNext(handlers.get(i + 1));
}
}
header = handlers.get(0);
}
}
Ключом к этому коду является то, что каждый подкласс PayHandler определяет следующий подкласс PayHandler, который необходимо выполнить, формируя вызов цепочки, а структура цепочки собирается через PayHandlerChain.
6. Другие способы устранения if...else
Конечно, существует множество сценариев, использующих суждение "если...иначе" при реальной разработке проекта, и приведенные выше — лишь некоторые из них. Ниже перечислены другие распространенные сценарии.
1. Вернуть разные строки по разным числам
public String getMessage(int code) {
if (code == 1) {
return "成功";
} else if (code == -1) {
return "失败";
} else if (code == -2) {
return "网络超时";
} else if (code == -3) {
return "参数错误";
}
throw new RuntimeException("code错误");
}
На самом деле такого рода суждения не нужны, это можно сделать с помощью перечисления.
public enum MessageEnum {
SUCCESS(1, "成功"),
FAIL(-1, "失败"),
TIME_OUT(-2, "网络超时"),
PARAM_ERROR(-3, "参数错误");
private int code;
private String message;
MessageEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
public static MessageEnum getMessageEnum(int code) {
return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);
}
}
Затем немного скорректируйте метод вызова
public String getMessage(int code) {
MessageEnum messageEnum = MessageEnum.getMessageEnum(code);
return messageEnum.getMessage();
}
Идеально.
2. Суждение в сетах
Метод getMessageEnum в приведенном выше перечислении MessageEnum может быть написан так, если вы не используете синтаксис java8
public static MessageEnum getMessageEnum(int code) {
for (MessageEnum messageEnum : MessageEnum.values()) {
if (code == messageEnum.code) {
return messageEnum;
}
}
return null;
}
Для фильтрации данных в коллекциях или поиска методов в java8 есть более простой способ устранения суждений if...else.
public static MessageEnum getMessageEnum(int code) {
return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);
}
3. Простое суждение
На самом деле, некоторые простые if...else совершенно не нужны для записи и могут быть заменены тернарными операторами, такими как этот случай:
public String getMessage2(int code) {
if(code == 1) {
return "成功";
}
return "失败";
}
Измените его на тернарный оператор:
public String getMessage2(int code) {
return code == 1 ? "成功" : "失败";
}
После модификации код стал более лаконичным.
4. Суд весной
Что касается исключений параметров, чем раньше они будут найдены, тем лучше.Утверждение предоставляется весной, чтобы помочь нам определить, являются ли параметры действительными.
public void save(Integer code,String name) {
if(code == null) {
throw new Exception("code不能为空");
} else {
if(name == null) {
throw new Exception("name不能为空");
} else {
System.out.println("doSave");
}
}
}
Если параметров много, оператор if...else будет очень длинным.В настоящее время, если вы используете класс Assert для суждения, код будет сильно упрощен:
public String save2(Integer code,String name) {
Assert.notNull(code,"code不能为空");
Assert.notNull(name,"name不能为空");
System.out.println("doSave");
}
Конечно, есть много других сценариев, которые можно оптимизировать, если... иначе я не буду представлять их здесь по одному, и заинтересованные друзья могут оставить мне сообщение для обсуждения и изучения вместе.
Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение
Последнее слово (пожалуйста, обратите внимание, не проституируйте меня по пустякам)
Если эта статья оказалась для вас полезной или поучительной, отсканируйте QR-код и обратите внимание, ваша поддержка — самая большая мотивация для меня продолжать писать.
Попросите в один клик три ссылки: лайк, вперед и смотреть.
Ответ в официальном аккаунте: Интервью, кодовый артефакт, руководство по разработке, управление временем имеют большие преимущества для поклонников, и ответ: Присоединяйтесь к группе, вы можете общаться и учиться у старших руководителей многих производителей BAT.
Для получения более хороших статей, пожалуйста, обратите внимание на общедоступный номер