聊一聊防御式编程

205 阅读4分钟

我正在参加「掘金·启航计划」

防御式编程背景

定义

防御性编程(Defensive programming)是防御式设计的一种具体体现,它是为了保证,对程序的不可预见的使用,不会造成程序功能上的损坏。它可以被看作是为了减少或消除墨菲定律效力的想法。防御式编程主要用于可能被滥用,恶作剧或无意地造成灾难性影响的程序上。

所以说防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。

更一般地说,其核心想法是要承认程序都会有问题,都需要被修改,聪明的程序员应该根据这一点来编程序。

手段

防御式编程就是不信任任何外部输入参数,在程序未执行前解决掉所有的风险点,主要有以下几点:

  1. 检查所有外部输入数据的值
  2. 检查子程序所有数据参数的值
  3. 决定如何处理错误的输入数据

目的

  • 提高工程质量,减少bug和问题
  • 提高代码可读性,使得代码更容易理解
  • 让程序能通过预期的行为来处理不可预期的用户操作

举个例子,下面的代码就是关于防御式编程一个简单的例子

public void doSomething(DomainA a) {
    if (a == null) {
        return ; //log some errorA
    }
    assignAction;
	
    if (a.getB() == null) {
        return ; //log some errorB
    }
    otherAction;
	
    if (!(a.getB().getC instanceof DomainC)) {
        return ;//log some errorC
    }
    doSomethingA();
    doSomethingB();
    doSomthingC();
}

后续我们一起看看防御式编程练习,进一步理解 Java 中如何优雅的防御式解决空指针的异常。

防御式编程练习

reference:【灰蓝 Java 训练】如何处理空值

总结:防御 NullPointerException 的方法有很多,针对不同类型可选方案也有许多。比如:

  1. Optional 可选值类型
  2. @NonNull @Nullable 注解
  3. StringUtils、ObjectUtils、BooleanUtils、CollectionUtils 等工具类

1. 代码纠错

// "POST".equals(xMethod)
if (xMethod.equals("POST")) {
   doPost();
}

//  Objects.equals(xArg1, xArg2)
if (xArg1.equals(xArg2)) {
    System.out.println("arg1 == arg2");
}

2. 代码纠错

// 不可变集合不能为 null 
// @NonNull 参数 (其它方式暂时没想到)
var map1 = Map.of("abc", 10, "def", 20, xStr, 30);

// 不可变集合不能为 null 
// var set = new HashSet<Integer>();
// CollectionUtils.addAll(set, list1);
var list1 = Arrays.asList(1, 2, -3, 9, null, 15);
var set1 = Set.copyOf(list1);

// 不可变集合不能为 null != xStr

// ObjectUtils.isNotEmpty(xStr) && ...
if (Map.of("hello", 1, "world", 2).containsKey(xStr)) {
    System.out.println("either 'hello' or 'world'");
}

3. 加注可空性注解

public static <T, U> U mapSome(T x, Function<T, U> mapper) {
    return x == null ? null: mapper.apply(x);
}

@Nullable
public static <T, U> U mapSome(@Nullable T x,@NonNull Function<T, U> mapper) {
    // 返回值以及参数可以加注解做校验 Idea 会做出对应的提醒
    return x == null ? null: mapper.apply(x);
}

// Optional 重构

public static <T, U>  U mapSome1( T x, Function<T, U> mapper) {
    // Option 实现  boolean ? x1: x2;
    return (U) Optional.ofNullable(x).orElse((T) mapper.apply(x));
}

4. Optional 重构

Integer xInt = Math.random() > 0.8 ? null : Math.random() > 0.5 ? 5 : 12;

// 重构以下代码

var x1 = (xInt == null || xInt % 2 != 0) ? null : xInt / 2;
if (x1 != null) {
    System.out.println(x1);
}

// 重构
Optional.ofNullable(xInt).filter(x -> x % 2 == 0).map( y -> y / 2).ifPresent(System.out::println);

5. 用 Optional 重构 getTitledContent()

// 重构一下代码

@NonNull

public static String getUpperTitle(@Nullable Post post) {
    if (post == null || post.getTitle() == null) {
        log.warning("no title")
        return "- UNTITLED -";
    }

    return post.getTitle().toUpperCase();
}

public class Post {

    @Nullable
    public String getTitle();

}
//  重构后
private static String getUpperTitle1(Post post) {
    return Optional.ofNullable(post).map(Post::getTitle).orElse("- UNTITLED -").toUpperCase();
}

// 更正
@NonNull
private static String getUpperTitle2(@Nullable Post post) {
    var temp = Optional.ofNullable(post).flatMap(item -> Optional.ofNullable(item.getTitle()));

    if (temp.equals(Optional.empty())) {
        log.warning("no title");
    }

    return temp.orElse("- UNTITLED -").toUpperCase();
}


// 更正 (不知道对不对 但是只找到这一种方式)
private static String getUpperTitle1(Post post) {
    return Optional.ofNullable(post).
            map(Post::getTitle).
            orElseGet(() -> {log.warning("no title");return "- UNTITLED -";}).
            toUpperCase();
}

6. 用 StringUtilsgetChoice() 的实现重构成一行代码

String getChoice(String choice, boolean highest) {
    if (choice != null && !choice.isEmpty())
        return choice;

    if (highest)
        return "High";
    return "Low";
}

// 结果为
private static String getChoice1(String choice, boolean highest) {
    return  StringUtils.isNotEmpty(choice) ? choice : highest ? "High": "Low";
}

7. 使用 Common Collections 重构 getIdsString()

private static final List<Integer> IMPLICIT_IDS = List.of(101, 111, 191);
public static String getIdsString(@Nullable Collection<@NonNull Integer> ids) {
    if (ids == null) {
        return IMPLICIT_IDS.stream()
                .map(Object::toString)
                .collect(Collectors.joining());
    }
    return Stream.concat(ids.stream(), IMPLICIT_IDS.stream())
            .map(Object::toString)
            .collect(Collectors.joining());
}



// 重构后

import org.apache.commons.collections4.CollectionUtils;

public static String getIdsString1(@Nullable Collection<@NonNull Integer> ids) {
    ids = CollectionUtils.emptyIfNull(ids);
    return Stream.concat(ids.stream(), IMPLICIT_IDS.stream())
            .map(Object::toString)
            .collect(Collectors.joining());
}

8. 使用 BooleanUtils 重构

public static String toHex(int n, @Nullable Boolean useUpper) {
    String s = Integer.toString(n, 16);
    return useUpper != null && useUpper ? s.toUpperCase() : s;
}

// 重构后
public static String toHex1(int n, Boolean useUpper) {
    String s = Integer.toString(n, 16);
    return BooleanUtils.toBoolean(useUpper) ? s.toUpperCase() : s;
}

9. 使用 ObjectUtils 重构

public static Instant tomorrowOf(@Nullable Instant x) {
    if (x == null) {
        log.debug("the base Date is null");
        x = Instant.now();
    }

    return x.plus(Duration.ofDays(1));
}

// 重构后
public static Instant tomorrowOf1( Instant x) {

    return ObjectUtils.isEmpty(x) ? Instant.now().plus(Duration.ofDays(1)) : x.plus(Duration.ofDays(1));

}

// 更正 更正  
public static Instant tomorrowOf2( Instant x) {
    if (ObjectUtils.isEmpty(x)) {
        log.debug("the base Date is null");
        x = Instant.now();
    }
    return  x.plus(Duration.ofDays(1));
}

// 更正 
public static Instant tomorrowOf3( Instant x) {
            map(obj -> obj.plus(Duration.ofDays(1))).
            orElseGet(() -> {log.debug("the base Date is null");return Instant.now().plus(Duration.ofDays(1));});

}

// 更正 2021-12-15 20:40
public static Instant tomorrowOf41( Instant x) {
    return  ObjectUtils.getFirstNonNull(
            () -> x,
            () -> {
                log.debug("the base Date is null");
                return Instant.now();
            }).plus(Duration.ofDays(1));

public static Instant tomorrowOf42( Instant x) {
    return ObjectUtils.getIfNull(x, 
            () -> {
                log.debug("the base Date is null");
                return Instant.now();
            }).plus(Duration.ofDays(1));
}

参考文档