Total Pageviews

2013/11/18

AngularJS - How to remove unnecessary blank option in dynamic drop down list

Based on AngularJS - How to create dynamic drop down list this post, you had learned how to create a dynamic drop down list via AnguarJS.

Problem
But we found out a problem, AngularJS will create an empty option in the dynamic drop down list and will place at first location. It's not my expected result.

Solution
You can set a default value to your dynamic drop down list, then you can fix your problem. Here is code snippet:
1:      //query  
2:      $scope.query = function(){  
3:        var result = nss702rService.query('rest/', $scope.model);  
4:        result.then(function(response){  
5:         $scope.dpNos = response;  
6:         //if have more than one record in dpNos,  
7:         //then set the first option as its default value  
8:         if($scope.dpNos.length > 0){  
9:           $scope.model.dpNo = $scope.dpNos[0].uid;  
10:         }  
11:       });  
12:      };  

Check the result. You won't find the empty option in the dynamic drop down list


AngularJS - How to create dynamic drop down list

Requirement
The "區局代號" drop down list in this page should generate dynamically from database

The sql statement is as bellows:
1:  select UID, NAME   
2:  from UAA001V1   
3:  where ORGCD = 'cht12345' and UID <> '957500';  

And expected to retrieve two records

How to do it
[Step1] Create an value object class to store the result.
1:  /**  
2:   *   
3:   */  
4:  package gov.nta.nss.dto;  
5:  import java.io.Serializable;  
6:  // TODO: Auto-generated Javadoc  
7:  /**  
8:   * The Class UAA001V1Bean.  
9:   */  
10:  public class UAA001V1Bean implements Serializable {  
11:    private static final long serialVersionUID = -7541446851729769394L;  
12:    // 機關代號  
13:    private String uid;  
14:    // 機關名稱  
15:    private String name;  
16:    /**  
17:     * Gets the uid.  
18:     *   
19:     * @return the uid  
20:     */  
21:    public String getUid() {  
22:      return uid;  
23:    }  
24:    /**  
25:     * Sets the uid.  
26:     *   
27:     * @param uid  
28:     *      the uid to set  
29:     */  
30:    public void setUid(String uid) {  
31:      this.uid = uid;  
32:    }  
33:    /**  
34:     * Gets the name.  
35:     *   
36:     * @return the name  
37:     */  
38:    public String getName() {  
39:      return name;  
40:    }  
41:    /**  
42:     * Sets the name.  
43:     *   
44:     * @param name  
45:     *      the name to set  
46:     */  
47:    public void setName(String name) {  
48:      this.name = name;  
49:    }  
50:    /**  
51:     * {@inheritDoc}  
52:     */  
53:    @Override  
54:    public String toString() {  
55:      return "UAA001V1Bean [uid=" + uid + ", name=" + name + "]";  
56:    }  
57:  }  

[Step2] Create a controller class, and do query to return a List of UAA001V1Bean
1:  /**  
2:   *   
3:   */  
4:  package gov.nta.nss.web.rest;  
5:  import gov.nta.nss.Messages;  
6:  import gov.nta.nss.dto.UAA001V1Bean;  
7:  import gov.nta.nss.service.Nss702rService;  
8:  import gov.nta.nss.web.dto.Nss702r;  
9:  import java.util.List;  
10:  import org.apache.commons.collections.CollectionUtils;  
11:  import org.slf4j.Logger;  
12:  import org.slf4j.LoggerFactory;  
13:  import org.springframework.beans.factory.annotation.Autowired;  
14:  import org.springframework.http.MediaType;  
15:  import org.springframework.stereotype.Controller;  
16:  import org.springframework.web.bind.annotation.RequestBody;  
17:  import org.springframework.web.bind.annotation.RequestMapping;  
18:  import org.springframework.web.bind.annotation.RequestMethod;  
19:  import org.springframework.web.bind.annotation.ResponseBody;  
20:  import com.cht.commons.web.Alerter;  
21:  /**  
22:   *   
23:   */  
24:  @Controller  
25:  @RequestMapping("NSS702R/rest/")  
26:  public class Nss702rResouce {  
27:    private final static Logger LOG = LoggerFactory.getLogger(Nss702rResouce.class);  
28:    @Autowired  
29:    private Nss702rService nss702rService;  
30:    /**  
31:     * Gets the uaa001v1 beans.  
32:     *   
33:     * @return the uaa001v1 beans  
34:     */  
35:    @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)  
36:    public @ResponseBody  
37:    List<UAA001V1Bean> getUaa001v1Beans(@RequestBody Nss702r nss702r, Alerter alerter) {  
38:      // find all 區局代號 list  
39:      List<UAA001V1Bean> uaa001v1Beans = nss702rService.getUaa001v1Beans();  
40:      LOG.info("uaa001v1Beans=" + uaa001v1Beans.toString());  
41:      // if List of UAA001V1Bean is empty, return "找不到區局代號"  
42:      if (CollectionUtils.isEmpty(uaa001v1Beans)) {  
43:        alerter.info(Messages.nss702r_dpno_not_exist());  
44:      }  
45:      return uaa001v1Beans;  
46:    }  
47:  }  

[Step3] js file (only includes code snippet)
1:       //......................  
2:      //query  
3:      $scope.query = function(){  
4:        var result = nss702rService.query('rest/', $scope.model);  
5:        result.then(function(response){  
6:         $scope.dpNos = response;   
7:       });  
8:      };  
9:      $scope.refresh = function(){  
10:        $scope.query();  
11:      };  
12:      $scope.refresh();  
13:            //......................  

[Step4] html file  (only includes code snippet)
1:           <div class="form-group col-sm-5">  
2:            <label class="control-label">區局代號 :</label>  
3:            <select class="form-control" style="width: 80%;"  
4:                id="dpNo" name="dpNo" data-ng-model="model.dpNo"  
5:                data-ng-options="dpNo.uid as dpNo.name for dpNo in dpNos">  
6:            </select>  
7:           </div>  

Check the result

2013/11/15

How to adjust column width to fit the contents in Apache POI

Problem
I utilized Apache POI to write data into excel file. But I found out each cell width has the same default width, it does not adjust it's width based on its contents.

Solution
You can call autoSizeColumn method in HSSFSheet to fix this problem.
Here is the JavaDoc: http://poi.apache.org/apidocs/org/apache/poi/hssf/usermodel/HSSFSheet.html#autoSizeColumn(int, boolean)
If you have 10 columns, you need to call autoSizeColumn 10 times in for-loop.
For example.
1:          for (int resizeCnt = 0; resizeCnt < pdateSet.size() + 3; resizeCnt++) {  
2:            sheet.autoSizeColumn(resizeCnt);  
3:          }  
Check the result after we call autoSizeColumn. 
You can see some columns' width may not really fit the contents, but it's much better than the original one.

Apache Commons Application - Predicate Chain

Requirement
If you have a collection of value object, and would like to do search just like SQL.
And you may have more complex requirement, ex. two or more search criteria.
You may can try Apache Commons CollectionUtils and BeanUtils.

Example
Prepare a bean class whose named person with 3 attributes (name, birthPlace and department)
1:  /**  
2:   *   
3:   */  
4:  package test.collection;  
5:  import java.io.Serializable;  
6:  // TODO: Auto-generated Javadoc  
7:  /**  
8:   * The Class Person.  
9:   *   
10:   * @author albert  
11:   */  
12:  public class Person implements Serializable {  
13:       /** The Constant serialVersionUID. */  
14:       private static final long serialVersionUID = -4637096033730683016L;  
15:       /** The name. */  
16:       private String name;  
17:       /** The birth place. */  
18:       private String birthPlace;  
19:       /** The department. */  
20:       private String department;  
21:       /**  
22:        * Instantiates a new person.  
23:        */  
24:       public Person() {  
25:            super();  
26:            // TODO Auto-generated constructor stub  
27:       }  
28:       /**  
29:        * Instantiates a new person.  
30:        *   
31:        * @param name  
32:        *      the name  
33:        * @param birthPlace  
34:        *      the birth place  
35:        * @param department  
36:        *      the department  
37:        */  
38:       public Person(String name, String birthPlace, String department) {  
39:            super();  
40:            this.name = name;  
41:            this.birthPlace = birthPlace;  
42:            this.department = department;  
43:       }  
44:       /**  
45:        * Gets the name.  
46:        *   
47:        * @return the name  
48:        */  
49:       public String getName() {  
50:            return name;  
51:       }  
52:       /**  
53:        * Sets the name.  
54:        *   
55:        * @param name  
56:        *      the new name  
57:        */  
58:       public void setName(String name) {  
59:            this.name = name;  
60:       }  
61:       /**  
62:        * Gets the birth place.  
63:        *   
64:        * @return the birth place  
65:        */  
66:       public String getBirthPlace() {  
67:            return birthPlace;  
68:       }  
69:       /**  
70:        * Sets the birth place.  
71:        *   
72:        * @param birthPlace  
73:        *      the new birth place  
74:        */  
75:       public void setBirthPlace(String birthPlace) {  
76:            this.birthPlace = birthPlace;  
77:       }  
78:       /**  
79:        * Gets the department.  
80:        *   
81:        * @return the department  
82:        */  
83:       public String getDepartment() {  
84:            return department;  
85:       }  
86:       /**  
87:        * Sets the department.  
88:        *   
89:        * @param department  
90:        *      the new department  
91:        */  
92:       public void setDepartment(String department) {  
93:            this.department = department;  
94:       }  
95:       /*  
96:        * (non-Javadoc)  
97:        *   
98:        * @see java.lang.Object#toString()  
99:        */  
100:       @Override  
101:       public String toString() {  
102:            return "Person [birthPlace=" + birthPlace + ", department="  
103:                      + department + ", name=" + name + "]";  
104:       }  
105:  }  

Here is our CollectionTest class to demonstrate how to do filter in collection via Apache Commons CollectionUtils and BeanUtils.
The entry point is main method.

  • Step1. Call setUpData to create test data (List of Person).
  • Step2. Call filterDataWithTwoCriteria and pass search criteria (find out the person who birthPlace is 'ChiaYi' and work at 'DEPT1' department)
1:  package test.collection;  
2:  import java.util.ArrayList;  
3:  import java.util.List;  
4:  import org.apache.commons.beanutils.BeanPropertyValueEqualsPredicate;  
5:  import org.apache.commons.collections.CollectionUtils;  
6:  import org.apache.commons.collections.Predicate;  
7:  import org.apache.commons.collections.PredicateUtils;  
8:  // TODO: Auto-generated Javadoc  
9:  /**  
10:   * The Class CollectionTest.  
11:   */  
12:  public class CollectionTest {  
13:       /** The person list. */  
14:       List<Person> personList = new ArrayList<Person>();  
15:       /**  
16:        * Sets up data.  
17:        */  
18:       void setUpData() {  
19:            personList.add(new Person("Albert", "ChiaYi", "DEPT1"));  
20:            personList.add(new Person("Mandy", "Taipei", "DEPT2"));  
21:            personList.add(new Person("Alex", "ChiaYi", "DEPT1"));  
22:            personList.add(new Person("Chris", "Taipei", "DEPT1"));  
23:       }  
24:       /**  
25:        * Filter data with two criteria.  
26:        *   
27:        * @param birthPlace  
28:        *      the birth place  
29:        * @param department  
30:        *      the department  
31:        */  
32:       @SuppressWarnings("unchecked")  
33:       void filterDataWithTwoCriteria(String birthPlace, String department) {  
34:            // set up birth place predicate  
35:            BeanPropertyValueEqualsPredicate birthPlacePredicate = new BeanPropertyValueEqualsPredicate(  
36:                      "birthPlace", birthPlace);  
37:            // set up department predicate  
38:            BeanPropertyValueEqualsPredicate departmentPredicate = new BeanPropertyValueEqualsPredicate(  
39:                      "department", department);  
40:            // Create a new Predicate that returns true only if all of the specified  
41:            // predicates are true (i.e. birthPlace='ChiaYi' and department='DEPT1')  
42:            Predicate predicates = PredicateUtils.allPredicate(new Predicate[] {  
43:                      birthPlacePredicate, departmentPredicate });  
44:            // Selects all elements from input collection which match the given  
45:            // predicate into an output collection.  
46:            List<Person> persons = (List<Person>) CollectionUtils.select(  
47:                      personList, predicates);  
48:            // print the output collection  
49:            if (CollectionUtils.isNotEmpty(persons)) {  
50:                 for (Person person : persons) {  
51:                      System.out.println(person.toString());  
52:                 }  
53:            }  
54:       }  
55:       /**  
56:        * The main method.  
57:        *   
58:        * @param args  
59:        *      the arguments  
60:        */  
61:       public static void main(String[] args) {  
62:            CollectionTest test = new CollectionTest();  
63:            // Sets up data  
64:            test.setUpData();  
65:            // find the person who birth place is ChiaYi and work at DEPT1  
66:            // department  
67:            test.filterDataWithTwoCriteria("ChiaYi", "DEPT1");  
68:       }  
69:  }  

After executing this standalone Java application, the console will print the person whose birth place is 'ChiaYi' and work at 'DEPT1' department
1:  Person [birthPlace=ChiaYi, department=DEPT1, name=Albert]  
2:  Person [birthPlace=ChiaYi, department=DEPT1, name=Alex]  

2013/11/14

How to set currecny cell to right-justified horizontal alignment and apply 1000 separator in Apache POI

Problem
We utilized Apache POI to write excel, but some currency cells do not right-justified horizontal alignment and do not apply 1000 separator.

Solution
1:          //create CellStyle, and define style information  
2:          CellStyle cs = workbook.createCellStyle();  
3:          cs.setBorderBottom((short) 1);  
4:          cs.setBorderTop((short) 1);  
5:          cs.setBorderLeft((short) 1);  
6:          cs.setBorderRight((short) 1);  
7:          cs.setAlignment(CellStyle.ALIGN_RIGHT);//right-justified horizontal alignment  
8:          cs.setDataFormat(HSSFDataFormat.getBuiltinFormat("#,##0.00"));//apply 1000 separator  
9:          .....  
10:          .....  
11:          //set cs, the CellStyle with style information which we defined, into cell  
12:          cell.setCellStyle(cs);  


For more DataFormat information, you can check it:

Check the result

Here you can find more useful information: http://javacrazyer.iteye.com/blog/894850

2013/11/12

POI 常用API筆記

參考資料來源:http://become.wei-ting.net/2011/11/poiexcel.html

1:  File tempFile = new File(filePath,filename);//建立儲存檔案  
2:  Workbook workbook = new HSSFWorkbook();//建立Excel物件  
3:  String safeName = WorkbookUtil.createSafeSheetName(SHEETNAME);   
4:  Sheet sheet = workbook.createSheet(safeName);//建立工作表  
5:  Row row1 = sheet.createRow((short)0);//建立工作列  
6:  //字型設定  
7:  Font font = workbook.createFont();  
8:  font.setColor(HSSFColor.WHITE.index);//顏色  
9:  font.setBoldweight(Font.BOLDWEIGHT_BOLD); //粗體  
10:  //設定儲存格格式  
11:  CellStyle styleRow1 = workbook.createCellStyle();  
12:  styleRow1.setFillForegroundColor(HSSFColor.GREEN.index);//填滿顏色  
13:  styleRow1.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);  
14:  styleRow1.setFont(font);//設定字體  
15:  styleRow1.setAlignment(HSSFCellStyle.ALIGN_CENTER);//水平置中  
16:  styleRow1.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//垂直置中  
17:  //設定框線  
18:  styleRow1.setBorderBottom((short)1);  
19:  styleRow1.setBorderTop((short)1);  
20:  styleRow1.setBorderLeft((short)1);  
21:  styleRow1.setBorderRight((short)1);  
22:  styleRow1.setWrapText(true);//自動換行  
23:  Cell cell = row1.createCell(0);//建立儲存格  
24:  cell.setCellStyle(styleRow1);//套用格式  
25:  cell.setCellValue(CELLTEXT);//設定內容  
26:  sheet.autoSizeColumn(0);//自動調整欄位寬度  
27:  //儲存檔案  
28:  FileOutputStream fileOut = new FileOutputStream(tempFile);  
29:  workbook.write(fileOut);  
30:  fileOut.close();  

How do I do redirect in AngularJS

Question
I would like to do window.location (JavaScript way) to redirect page.
How do I do in AngularJS?

Answer
You can use $window.location.href=[your link]

Example
According to the system analysis specification, system should redirect to specific page based on the option which chose by user.


1:  <div class="form-group col-sm-5">  
2:            <label class="control-label">選擇查詢報表 :</label>  
3:            <select class="form-control" data-ng-change="update()"  
4:                id="reportName" name="reportName"   
5:                data-ng-model="model.reportName" >  
6:              <option value="" >請選擇</option>  
7:              <option value="ETS401R">收支狀況日報表 (總表)</option>  
8:              <option value="ETS402R">收支狀況日報表 (支出明細表)</option>  
9:              <option value="ETS403R">收支狀況日報表 (支入明細表)</option>  
10:              <option value="ETS407R">年度國庫現金餘額表</option>  
11:            </select>  
12:           </div>  

We can define $window.location.href in js file.
1:  $scope.update = function(){  
2:        var reportName = $scope.model.reportName;  
3:        $window.location.href = '/nss/'+reportName+'/';  
4:      };  

How to fix com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException

Exception
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "reportName" (class gov.nta.ets.web.dto.Ets405rFormBean), not marked as ignorable (4 known properties: , "accYy", "type", "data", "funcId"])
 at [Source: java.io.ByteArrayInputStream@57a9b75a; line: 4, column: 18] (through reference chain: gov.nta.ets.web.dto.Ets405rFormBean["reportName"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:79) [jackson-databind-2.2.3.jar:]

Root Cause
This exception means that there are get and set methods of an object in your class and Jackson is unable to figure out during serialization and deserialization process.

Solution
Apply @JsonIgnore on getter method, i.e. getReportName
1:   /**  
2:     * Gets the report name.  
3:     *   
4:     * @return the reportName  
5:     */  
6:    @JsonIgnore  
7:    public String getReportName() {  
8:      return reportName;  
9:    }  

2013/10/17

Windows 無法自動偵測此網路的proxy設定


今天在公司,突然電腦無法上網,出現 "Windows 無法自動偵測此網路的proxy設定 (Windows could not automatically detect this network's proxy settings)"此錯誤訊息
檢查了網路孔沒問題、ip相關設定沒問題,也重開機了,還是無法上網

最後 Google 到兩個指令,依照以下四步驟就可以解決了(只是還是不知道原因是什麼):

  1. 打開命令提示字元 (open command line)
  2. 執行 netsh winsock reset (execute netsh winsock reset)
  3. 執行 netsh int ipv4 reset (execute netsh int ipv4 reset)
  4. 重開機 (reboot)


Reference:http://answers.microsoft.com/en-us/windows/forum/windows_7-networking/windows-could-not-automatically-detect-this/d243dea1-d1c8-4c4a-ba96-2b49bc9bab1a

2013/09/24

How to Change User in TortoiseSVN

Question
When I authenticate with a Subversion server, the username and password are cached locally so I don't have to keep entering them each time. But I would like to clear username and password data for some reasons, how do I do?

Answer
1. Right Click --> Settings

2. Choose "Saved Data", then click "Clear" button which besides Authentication data. It clear all authentication data, then you can enter another username and password.


2013/09/03

SP2-0734: unknown command beginning "alter tab..." - rest of line ignored.

Problem
I would like to execute a SQL script file with simple alter table script  as follows:

But it showed this error message:
1:  SQL*Plus: Release 11.2.0.2.0 Production on Tue Sep 3 08:03:36 2013  
2:  Copyright (c) 1982, 2010, Oracle. All rights reserved.  
3:  Connected to:  
4:  Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit Production  
5:  With the Partitioning, Oracle Label Security and Oracle Database Vault options  
6:  SP2-0734: unknown command beginning "alter tab..." - rest of line ignored.  
7:  SQL> Disconnected from Oracle Database 11g Enterprise Edition Release 11.2.0.2.0  
8:  - 64bit Production  


With the Partitioning, Oracle Label Security and Oracle Database Vault options

Root Cause
It resulted from the SQL file has BOM character:
Solution
What you need to do is to convert the encoding of SQL file to UTF-8 without BOM and execute again, then this problem will be resolved.

2013/08/28

Utilize ime-mode property to Control Input Method Editor

Problem
Customer asked to control textfield1 can only fill in alphanumeric, textfield2 can fill in alphanumeric and Tranditional Chinese

Solution
We can use ime-mode CSS property to controls the state of the input method editor for text fields.

The default value of ime-mode property is auto, it means "no change is made to the current input method editor state".

If you would like to disable input method editor to allow for numbers and alphabets only, set the property of ime-mode property to "disabled".


If you would like to activate input method editor to allow for numbers, alphabets, and Tranditional Chinese, set the property of ime-mode property to "active".


Demo


Pay attention to some properties do not be supported by specific browser.

Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/ime-mode

2013/07/24

ORA-01452: cannot CREATE UNIQUE INDEX;

Problem
As DBA rebuilt index, it produced error report as following:

Root Cause
Based on the NIGT021 schema, it only has one primary key. Therefore, it may has abnormal and duplicate data in NIGT021.

Solution
1. Figure out abnormal data
2. Delete abnormal data
3. rebuild index

Here is the SQL statement template to find duplicate value in this table
   select column_name, count(column_name)
   from [table]
   group by column_name
   having count (column_name) > 1;
Here is an example to apply to NIGT021
   select pnsh_tp, count(pnsh_tp)
   from nigt021
   group by pnsh_tp
   having count (pnsh_tp) > 1;

2013/06/22

2013/06 高雄

85大樓


俯瞰高雄港


愛河夜景


駁二特區

2013/05/24

Utilize SVN relocate to change repository's root URL

Problem
Administrator changed the location of repository for some reason. The content of the repository doesn't change, but the repository's root URL does.
If we utilized old URL, it will show failed error message as bellows:

Solution
We can use "SVN relocate" to change the repository's root URL.
Step1. Right Click --> TortoiseSVN -->Relocate

Step2. Change the repository's root URL, and click OK

Then you will get successful dialog as bellows:

Finally, we do SVN update to test if it works or not


2013/05/01

2013/05 Singapore


Singapore flyer

Singapore flyer


Singapore Foods

Singapore早餐必備兩顆生雞蛋

The Jewelry Box

Bird view from Jewelry Box

Universal Studio

S.E.A Aquarium

Singapore Zoo

Singapore Botanic Gardens

Singapore 夜景

Singapore 夜景

Singapore 夜景

2013/04/23

The Software Craftsman


Top 10 Traits of a Rockstar Software Engineer

  1. Loves To Code
  2. Gets Things Done
  3. Continuously Refactors Code
  4. Uses Design Patterns
  5. Writes Tests
  6. Leverages Existing Code
  7. Focuses on Usability
  8. Writes Maintainable Code
  9. Can Code in Any Language
  10. Knows Basic Computer Science

2013/04/15

Test a string for a numeric value


Environment 
Oracle 11g

Problem

I write wrong value into specific column, it should accept numeric only.


Solution

To test a string for numeric characters, you could use a combination of the length function, trim function, and translate function built into Oracle.

You can use the following command:




This function will return a null value if vio_yr is numeric. It will return a value "greater than 0" if vio_yr contains any non-numeric characters.


Reference: http://www.techonthenet.com/oracle/questions/isnumeric.php

2013/04/09

ALL_TAB_COLUMNS Usage

Environment 
Oracle 11g 

Problem 

Customer requests to update ACS_NO this column length from 8 to 10. But I have more than 100 tables in my system, how do I quickly get the table which have ACS_NO column? 

Solution 

Making good use of ALL_TAB_COLUMNS. ALL_TAB_COLUMNS provides by Oracle, it describes the columns of the tables, views, and clusters accessible to the current user. 

We can use this SQL statement to find out the result:



See....we can get the result easily, and start to author alter table script.



Reference : http://docs.oracle.com/cd/B19306_01/server.102/b14237/statviews_2094.htm

2013/03/08

什麼是“對用戶友好”



當我提到一個工具“對用戶不友好”(user-unfriendly)的時候,我總是被人“鄙視”。難道這就叫“以其人之道還治其人之身”?想當年有人對我抱怨 Linux 或者 TeX 對用戶不友好的時候,我貌似也差不多的態度吧。現在當我指出 TeX 的各種缺點,提出新的解決方案的時候,往往會有美國同學眼角一抬,說:“菜鳥們抱怨工具不好用,那是因為他們不會用。LaTeX 是‘所想即所得’,所以不像 Word 之類的上手。”

殊不知他面前這個“菜鳥”,其實早已把 TeX 的配置搞得滾瓜爛熟,把 TeXbook 翻來覆去看了兩遍,"double bend" 的習題都全部完成,可以用 TeX 的語言來寫巨集包。而他被叫做“菜鳥”,這是一個非常有趣的問題。所以現在拋開個人感情不談,我們來探討一下這種“鄙視”現象產生的原因,以及什麼叫做“對用戶友好”。

首先我們從心理的角度來分析一下為什麼有人對這種“對用戶不友好”的事實視而不見,而稱抱怨的用戶為“菜鳥”。這個似乎很明顯,答案是“優越感”。如果每個人都會做一件事情,如何能體現出我的超群智力?所以我就是要專門選擇那種最難用,最晦澀,最顯得高深的東西,把它折騰會。這樣我就可以被稱為“高手”,就可以傲視群雄。我不得不承認,我以前也有類似的思想。從上本科以來我就一直在想,同樣都會寫程式,是什麼讓電腦系的學生與非電腦系的學生有所不同?經過多年之後的今天,我終於得到了答案(以後再告訴你)。可是在多年以前,我犯了跟很多人一樣的錯誤:把“難度”與“智力”或者“專業程度”相等同。但是其實,一個人會用難用的工具,並不等於他智力超群或者更加專業。

可惜的是,我發現世界上有非常少的人明白這個道理。在大學裡,公司裡,彰顯自己對難用的工具的掌握程度的人比比皆是。這不只是對於電腦系統,這也針對數學以及邏輯等抽象的學科。經常聽人很自豪的說:“我準備用XX邏輯設計一個公理化的系統……”可是這些人其實只知道這個邏輯的皮毛,他們會用這個邏輯,卻不知道它裡面所有含混晦澀的規則都可以用更簡單更直觀的方法推導出來。

愛因斯坦說:“Any intelligent fool can make things bigger and more complex... It takes a touch of genius - and a lot of courage to move in the opposite direction.”我現在深深的體會到這句話的道理。想要簡化一個東西,讓它更“好用”,你確實需要很大的勇氣。而且你必須故意的忽略這個東西的一些細節。但是由於你的身邊都是不理解這個道理的人,他們會把你當成菜鳥或者白癡。即使你成功了,可能也很難說服他們去嘗試這個簡化後的東西。

那麼現在我們來談一下什麼是“對用戶友好”。如何定義“對用戶友好”?如何精確的判斷一個東西是否對用戶友好?我覺得這是一個現在仍然非常模糊的概念,但是程式語言的設計思想,特別是其中的類型理論(type theory)可以比較好的解釋它。我們可以把機器和人看作同一個系統:

  1. 這個系統有多個模組,包括機器模組和人類別模組。
  2. 機器模組之間的介面使用通常的程式介面。
  3. 人機交互的介面就是機器模組和人類別模組之間的介面。
  4. 每個介面必須提供一定的抽象,用於防止使用者得到它不該知道的細節。這個使用者可能是機器模組,也可能是人類別模組。
  5. 抽象使得系統具有可擴展性。因為只要介面不變,模組改動之後,它的使用者完全不用修改。

在機器的各個模組間,抽象表現為函數或者方法的類型(type),程式的模組(module)定義,作業系統的系統調用(system call),等等。但是它們的本質都是一樣的:他們告訴使用者“你能用我來幹什麼”。很多程式師都會注意到這些機器介面的抽象,讓使用者儘量少的接觸到實現細節。可是他們卻往往忽視了人和機器之間的介面。也許他們沒有忽視它,但是他們卻用非常不一樣的設計思想來考慮這個問題。他們沒有真正把人當成這個系統的一部分,沒有像對待其它機器模組一樣,提供具有良好抽象的介面給人。他們貌似覺得人應該可以多做一些事情,所以把紛繁複雜的程式內部結構暴露給人(包括他們自己)。所以人對“我能用這個程式幹什麼”這個問題總是很糊塗。當程式被修改之後,還經常需要讓人的操作發生改變,所以這個系統對於人的可擴展性就差。通常程式師都考慮到機器各介面之間的擴展性,卻沒有考慮到機器與人之間介面的可擴展性。

舉個例子。很多 Unix 程式都有設定檔,它們也設置環境變數,它們還有命令列參數。這樣每個用戶都得知道設定檔的名字,位置和格式,環境變數的名字以及意義,命令列參數的意義。一個程式還好,如果有很多程式,每個程式都在不同的位置放置不同名字的設定檔,每個設定檔的格式都不一樣,這些配置會把人給搞糊塗。經常出現程式說找不到設定檔,看手冊吧,手冊說設定檔的位置是某某環境變數 FOO 決定的。改了環境變數卻發現沒有解決問題。沒辦法,只好上論壇問,終於發現設定檔起作用當且僅當在同一個目錄裡沒有一個叫 ".bar" 的文件。好不容易記住了這條規則,這個程式升級之後,又把規則給改了,所以這個用戶又繼續琢磨,繼續上論壇,如此反復。也許這就叫做“折騰”?他何時才能幹自己的事情?

TeX 系統的配置就更為麻煩。成千上萬個小檔,很少有人理解 kpathsea 的結構和用法,折騰好久才會明白。但是其實它只是解決一個非常微不足道的問題。TeX 的語言也有很大問題,使得擴展起來非常困難。這個以後再講。

一個良好的介面不應該是這樣的。它給予使用者的介面,應該只有一些簡單的設定。使用者應該用同樣的方法來設置所有程式的所有參數,因為它們只不過是一個從變數到值的映射(map)。至於系統要在什麼地方存儲這些設定,如何找到它們,具體的格式,用戶根本不應該知道。這跟高階語言的運行時系統(runtime system)的記憶體管理是一個道理。程式請求建立一個物件,系統收到指令後分配一塊記憶體,進行初始化,然後把物件的引用(reference)返回給程式。程式並不知道物件存在於記憶體的哪個位置,而且不應該知道。程式不應該使用物件的位址來進行運算。

所以我們看到,“對用戶不友好”的背後,其實是程式設計的不合理使得它們缺少抽象,而不是用戶的問題。這種對用戶不友好的現象在 WindowsMaciPhone, Android 裡也普遍存在。比如幾乎所有 iPhone 用戶都被洗腦的一個錯誤是“iPhone 只需要一個按鈕”。一個按鈕其實是不夠的。還有就是像 Photoshop, Illustrator, Flash 之類的軟體的功能表介面,其實把使用者需要的功能和設置給掩藏了起來,分類也經常出現不合理現象,讓他們很難找到這些功能。

如何對用戶更加友好,是一兩句話說不清楚的事情。所以這裡只粗略說一下我想到過的要點:

  1. 統一:隨時注意,人是一個統一的系統的一部分,而不是什麼古怪的神物。基本上可以把人想像成一個程式模組。
  2. 抽象:最大限度的掩蓋程式內部的實現,儘量不讓人知道他不必要知道的東西。不願意暴露給其它程式模組的細節,也不要暴露給人。“機所不欲,勿施於人”。
  3. 充要:提供給人充分而必要(不多於)的機制來完成人想完成的任務。
  4. 正交:機制之間應該儘量減少冗餘和重疊,保持正交(orthogonal)
  5. 組合:機制之間應該可以組合(compose),儘量使得幹同一件事情只有一種組合。
  6. 理性:並不是所有人想要的功能都是應該有的,他們經常欺騙自己,要搞清楚那些是他們真正需要的功能。
  7. 通道:人的輸入輸出包括5種感官,雖然通常電腦只與人通過視覺和聽覺交互。
  8. 直覺:人是靠直覺和模型(model)思考的,給人的資訊不管是符號還是圖形,應該容易在人腦中建立起直觀的模型,這樣人才能高效的操作它們。
  9. 上下文:人腦的“快取記憶體”的容量是很小的。試試你能同時想起7個人的名字嗎?所以在任一特定時刻,應該只提供與當前被關注物件相關的操作,而不是提供所有情況下的所有操作供人選擇。上下文功能表和依據上下文的鍵盤操作提示,貌似不錯的主意。