URI và REST

Thêm một tí về REST.

Hiểu HTTP

Đúng ra bài viết  là về HTTP, vì REST không bắt buộc dùng HTTP. Tuy vậy những vấn đề về URI chủ yếu được để ý khi mọi người “viết REST”, còn lúc làm web bình thường thì URI kiểu nào cũng không bị để ý. Trong khi đó những khái niệm về URI đều từ HTTP mà ra cả, không hề liên quan tới REST.

Về HTTP thì đọc RFC là tốt nhất. RFC tương đối ngắn gọn, súc tích, dạng plain text rõ ràng, nhưng lại hay bị bỏ qua. Dưới đây tôi cũng chủ yếu tóm lược lại và link tới những tài liệu quan trọng để bạn có thể dễ dàng tìm đọc và hiểu đúng, chứ tôi viết thì cũng có thể sai sót.

POST, PUT, tạo và cập nhật

Thường thì hai loại method này được hiểu là POST dùng cho tạo, PUT dùng cho cập nhật. Điều này không hoàn toàn đúng. Có hai chỗ bạn có thể đọc thêm, một là RFC 2615, hai là trong quyển RESTful web services. RFC là chính xác nhất. Dịch thì cũng hơi khó, nhưng ý tưởng cơ bản là như sau:

  • POST là để cung cấp thành phần phụ cho một resoure xác định bởi URI, theo kiểu thêm một bài viết vào blog, hay thêm bình luận vào một bài viết (bài viết là resource), v.v. POST cũng có thể dùng để đẩy dữ liệu tới một quá trình xử lí nào đó. Kết quả của POST có thể không phải là một resource được xác định bởi một URI, chẳng hạn như khi bạn xem bình luận của một bài viết không phải là resource. Tất nhiên kết quả của POST cũng có thể là một resource với URI xác định. Điều này thường dùng trong viết REST khi chúng ta tạo một đơn vị resource mới trong một tập các resource cùng loại, ví dụ POST /article để tạo ra một bài báo mới ở article/{id}. Trong trường hợp có resource tạo ra như vậy thì phải trả về 201, chứ không phải 200 hay 204.
  • PUT là để đưa dữ liệu tới một resource xác định bởi URI. Tùy thuộc vào resource đã tồn tại hay chưa mà hành động này là create hay cập nhật. Chẳng hạn PUT /user/nmh để tạo hay cập nhật user nmh, chứ server không thể tạo user abc để trả về được. Chính vì vậy là không thể nói là POST dùng cho tạo, PUT dùng cho cập nhật.

Tóm lại có thể nói đơn giản là POST có thể dùng cho những việc hoàn toàn không liên quan đến tạo resource. Trong trường hợp POST dùng để tạo resource, thì resource sẽ được tạo ra chưa có URI xác định lúc gửi request. Với PUT thì resource sẽ được tạo ra hay cập nhật luôn có URI xác định ngay lúc gửi request.

Path variable, request parameter, và matrix URI

Dùng path variable hay request parameter? Câu trả lời là thế nào cũng được. Nhưng trong quyển RESTful web services có một đề xuất dựa trên RFC 3986. Dùng path variable nếu bạn muốn thể hiện sự phân cấp, ví dụ node/{id}/comment thay cho comment?node={id}. Trong trường hợp các biến không thể hiện sự phân cấp, hoặc có thể có có thể không, thì nên dùng request parameter.

Matrix URI thì là một thứ hơi khó hiểu và ít được nhắc đến. Về những ví dụ cụ thể có thể xem ở các web framework hỗ trợ nó, như Spring chẳng hạn. Về lí do ra đời thì có thể hiểu đơn giản là đâu đó có thể có nhu cầu dùng một associate array dưới dạng path variable, trong đó một số element có thể có hoặc không. Ví dụ (từ Restful web service) bạn muốn cho kinh độ và vĩ độ vào path variable để tạo URI thể hiện một điểm nào đó trên bản đồ, trong khi đó kinh độ và vĩ độ ngang hàng nhau nên không thể xếp một trong yếu tố đứng trước hoặc sau. Trong tình huống này bạn cần dùng matrix URI.

Advertisements

Cứ phải là REST sao

RESTful web services là xu hướng mới của thời đại, trong nước và ngoài nước. Trong khi người tạo ra REST và những nhà REST lí thuyết thì số vẫn đang bận rộn chỉ ra những API đội lốt RESTful thì những bộ API dạng REST vẫn liên tục xuất hiện. REST gần như là “chuẩn”, tức là nói tới việc viết API là mọi người sẽ nghĩ “viết REST”.

REST cũng tương tự Agile. Chúng có những lí thuyết riêng. Những lí thuyết này làm nên sức mạnh của chúng. Nhưng bản thân chúng thì lại ẩn sau vẻ ngoài dễ hiểu và đơn giản. Kết quả là những lí thuyết này thường không được tìm hiểu đầy đủ. Vậy nên những thứ không có quy tắc gì thì được gắn vào chúng. Viết web services dùng HTTP là viết REST. Làm phần mềm mà không có tính toán suy nghĩ gì thì tức là làm Agile.

Điều cần nói đầu tiên về REST là sự đơn giản giả tạo mà nó thể hiện. Điển hình là REST có vẻ gần gũi và tương đồng với HTTP. Người thiết kế REST quá chăm lo cho việc thiết kế media type, cách thể hiện tài nguyên (JSON hay XML, có field này hay không có field kia, v.v). Vấn đề thứ nhì là họ quá tập trung vào thiết kế URI cho đẹp, cho RESTful. Trong khi đó thì REST không quy định gì về URI hay JSON hay XML cả.

Trong khi đó hai thứ quan trọng mà người làm REST lại thường không để ý. Thứ nhất là URI của resources. URI tuyệt đối, chứ không phải ID như trong database, là cách mà REST dùng để định vị một resource. Thứ hai là HATEOAS, tức là hành vi của REST client của bạn trên mỗi resource phụ thuộc vào những hypermedia trong mỗi resource. Hypermedia là xương sống cho sự mềm dẻo và linh động và REST mang lại. Đây là những khái niệm không hiển nhiên nên đòi hỏi phải có sự tìm hiểu kĩ càng.

(Có một ví dụ kiểu tương tự mà bạn có thể sẽ hình dung đến khi tìm hiểu những khái niệm này. Khi bạn vào một tờ báo, chẳng hạn vnexpress.net, bạn sẽ thấy một danh sách tóm tắt các bài mới. Bạn đọc bài viết đầy đủ bằng cách bấm vào liên kết. Bạn không cần quan tâm ID của bài báo là gì, hay URI pattern của trang web là như thế nào. Bạn chỉ cần biết một điểm cuối duy nhất là vnexpress, từ đó bạn có thể đi khắp nơi. Khi bạn đọc một bài báo, bạn có thể bình luận hoặc không bình luận được, tùy vào sự hiện diện của form bình luận. Bạn không cần nhận về một field nào đó thể hiện điều này, hay phải thử bình luận rồi nhận về một status nào đó như 200, 400, 404, 500, v.v. Không chỉ bình luận, tất cả mọi thứ như in, chia sẻ lên mạng xã hội, v.v bạn biết là bạn làm được hay không làm được dựa vào sự hiện diện của nó ngay tại trang báo. Thứ bạn cần biết là cách nhận dạng những thành phần này, như biểu tượng máy in, biểu tượng facebook, v.v.)

Điều cần nói thứ hai, REST chỉ là công cụ, không phải đích đến. Và đến cuối ngày, REST không phải là thứ quan trọng. Tôi từng phải viết client để consume web services của một số phần mềm như BugZilla hay Jira. Điều mà tôi nhận ra, là API không bao giờ đủ. Có những nhu cầu hết sức bình thường và đơn giản thì API lại không có. Điều thứ hai tôi nhận ra là tôi cần làm dễ, làm nhanh chứ không cần cái “trình độ” của một API. Thế nên REST với tôi không tuyệt vời bằng một thư viện client, nơi tôi chỉ cần gọi hàm và nhận kết quả ngay trên ngôn ngữ lập trình mà tôi sử dụng. Vì vậy API tốt là API đầy đủ, dễ dùng, và có tài liệu tốt.

Và bây giờ, nếu bạn muốn tìm hiểu/tìm hiểu lại về REST thì đây là những thứ đáng đọc:

Luận án đã sinh ra REST

RESTful web services

REST in Practice: Hypermedia and Systems Architecture—cuốn này viết không chính xác lắm về REST, nhưng lại có đi sâu vào một số phần của HTTP mà chúng ta sẽ hay dùng khi viết REST over HTTP. Lâu lâu mới có một cuốn O’Reilly mà bìa không in hình thú vật.

Practical API Design: Confessions of a Java Framework Architect—vì phải biết xây dựng API tốt thì mới biết cách ứng dụng REST cho hợp lí

– Và tìm hiểu những bộ web services tốt nhất

Kiến trúc sư phần mềm

Chỉ là một vài thứ tôi mong chờ ở một kiến trúc sư phần mềm (KTSPM).

KTSPM, trước hết, phải thấy xa hơn những gì mà một kĩ sư bình thường thấy và thực sự hiểu rõ về hệ thống. Có một chuyện khá là kì cục là các dự án hay bắt đầu bằng cách cho KTSPM viết ngày viết đêm ra một cái gọi là framework (framework ở đâu ra mà lắm thế), sau đó lui ra, và các kĩ sư bình thường sẽ nhào vô viết thêm tính năng, và chúng ta có một phần mềm. Sau đó có thể có chút trục trặc, chậm chạp thì các KSTPM này lại quay trở lại để dọn rác.

Nhưng hãy để tôi kể câu chuyện về dự án mà tôi đang làm đã. Đầu tiên chúng tôi có một KTSPM. Người này chuẩn bị mọi thứ, từ hệ thống build, database, Spring, Hibernate này kia, và cũng viết một ít làm mẫu. Và vì chỉ là làm giúp nên rời dự án nhanh chóng. Sau đó chúng tôi bỏ một thời gian dài loay hoay vừa muốn giữ cái nền này vừa muốn đập bỏ. Khi yêu cầu phần mềm thay đổi và vấn đề nảy sinh thì cái nền rõ ràng là không phù hợp. Nhưng cái nền là do KTS viết ra mà. Sự thiếu dứt khoát khiến cho mọi thứ trong hệ thống mà chúng tôi xây dựng vẫn còn khá là vá víu và không linh hoạt. Trong giai đoạn này có hai người nữa, một người là KTS và một người gần được KTS. Nhưng họ cũng chỉ viết mã cho một vài phần. Có thể là những phần khó hơn, nhưng nó chỉ là một phần nhỏ của toàn bộ bức tranh. Và nó cũng chứa những sai lầm tiềm ẩn như những nhiều phần khác. Và nó cũng có thể được hoàn thành bởi những kĩ sư khác trong dự án, mặc dù có thể là phải có một chút hướng dẫn. Nói tóm lại, những KTSPM đang làm những việc mà kĩ sư làm, chỉ là (có thể) ở một trình độ tốt hơn.

Vậy những sai lầm mà dự án tôi gặp phải và mắc kẹt là gì. Có thể kể ra những thứ khá cơ bản, như bỏ qua các bước an ninh cơ bản (chống XSS, CSRF), không xây dựng một dạng protocol (hay interface) thống nhất cho các request, bỏ lơ vấn đề cache và performance (cache phía server, cache phía client), không có hệ thống build tự động, không có phương pháp chung để xử lí vấn đề cross-browser, v.v. Đây là những vấn đề có tác động quan trọng tới thiết kế của phần mềm, nên không thể giải quyết một sớm một chiều được. Dĩ nhiên, phần mềm là một hệ thống phức tạp mà khi đã đầy đủ các bộ phận thì việc thay đổi sẽ tốn kém. Vì vậy, không thể ngày hôm nay nghĩ về vấn đề này thì xây dựng nó một kiểu, rồi mai gặp hay chợt nghĩ về vấn đề khác thì sửa đối nó theo kiểu khác.

Tất nhiên KTS vẫn nên là người viết những thành phần quan trọng, để góp phần tạo ra một phần mềm có chất lượng tốt. Nhưng kĩ sư giỏi thì cũng có thể viết được những phần khó, họ cũng có thể đọc sách, tìm hiểu, suy nghĩ. Vậy thì giá trị đặc biệt của KTSPM nằm ở chỗ nào, tại sao họ đứng trên một cấp, và lương cao hơn? Theo tôi nghĩ đấy là vì kĩ sư giỏi là những người chỉ giải quyết được những vấn đề cục bộ, riêng rẽ, chứ không thể quan sát hệ thống một cách tổng thể. KTSPM phải là người định hướng. Họ phải liệt kê ra được những yêu cầu, vấn đề, hoặc thử thách mà dự án sẽ đối mặt, trước mắt và tương lai. Họ phải xây dựng được những quy tắc chung để kĩ sư có thể tuân theo nhằm hạn chế sai sót. Họ phải biết phân bổ nhân lực cho hợp lí với từng phần việc. Và họ phải nắm tình trạng phát triển hiện tại của phần mềm để nhận ra những bài toán hay cải tiến cần được thực hiện một cách thực sự đúng đắn và dứt khoát.

(Bạn nghỉ ngơi một xíu trả lời câu này xem. Nếu bối rối thì chắc chưa phải KSTPM rồi.)

Không chỉ nhìn xa hơn những kĩ sư, KTSPM còn phải cố gắng nhìn xa hơn những gì ông chủ nghĩ. Không phải là về kinh doanh, mà là về chức năng của phần mềm. Họ phải biết trừu tượng hóa các yêu cầu cụ thể để có cái nhìn bao quát cho chức năng mà trong đó yêu cầu của những người phân tích nghiệp vụ chỉ là một lựa chọn, một hướng hiện thực cụ thể. Ý định của con người luôn thay đổi rất nhanh, và yêu cầu cũng vậy, nên không có lí do gì lập trình viên đặt niềm tin là những thứ họ phải làm hiện tại sẽ giống vậy mãi mãi. Chuẩn bị tốt cho những lựa chọn chưa xảy ra sẽ giúp phần mềm linh động với những nhu cầu, đòi hỏi từ thị trường. Và điều quan trọng là thấy trước thay đổi của hệ thống là nhiệm vụ của KSTPM, không cần ai khác phải nói ra, và họ sẽ không thể đổ lỗi đi đâu đó như người phân tích nghiệp vụ, hay người dùng nếu không thực hiện tốt nhiệm vụ này.

Chẳng hạn, hãy nói việc hỗ trợ bookmark cho những web application dùng AJAX nhiều. Nếu một trang web mà trạng thái của nó, như đang mở tab nào, đang hiện nội dung gì, không được thể hiện trên địa chỉ thì người dùng sẽ không thể bookmark để quay lại trạng thái đó một cách nhanh chóng được. Tuy nhiên, yêu cầu phần mềm thường quên mất chuyện này, trong khi nếu không được thiết kế ngay từ đầu thì việc chỉnh sửa thiết kế của ứng dụng để hỗ trợ chức năng này sẽ khá rắc rối. Do nên KTSPM phải biết tự thấy trước và đáp ứng chức năng ngay khi chưa có yêu cầu.

Có thể tóm lại là KSTPM trước hết phải biết nhìn xa, nhìn trừu tượng, không chỉ xây nên framework mà còn những tiêu chuẩn, quy định, và đường lối cho dự án.

Điều thứ ba, liên quan đến cách mà một KTSPM làm việc với nhân viên (và sẽ không viết ở đây).

(Nhưng bạn có thể đọc bài này để tìm kiếm một vài ý tưởng.)

Some facts about Spring’s scoped proxy

I just want to give some more details about some aspects of Spring’s scoped proxy. Please take a look at Spring reference first.

Proxy is not always necessary

Spring creates proxies only when it needs to attach behaviors to your methods, for example transactional management, or your own AOP advices. If a class uses Spring only for dependency injection, it will not be proxied. Also, in this case, you can wire a property whose type is a concrete class instead of an interface. The following code works, without any help from JDK proxy or CGLIB proxy:

@Autowired Interface property;
@Autowired InterfaceImplement property;

Proxy can be avoided using byte code weaving, but it is not covered here.

How Spring creates scoped proxy

A scoped proxy can be created using JDK dynamic proxy or CGLIB.

Let’s say we have the interface Vehicle and it’s implementation—class Motor.

interface Vehicle { 
    void start();
}
class Motor implements Vehicle {
    void start() { }
}

Using JDK dynamic proxy

Spring will create a class, say JdkProxyBasedVehicle that also implements Vehicle.

class JdkProxyBasedVehicle implements Vehicle {
    void start() {
        Vehicle target = getBeanOfTypeMotor();
        target.start();
    }
}

In this case JdkProxyBasedVehicle is not assignable from Motor. Once the class Motor is proxied this way, the following code will not work:

@Autowired Motor motor;

Using CGLIB

Spring will create a subclass of Motor, say CglibBasedVehicle.

class CglibBasedVehicle extends Motor {
    CglibBasedVehicle() {
        super();
    }
    void start() {
        Vehicle target = getBeanOfTypeMotor();
        target.start();
     }
}

As a consequence, there are some side effects:

  • CglibBasedVehicle is assignable from Motor, and this code will work:
    @Autowired Motor motor;
  • The method start() can be overridden only when it is not marked as final.
  • The constructor of Motor will be executed twice, by getBeanOfTypeMotor() and CglibBasedVehicle’s constructor. CglibBasedVehicle’s constructor must delegate to Motor’s constructor because it extends Motor, so its internal states must also be initialized consistently like the parent class. The class Motor might has some final methods, and these methods would need to access these internal states when being called from a CglibBasedVehicle instance.

Note that regardless of how Spring create scoped proxy, it is the target (the bean of type Motor) that gets injected with dependencies, not the proxy instance. Therefore, inside CglibBasedVehicle instances all @Autowired properties inherited from Motor will not be injected. Later in this post we will see why it might get you into a trouble.

The word "subclass" might make you wonder why Spring does not inject directly into CglibBasedVehicle instances, and use these instances as Motor beans.

class CglibBasedVehicle extends Motor {
    void start() {
        super.start(); // actually this method is not overridden.
    }
}

Looks good. The constructor will not be executed twice. But then scoped proxies would be useless.

What scoped proxy does?

It helps wire differently-scoped beans together. A typical usage is to inject a shorter-lived scoped bean (for example a request scoped bean) to a longer-lived scoped bean (for example a singleton scoped bean). In the above example, the magic is inside the method getBeanOfTypeMotor(). Based on your configuration, the proxy will decide whether to look up the bean inside the Spring container, or create a fresh Motor instance.

A small idea with scoped proxy

I do not have any chance to verify, but in Spring it should be possible to build a custom scope to add thread safety to non thread-safe components. In our example, if the class Motor is not thread-safe, getBeanOfTypeMotor() can return different beans for different threads.

Be aware of using final method and dependency injection with CGLIB based proxies

So let’s tailor our previous example.

interface Vehicle { 
    void start();
}
class Motor implements Vehicle {
    @Autowired Engine engine;
    void start() {
        engine.start();
    }
}
class CglibBasedVehicle extends Motor {
    // engine is null, because dependency injection is not done inside proxy
    void start() {
        Vehicle target = getBeanOfTypeMotor();
        target.start();
    }
}

The proxy’s start() works fine because target.engine is injected. But what will happen if start() is marked as final?

class Motor implements Vehicle {
    @Autowired Engine engine;
    final void start() {
        engine.start();
    }
}
class CglibBasedVehicle extends Motor {
    // engine is null, because dependency injection is not done inside proxy
    // cannot overridden start();
}

In this case, the proxy’s start() will act the same way as Motor, but its field engine is null, and we have a NullPointerException.

Since the overhead with both JDK proxy-based and CGLIB-based approaches is not a problem (http://blog.springsource.org/2007/07/19/debunking-myths-proxies-impact-performance/), and interfaces help inject mocks and stubs easily, I think if we use Spring we should use interfaces exclusively. For well-structured codebases it is truly an urban myth that interfaces make code bloat.

An easy way to enable SSL for Tomcat servers inside Eclipse

This should be easy, but Tomcat’s guideline is a little confusing.

First, create a keystore with JDK’s keytool. Remember to use the default password by not putting any password when being prompted. The default password should be “changeit”.

%JAVA_HOME%\bin\keytool -genkey -alias tomcat -keyalg RSA

The generated keystore should be in the user’s directory, e.g. C:\Users\username\.keystore.

Then go to view Project Explorer in Eclipse, look for your Tomcat server’s configuration file server.xml under the project Servers. Uncomment the following line:

 

I don’t know why, but Tomcat servers inside Eclipse use the JSSE implementation instead of APR implementation.

You can remap your server’s ports by double click on it in view Servers.

Now your Tomcat server is SSL-enabled.

UX cho người không chuyên

Tôi thấy UX là một vấn đề vừa bị xem nhẹ vừa bị thổi phồng. Xem nhẹ ở chỗ dường như ai cũng hiểu và lí luận về UX mà không cần tham khảo ý kiến xung quanh, ngay cả khi chưa từng biết hay tìm hiểu về những kiến thức liên quan. Thổi phồng là ở chỗ người ta quá coi trọng nó, chăm chút nó, tới nỗi tiêu tốn một phần không nhỏ thời gian cho những cuộc họp dài lê thê hay sửa đi sửa lại một chức năng nhỏ. Nếu dự án thiếu đi một người thực sự giỏi về UX để cầm trịch, thì những điều nói trên sẽ càng trầm trọng và đe dọa sự thành công của phần mềm.

Như vậy nên làm thế nào nhỉ? Tôi không biết, vì những gì tôi gặp phải chỉ là những kinh nghiệm rời rạc trong từng tình huống cụ thể, chưa đủ để tạo thành một tư tưởng gì đó thật đúng đắn. Bạn mà làm lập trình viên thì bạn cũng sẽ phải làm đi làm lại một chức năng cho UX nó tốt hơn thôi. Tuy nhiên, dưới con mắt lập trình viên, tôi nghĩ rằng với những nhóm nhỏ không chuyên về UX, thì điều quan trọng là làm cho phần mềm ổn định, logic, và linh động (hay nói cách khác là dẹp UX qua một bên).

Để có UX tốt thì trước hết phải chạy được đã, tức là phần mềm phải ổn định. Để phần mềm ổn định thì nhất định không được làm những thứ quá khó, đặc biệt là những thành phần trên giao diện web trong thời đại mà JavaScript là tất cả. Tôi biết là lập trình viên ham thử thách, và không có gì là không thể khi ta quyết tâm, v.v nhưng mà đôi khi phải nhìn nhận thực tế là chúng ta không đủ trình độ để làm mọi thứ, và nhìn ra thứ gì khó làm, không làm được, hoặc dễ gây lỗi cũng là một cách thể hiện trình độ. Với người viết yêu cầu cho phần mềm, cần nhìn ra bản chất của một yêu cầu để tìm ra cách thể hiện phù hợp và khả thi, nhất là tránh bị cuốn vào những hành vi tủn mủn trên giao diện mà quên mất chính flow bên dưới mới là thứ quyết định UX tốt hay không.

(Đấy là chưa kể thời gian cho việc phát triển một phần mềm luôn là có hạn, cho nên hệ quả là thời gian cho nhiều thứ quan trọng không kém như kiến trúc, unit test, build, deploy, v.v sẽ bị ăn bớt. Kiến trúc là thứ cần ổn định, mà một khi phần mềm đã cho dùng đại trà thì việc thay đổi nó sẽ khó khăn hơn. Còn yêu cầu về chức năng, UX, v.v thì luôn cần thử nghiệm và đổi mới, cho nên sẽ không hợp lí nếu tập trung công sức và thời gian vào nó trong giai đoạn đầu.)

(Nhưng ổn định không phải là không có gì. Bạn có nhận thấy vào buổi đầu của những ứng dụng web, người ta cố gắng nhái theo những phần mềm trên desktop, điển hình là các web mail. Kết quả là chúng ta có những ứng dụng đầy đủ chức năng, nhưng cồng kềnh, chậm chạp, nhiều lỗi. Về sau này, người ta lại có xu hướng tạo ra những ứng dụng cực kì đơn giản, rồi bổ sung dần tính năng song song với việc đảm bảo tính ổn định. Mọi chức năng đều cần được đưa vào đúng thời điểm.)

Nếu chúng ta không đủ trình độ hay dữ kiện để hiểu được người dùng mong đợi gì, thì hãy cố gắng làm do mọi chức năng logic nhất có thể. Việc không làm người dùng bất ngờ thì sách báo hay nói rồi, tôi cũng không dám ý kiến gì nhiều. Nhưng ít nhất yêu cầu chức năng không được làm lập trình viên bất ngờ. Tính logic sẽ giúp lập trình viên giữ code "ngăn nắp" và ít lỗi. Không nên để lập trình viên phải xoay sở với những sơ đồ trạng thái lòng vòng đầy những kịch bản tủn mủn và khó xảy ra. Chẳng hạn, những thứ mà lập trình viên có thể đoán đúng mà chưa cần đọc yêu cầu, hay người kiểm tra có thể nhớ được dễ dàng thì thường có logic tốt.

(Tôi định viết một vài ví dụ trong dự án tôi làm, nhưng có vẻ dài dòng không cần thiết. Có vẻ các dự án mới thường hay có xu hướng phức tạp hóa mọi flow vì sợ người dùng không hiểu, người dùng "ngu si", v.v. Cho nên người ta hay bỏ thời gian vào việc suy đoán và lựa chọn giùm đường đi cho người dùng, chặn cái này cái kia, thậm chí tìm cách bỏ bớt các thông báo cần thiết. Khi đã cho dùng thử và bắt đầu cảm thấy tự tin thì họ lại bắt đầu đơn giản hóa nhiều thứ. Tuy không có thẩm quyền can thiệp vào chuyện này,  lập trình viên nên sẵn sàng cho những thay đổi tới lui như vậy.)

Có một cách giúp phần mềm ổn định và logic là xây dựng một hệ thống tổng quát cho mọi tình huống, linh động, để người dùng muốn làm gì thì làm. Thay vì bắt người dùng hạnh phúc theo một cách nào đó, thì tốt nhất để họ tự "mưu cầu hạnh phúc". Chẳng hạn, chúng ta có custom fields hay custom flows trong các phần mềm như Jira, tùy chọn màu trong những phần mềm của Microsoft, add on trong các trình duyệt. Bên cạnh đó, undo (như trong Remember the milk) là một công cụ tuyệt vời để người dùng có thể yên tâm khám phá. Tất nhiên, nhìn ra những điều như vậy trong những ngày đầu xây dựng một phần mềm thực sự không dễ. Thường thì những ý tưởng khái quát sẽ nằm sau những giới hạn bí ẩn, không rõ lí do kiểu như người dùng chỉ được tạo một cái gì đó, liên kết một đối tượng kiểu này với đối tượng kiểu khác, không được nhập quá bao nhiêu đó thông tin. Cả người viết yêu cầu và lập trình viên phải nhìn ra các giới hạn ẩn trong yêu cầu của phần mềm để tìm kiếm những bản chất và khái niệm khái quát hơn đằng sau những yêu cầu đó.

(Tôi có nhớ đã đọc ở ít nhất một cuốn sách toán là khi không thể tìm lời giải cho một bài toán, hãy tìm lời giải cho bài toán tổng quát của nó. Nếu bạn vẫn nhớ những bài toán phổ thông thì sẽ thấy sự khái quát trước hết đến từ việc loại bỏ đi những giới hạn cụ thể, chẳng hạn từ (x+1)2 >= 4x ta có (x+y)2 >= 4xy, rồi (x+y)n >= (tôi quên rồi), hay (x1+…+xn)2 >= (tôi cũng quên rồi). Dĩ nhiên là đi xa hơn để nhìn ra giới hạn từ số n (lũy thừa n), hay phép cộng (+) thì phức tạp hơn từ 2 (bình phương), và cần những bộ óc siêu việt hơn.)

Cuối cùng, phải nhắc lại là UX là một thứ rất khó, và quyết định nhầm là chuyện hoàn toàn có thể xảy ra. Nhiệm vụ của lập trình viên là lập trình và lập trình thật tốt, vì vậy trong bất cứ tình huống nào, cũng cần đảm bảo phần mềm ở trong trạng thái vận hành được, và việc thay đổi yêu cầu chức năng, dù nhỏ hay lớn, đều có thể thực hiên trong một khoảng thời gian xác định được, và sẽ không làm mất đi khả năng vận hành của phần mềm.

Quản lí URI trong web application

Đây không phải là chuyện gì cao siêu quá về kĩ thuật, mà chỉ là có nghĩ tới hay không thôi. Khi làm việc với những MVC web framework thông thường như Spring hay Struts, nhất là khi web application sử dụng JavaScript và AJAX nhiều thì tập URI cần được quản lí và tổ chức một cách cẩn thận. ("MVC web framework thông thường" ở đây tôi dùng để chỉ các MVC framework mà bạn phải khai báo cụ thể Model, Controller, View và mapping giữa URI và Controller. Chú ý là những framework dạng component như JSF cũng có thể được xem là đi theo pattern MVC.)

Thông thường, URI được sử dụng ở phía client trong JavaScript hay view template dưới dạng string literal, có thể ghép nối một số parameter, kiểu như var url = 'delete/nodeId/' + nodeId. Vì URI chỉ là literal string, nên nói chung IDE sẽ không giúp gì nhiều cho chúng ta trong việc tìm kiếm hay báo lỗi. Khi đó chúng ta sẽ không dễ dàng tìm ra handler tương ứng với URI đó, và cũng khó tìm kiếm một cách nhanh chóng và chắc chắn những chỗ mà URI này được sử dụng. Ở dự án mà tôi đang tham gia, đôi khi tôi thấy những URI dạng /something/update/something/update2, chỉ vì lập trình viên không dám sửa handler có sẵn.  (Về lí thuyết, nếu mà cả phía client và server đều có unit test đầy đủ thì sửa URI cũng không quá rủi ro, nhưng không phải dự án nào cũng đủ tầm nhìn và tài nguyên để viết unit test tới mức độ này :D). Hệ quả là chúng ta sẽ có một tập URI lộn xộn, không theo một convention nào, và có thể trùng lặp về chức năng.

Về mặt người dùng, bạn có thoải mái không khi trả tiền và đưa dữ liệu lên một web application mà chỉ tập URI thôi đã lộn xộn rồi? (Tôi hỏi vậy thôi, chứ chắc là có, nhiều công ty dễ dãi một cách kì lạ khi đặt niềm tin vào mọi hệ thống mà họ sẽ thuê. Dù sao thì tôi chỉ là lập trình viên bình thường nên có khi chỉ nghĩ kiểu ếch ngồi đáy giếng.) Rõ ràng đây là một biểu hiện khó che giấu của một sự bất ổn trong quy trình phát triển, bất kể bạn được giới thiệu là họ áp dụng quy trình abcxyz gì đó.

Để quản lí được URI có lẽ không quá khó, nhưng quan trọng là phải làm sớm, nếu để muộn thì rủi ro và công sức bỏ ra sẽ nhiều hơn (hiện tại tôi đang ở tình cảnh "quá muộn"). Ý tưởng là chuyển tất cả URI ra một chỗ riêng, mỗi URI có một định danh riêng. Tất cả mọi tham chiếu tới một URI đều thông qua định danh thay vì literal string. (Nói thì phức tạp, tóm lại là có thể dùng constant string, enum, hay một cái gì đó tương tự cho các URI/URI template.) Như thế URI có thể được thay đổi mà không gây ảnh hưởng, hoặc chỉ gây ảnh hưởng ở mức kiểm soát được, tới những thành phần khác của hệ thống. Nếu trên URI chứa tham số, ví dụ như {id} trong testcase/{id}/update, thì URI cần được tạo ra từ URI template và các tham số, chứ không phải bằng cách xây dựng string thủ công như 'testcase/' + id + '/update'.

Và tất nhiên, cấu trúc có chặt chẽ đến đâu, unit test có phủ kín tới mấy thì việc quan trọng nhất vẫn là con người, cụ thể là lập trình viên. Cho nên dù thiết kế hay hiện thực có thế nào thì cũng phải nhớ nhắc nhở lập trình viên của bạn làm theo :D.