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

Advertisements

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.