`
leonzhx
  • 浏览: 794152 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Validate state with class invariants

阅读更多

Class invariants are methods which check the validity of an object's state (its data). The idea is to define validation methods for fields, and to perform these validations whenever the fields change. As usual, this should be done without repeating any code.

An object's state may become invalid for various reasons :

An invalid argument is passed by the caller.
To ensure that the caller fulfills the requirements of a method or constructor, all arguments to non-private methods should be explicitly checked. An Exception should be thrown if a problem is detected. A special case is deserialization, which should treat readObject like a constructor. (Assertions should not be used for these types of checks.)

The implementation of the class is defective.
As a defensive measure, a method which changes the state of an object can include an assertion at its end, to check that the object has indeed remained in a valid state. Here, such assertions verify correctness of internal implementation details - they do not check arguments.

Example 1

Resto is an immutable Model Object (MO). Its class invariant is defined by the validateState method. In this particular case, if validation fails a checked exception is thrown, and the end user is presented with their original input, along with associated error messages.

Such immutable classes represent the simplest case, since validation is performed only once, during construction (and deserialization, if necessary). By definition, an immutable object cannot change state after construction, so performing validations at other times in the object's life is never necessary.

 

package hirondelle.fish.main.resto;

import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.model.Decimal;
import static hirondelle.web4j.model.Decimal.ZERO;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.Validator;
import static hirondelle.web4j.util.Consts.FAILS;

/** Model Object for a Restaurant. */
public final class Resto {

  /**
   Full constructor.
    
   @param aId underlying database internal identifier (optional) 1..50 characters
   @param aName of the restaurant (required), 2..50 characters
   @param aLocation street address of the restaurant (optional), 2..50 characters
   @param aPrice of the fish and chips meal (optional) $0.00..$100.00
   @param aComment on the restaurant in general (optional) 2..50 characters
  */
  public Resto(
    Id aId, SafeText aName, SafeText aLocation, Decimal aPrice, SafeText aComment
  ) throws ModelCtorException {
    fId = aId;
    fName = aName;
    fLocation = aLocation;
    fPrice = aPrice;
    fComment = aComment;
    validateState();
  }
  
  public Id getId() { return fId; }
  public SafeText getName() {  return fName; }
  public SafeText getLocation() {  return fLocation;  }
  public Decimal getPrice() { return fPrice; }
  public SafeText getComment() {  return fComment; }

  @Override public String toString(){
    return ModelUtil.toStringFor(this);
  }
  
  @Override public  boolean equals(Object aThat){
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if ( result ==  null ) {
      Resto that = (Resto) aThat;
      result = ModelUtil.equalsFor(
        this.getSignificantFields(), that.getSignificantFields()
      );
    }
    return result;
  }
  
  @Override public int hashCode(){
    if ( fHashCode == 0 ){
      fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
    }
    return fHashCode;
  }
  
  // PRIVATE //
  private final Id fId;
  private final SafeText fName;
  private final SafeText fLocation;
  private final Decimal fPrice;
  private final SafeText fComment;
  private int fHashCode;
  
  private static final Decimal HUNDRED = Decimal.from("100");

  private void validateState() throws ModelCtorException {
    ModelCtorException ex = new ModelCtorException();
    if ( FAILS == Check.optional(fId, Check.range(1,50)) ) {
      ex.add("Id is optional, 1..50 chars.");
    }
    if ( FAILS == Check.required(fName, Check.range(2,50)) ) {
      ex.add("Restaurant Name is required, 2..50 chars.");
    }
    if ( FAILS == Check.optional(fLocation, Check.range(2,50)) ) {
      ex.add("Location is optional, 2..50 chars.");
    }
    Validator[] priceChecks = {Check.range(ZERO, HUNDRED), Check.numDecimalsAlways(2)};
    if ( FAILS == Check.optional(fPrice, priceChecks)) {
      ex.add("Price is optional, 0.00 to 100.00.");
    }
    if ( FAILS == Check.optional(fComment, Check.range(2,50))) {
      ex.add("Comment is optional, 2..50 chars.");
    }
    if ( ! ex.isEmpty() ) throw ex;
  }
  
  private Object[] getSignificantFields(){
    return new Object[] {fName, fLocation, fPrice, fComment};
  }
}

 

Example 2

Here is an example of a mutable, Serializable class which defines class invariants.

Items to note :

  • the assertion at the end of the close method
  • the call to validateState at the end of the readObject method
  • the implementation is significantly more complex, since the class is mutable

 

import java.text.StringCharacterIterator;
import java.util.*;
import java.io.*;

/**
* In this style of implementation, both the entire state of the object
* and its individual fields are validated without duplicating any code.
*
* Argument validation usually has if's and thrown exceptions at the
* start of a method. Here, these are replaced with a simple
* call to validateXXX. Validation is separated cleanly from the
* regular path of execution, improving legibility.
*/
public final class BankAccount implements Serializable {

   /**
   * @param aFirstName contains only letters, spaces, and apostrophes.
   * @param aLastName contains only letters, spaces, and apostrophes.
   * @param aAccountNumber is non-negative.
   *
   * @throws IllegalArgumentException if any param does not comply.
   */
   public BankAccount( String aFirstName, String aLastName, int aAccountNumber) {
      //don't call an overridable method in a constructor
      setFirstName(aFirstName);
      setLastName(aLastName);
      setAccountNumber(aAccountNumber);
   }

   /**
   * All "secondary" constructors call the "primary" constructor, such that
   * validations are always performed.
   */
   public BankAccount() {
      this ("FirstName", "LastName", 0);
   }

   public String getFirstName() {
     return fFirstName;
   }

   public String getLastName(){
    return fLastName;
   }

   public int getAccountNumber() {
    return fAccountNumber;
   }

   /**
   * This method changes state internally, and may use an assert to
   * implement a post-condition on the object's state.
   */
   public void close(){
     //valid:
     fAccountNumber = 0;

     //this invalid value will fire the assertion:
     //fAccountNumber = -2;

     assert hasValidState(): this;
   }

   /**
   * Names must contain only letters, spaces, and apostrophes.
   *
   * @throws IllegalArgumentException if any param does not comply.
   */
   public void setFirstName( String aNewFirstName ) {
      validateName(aNewFirstName);
      fFirstName = aNewFirstName;
   }

   /**
   * Names must contain only letters, spaces, and apostrophes.
   *
   * @throws IllegalArgumentException if any param does not comply.
   */
   public void setLastName ( String aNewLastName ) {
      validateName(aNewLastName);
      fLastName = aNewLastName;
   }

   /**
   * AccountNumber must be non-negative.
   *
   * @throws IllegalArgumentException if any param does not comply.
   */
   public void setAccountNumber( int aNewAccountNumber ) {
      validateAccountNumber(aNewAccountNumber);
      fAccountNumber = aNewAccountNumber;
   }

   /**
   * Can be used to easily pass object description to an assertion,
   * using a "this" reference.
   */
   public String toString(){
     final StringBuilder result = new StringBuilder();
     final String SPACE = " ";
     result.append(fFirstName);
     result.append(SPACE);
     result.append(fLastName);
     result.append(SPACE);
     result.append(fAccountNumber);
     return result.toString();
   }

   /// PRIVATE /////

   private String fFirstName;
   private String fLastName;
   private int fAccountNumber;

   /**
   * Verify that all fields of this object take permissible values; that is,
   * this method defines the class invariant.
   *
   * Call after deserialization.
   * @throws IllegalArgumentException if any field takes an unpermitted value.
   */
   private void validateState() {
      validateAccountNumber(fAccountNumber);
      validateName(fFirstName);
      validateName(fLastName);
   }

   /**
   * Return true if <code>validateState</code> does not throw
   * an IllegalArgumentException, otherwise return false.
   *
   * Call at the end of any public method which has changed
   * state (any "mutator" method). This is usually done in
   * an assertion, since it corresponds to a post-condition.
   * For example,
   * <pre>
   * assert hasValidState() : this;
   * </pre>
   * This method is provided since <code>validateState</code> cannot be used
   * in an assertion.
   */
   private boolean hasValidState() {
     boolean result = true;
     try {
       validateState();
     }
     catch (IllegalArgumentException ex){
       result = false;
     }
     return result;
   }

   /**
   * Ensure names contain only letters, spaces, and apostrophes.
   *
   * @throws IllegalArgumentException if argument does not comply.
   */
   private void validateName(String aName){
     boolean nameHasContent = (aName != null) && (!aName.equals(""));
     if (!nameHasContent){
       throw new IllegalArgumentException("Names must be non-null and non-empty.");
     }
     StringCharacterIterator iterator = new StringCharacterIterator(aName);
     char character =  iterator.current();
     while (character != StringCharacterIterator.DONE ){
       boolean isValidChar = (Character.isLetter(character)
                             || Character.isSpaceChar(character)
                             || character =='\'');
       if ( isValidChar ) {
         //do nothing
       }
       else {
         String message = "Names can contain only letters, spaces, and apostrophes.";
         throw new IllegalArgumentException(message);
       }
       character = iterator.next();
     }
  }

  /**
  * AccountNumber must be non-negative.
  *
  * @throws IllegalArgumentException if argument does not comply.
  */
   private void validateAccountNumber(int aAccountNumber){
      if (aAccountNumber < 0) {
        String message = "Account Number must be greater than or equal to 0.";
        throw new IllegalArgumentException(message);
      }
   }

   private static final long serialVersionUID = 7526472295622776147L;

   /**
   * Always treat de-serialization as a full-blown constructor, by
   * validating the final state of the de-serialized object.
   */
   private void readObject(ObjectInputStream aInputStream)
                                throws ClassNotFoundException, IOException {
     //always perform the default de-serialization first
     aInputStream.defaultReadObject();

     //ensure that object state has not been corrupted or
     //tampered with maliciously
     validateState();
  }

  /**
  * Test harness.
  */
  public static void main (String[] aArguments) {
    BankAccount account = new BankAccount("Joe", "Strummer", 532);
    //exercise specific validations.
    account.setFirstName("John");
    account.setAccountNumber(987);
    //exercise the post-condition assertion
    //requires enabled assertions: "java -ea"
    account.close();

    //exercise the serialization
    ObjectOutput output = null;
    try{
      OutputStream file = new FileOutputStream( "account.ser" );
      OutputStream buffer = new BufferedOutputStream( file );
      output = new ObjectOutputStream( buffer );
      output.writeObject(account);
    }
    catch(IOException exception){
      System.err.println(exception);
    }
    finally{
      try {
        if (output != null) output.close();
      }
      catch (IOException exception ){
        System.err.println(exception);
      }
    }

    //exercise the deserialization
    ObjectInput input = null;
    try{
      InputStream file = new FileInputStream( "account.ser" );
      InputStream buffer = new BufferedInputStream( file );
      input = new ObjectInputStream ( buffer );
      BankAccount recoveredAccount = (BankAccount)input.readObject();
      System.out.println( "Recovered account: " + recoveredAccount );
    }
    catch(IOException exception){
      System.err.println(exception);
    }
    catch (ClassNotFoundException exception){
      System.err.println(exception);
    }
    finally{
      try {
        if ( input != null ) input.close();
      }
      catch (IOException exception){
        System.err.println(exception);
      }
    }
  }
} 

 

This article originates from http://www.javapractices.com/topic/TopicAction.do?Id=6

分享到:
评论

相关推荐

    jquery validate 信息气泡提示

    在网页开发中,jQuery Validate 是一个非常常用的验证插件,用于对用户输入的数据进行校验,确保数据的有效性和完整性。这个插件可以帮助开发者创建复杂的表单验证规则,提高用户体验,减少服务器端的压力。结合 ...

    jquery.validate 版本大全

    jquery.validate.1.9.0.min.js jquery.validate.1.12.0.min.js jquery.validate.1.13.1.min.js jquery.validate.1.16.0.min.js jquery.validate.1.14.0.min.js jquery.validate.1.15.1.min.js jquery.validate....

    validate方法

    标题中的"validate方法"通常指的是在编程中用于验证数据或对象的方法。这可能是为了确保输入的数据符合特定的格式、规则或者限制,以防止错误、安全问题或者数据不一致。在不同的编程语言和框架中,validate方法可能...

    jQueryValidate.rar

    在使用jQuery Validate时,我们需要对目标表单元素添加特定的class或者data属性来指定验证规则。例如,我们可以使用`required`标志来表明某个字段是必填的,或者使用`minlength`和`maxlength`来限制输入的字符长度。...

    jquery.validate.js下载

    jquery.validate.js jquery.validate.js

    mysql 安装密码校验插件validate_password.docx

    以下是安装validate_password插件的详细步骤,以及相关的配置和使用方法。 1. **修改配置文件** 首先,你需要编辑MySQL的配置文件,通常位于`/etc/my.cnf`(根据你的操作系统和安装路径可能有所不同)。使用命令`...

    jQuery.validate实例

    《jQuery.validate实例详解》 在Web开发中,表单验证是不可或缺的一部分,它能确保用户输入的数据符合我们设定的规则,提高数据的准确性和安全性。jQuery库中的validate插件为开发者提供了一种简单、高效的表单验证...

    jquery.validate使用攻略

    - **错误元素**:默认情况下,错误消息会显示在输入元素之后,但可以通过 `errorElement` 和 `errorClass` 设置自定义元素和类名。 - **错误定位**:使用 `errorPlacement` 回调函数控制错误消息的位置。 - **错误...

    jQuery validate框架的个性化验证

    jQuery Validate 是一个强大的客户端验证插件,用于在用户提交表单前进行实时验证。这个框架大大简化了HTML表单的验证过程,提供了丰富的内置验证规则和可扩展的自定义验证功能,使得表单验证更加人性化和高效。 1....

    jQuery Validate 1.1.2

    jQuery Validate 是一个强大的JavaScript库,专门用于前端表单验证,由jQuery团队开发并维护。它极大地简化了在网页上创建高效、用户友好的验证规则的过程,避免了开发者编写大量重复的验证代码。jQuery Validate ...

    jQuery Validate表单验证插件 添加class属性形式的校验

    jQuery Validate是jQuery的一个重要插件,它专门用于进行表单验证。表单验证是前端开发中一个重要的环节,可以确保用户提交的信息是完整和正确的。通过在class属性中添加校验规则,可以使表单校验的实现更为简便。 ...

    vue中使用vee-validate

    classNames: { touched: 'touched', untouched: 'untouched', valid: 'valid', invalid: 'invalid', pristine: 'pristine', dirty: 'dirty', }, events: 'blur', inject: true, }; Vue.use(VeeValidate...

    jquery.validate 使用说明文档

    在本文中,我们将深入探讨如何使用 jQuery Validate,包括基本的使用方法、可选参数以及常见的验证规则。 首先,要使用 jQuery Validate,你需要在页面中引入 jQuery 库和 jQuery Validate 的脚本文件。如以下代码...

    jQuery.validate.js+API中文

    《jQuery.validate.js API 中文详解》 jQuery.validate.js 是一个非常流行的JavaScript库,它为HTML表单提供了强大的验证功能。这个库是基于jQuery构建的,因此可以无缝集成到任何使用jQuery的项目中,大大简化了...

    jquery validate例子

    《jQuery Validate插件详解与实例应用》 在Web开发中,表单验证是不可或缺的一环,它能够确保用户输入的数据符合预设的规则,提高数据的准确性和安全性。jQuery Validate是一个强大的JavaScript库,专为jQuery设计...

    jquery_validate插件总结

    这是一个关于jquery_validate插件学习的总结,内容不多,但是都是干货,有兴趣的可以看一下。

    jquery validate 验证自定义样式

    在本文中,我们将深入探讨如何利用jQuery Validate来创建自定义验证样式。 首先,我们从标题"jquery validate 验证自定义样式"开始。jQuery Validate插件默认提供了一些基本的样式,但这些样式可能不能满足所有设计...

    jQuery.validate 用法

    《jQuery.validate 用法详解及源码解析》 在网页开发中,表单验证是必不可少的一环,确保用户输入的数据符合预设的规则,避免无效数据的提交。jQuery.validate插件是一个强大的、易于使用的JavaScript库,它使得在...

    jquery.validate.min.js

    jquery.validate.min.js jquery jquery验证插件 validate

    比较好用的 FormValidate

    标题中的“比较好用的 FormValidate”指的是一个用于表单验证的工具或库,它可能是一个JavaScript框架,旨在帮助开发者更方便、高效地实现前端或后端的表单数据验证。在网页开发中,表单验证是必不可少的部分,它...

Global site tag (gtag.js) - Google Analytics