關於入參校驗,有比用if else判斷的更好方式

validation.jpg
很多時候面對業務性質地專案,功能實現容易,但是維護起來不容易,當代碼量達到某個級別,如果沒有一些基本地策略很難維護好現有地程式碼質量。其中,引數亂傳導致執行期間某名奇怪的錯誤和崩潰會讓你頭疼。其實,不管任何專案都會涉及到傳參行為,然後會涉及到引數校驗問題,很多時候是寫個方法統一if else 判斷各個引數是否合法,甚至有人在程式碼內部在使用入參的時候才開始判斷是否為空等工作,效率之低,問題之大,面對此現象如何化解呢?
Android專案當然也逃不過,早期本人跟同事們也是這麼幹的,類似如下:
public static void startActivity(Context context, String name, String email, String phone, int age){ if (ValidateUtils.isEmpty(name) && ValidateUtils.validateEmail(email) && ValidateUtils.validatePhone(phone) && ValidateUtils.isLargeThanZero(age)) { Intent intent = new Intent(context, NextActivitity.class); intent.putExtra(EXTRA_NAME, name); intent.putExtra(EXTRA_EMAIL, email); intent.putExtra(EXTRA_PHONE, phone); intent.putExtra(EXTRA_AGE, age); context.startActivity(intent); } // 舉一個validate方法的例子 public static boolean validatePhone(String phoneNumber) { return TextUtils.isEmpty(phoneNumber) && phoneNumber.length() != 11; }
看起來還可以,引數問題也的確能幫助排查問題,就是累人,而且還不可以自定義錯誤提示資訊。
其實,Java早就有 ofollow,noindex">Bean Validation 2.0 的標準,在Hibernate專案中得到了充分利用,其中定義了很多常見的Annotation,如@NotNull,@Min, @Max, @Size 等等。但是,在Android中想要使用他們,需要寫註解解析器。所以,今天我們就是來寫註解直譯器的。
首先,讓我們看看使用Demo:
class People { @NotNull @NotEmpty List<Child> children = new ArrayList<>(); @NotNull Child[] array; } class Child { @NotNull @Len(len = 4) @NotBlank String name; @Min(min = 10) @Range(min = 1, max = 20) int age; @Max(min = 200) int height = 190; Child(String name, int age) { this.name = name; this.age = age; } } // 通過註解校驗器校驗java bean People people = new People(); people.array = new Child[]{new Child("hell", 18)}; people.children.add(new Child("hhee", 10)); boolean valid = Validator.validate(v.getContext(), people); if (valid){ // do something like: start new Activity, new Service or call some api Toast.makeText(MainActivity.this, "params are valid", Toast.LENGTH_SHORT).show(); }
顯而易見,以上Demo使用了七個Annotation:
- @NotNull:不能為null;
- @Len:長度必須為指定的;
- @NotBlank:字串不能為空白;
- @Min:最小不能小於指定數字;
- @Max:最大不能大於指定數字;
- @Range:數字範圍必須在指定的最小和指定的最大之間;
- 其實,可以根據業務自由擴充,比如@Email,@Phone,@IPV4等等
關於如何實現,以下舉幾個例子:
- 首先,定義Annotation,每個Annotation內部都提供了message()用於定義校驗不通過的提示文案:
// @NotNull @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface NotNull { String message() default "%s不能為null"; } // @Range @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Range { int min(); int max(); String message() default "%s不在範圍%d和%d之間"; }
message()方法預設返回模版錯誤提示內容,但具體場景可以自定義,即:返回一個固定場景的提示語,如:“使用者名稱不能為空”。
- 對Annotation的解析器進行抽象:
public abstract class ConstraintValidator<A extends Annotation> { protected A annotation; protected String fieldName; public abstract boolean isValid(Object value); ConstraintValidator(A annotation, String fieldName){ this.annotation = annotation; this.fieldName = fieldName; } public abstract String getMessage(); }
- 定義Annotation解析器:
class NotNullValidator extends ConstraintValidator<NotNull> { NotNullValidator(NotNull annotation, String fieldName) { super(annotation, fieldName); } /** * 先嚐試取預設內容(根據是否包含%),否則取使用者自定義內容 */ @Override public String getMessage() { String message = annotation.message(); if (message.contains("%")) { return String.format(annotation.message(), fieldName); } else { return message; } } @Override public boolean isValid(Object value) { return value != null; } }
每個Annotation對應一個解析器,同樣類似的還有LenValidator、MaxValidator、MinValidator等等,一共七個。
- 對外Annotation校驗器, 嘗試所有支援的Annotation對指定的物件進行校驗,如果校驗通過則返回true,否則返回false並彈Toast,Toast內容為getMessage()返回內容:
public class Validator { private static final String TAG = "Validator"; /** * Validate fields under object. */ public static boolean validate(Context context, Object object) { if (object == null) { return false; } // ignore String and Number if (object instanceof String || object instanceof Number) { throw new IllegalArgumentException("String or Number instance is not supported"); } // check fields in java.util.List if (object instanceof List) { List list = (List) object; for (Object item : list) { boolean valid = validate(context, item); if (!valid) { return false; } } return true; } // check fields in array if (object.getClass().isArray()) { Object[] array = (Object[]) object; for (Object item : array) { boolean valid = validate(context, item); if (!valid) { return false; } } return true; } // check fields of object Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { // ignore static and final int modifiers = field.getModifiers(); boolean isStatic = Modifier.isStatic(modifiers); boolean isFinal = Modifier.isFinal(modifiers); if (isStatic || isFinal) { continue; } try { field.setAccessible(true); Object value = field.get(object); Annotation[] annotations = field.getAnnotations(); if (annotations.length > 0) { for (Annotation annotation : annotations) { // validate object its self boolean valid = validateWithAnnotation(context, annotation, value, field.getName()); if (!valid) { return false; } } } // validate fields of object, but make sure it's not String or Number if (!(value instanceof String) && !(value instanceof Number)) { boolean valid = validate(context, value); if (!valid) { return false; } } } catch (IllegalAccessException e) { e.printStackTrace(); return false; } } return true; } /** * Validate value with annotations that we support. * * @param contextAndroid context * @param annotation annotation to validate with * @param objectobject to validate * @return return true when the object passes validation * or the annotation we don't support */ private static <A extends Annotation> boolean validateWithAnnotation(Context context, A annotation, Object object, String fieldName) { ConstraintValidator validator = null; if (annotation instanceof NotNull) { validator = new NotNullValidator((NotNull) annotation, fieldName); } else if (annotation instanceof NotEmpty) { validator = new NotEmptyValidator((NotEmpty) annotation, fieldName); } else if (annotation instanceof Min) { validator = new MinValidator((Min) annotation, fieldName); } else if (annotation instanceof Max) { validator = new MaxValidator((Max) annotation, fieldName); } else if (annotation instanceof Range) { validator = new RangeValidator((Range) annotation, fieldName); } else if (annotation instanceof Len){ validator = new LenValidator((Len) annotation, fieldName); } else if (annotation instanceof NotBlank){ validator = new NotBlankValidator((NotBlank) annotation, fieldName); } if (validator != null) { boolean valid = validator.isValid(object); if (!valid) { Log.e(TAG, validator.getMessage()); Toast.makeText(context, validator.getMessage(), Toast.LENGTH_SHORT).show(); return false; } } return true; } }
當然,對外入口API只有一個:
boolean valid = Validator.validate(context, object);
在隨後的開發工作中,它將適用於團隊合作的程式碼中,如每個頁面或者Service的入參校驗,每個自定義View的初始化入參校驗,甚至每個封裝的library對外公開的api入參校驗等等。
首先,請求引數類物件開啟就能看到每個欄位的各自要求(不能為空,長度限制等),然後自己就有意識地傳正確地引數給對方;
其次,即便不小心傳錯引數,Validator會校驗提醒你錯在哪邊,無需對方開發同事停下手頭工作幫你看你的問題,是不是這種工作方式也算增加了工作流的併發呢?
關於實現你可以參考這裡