Linux mập

Trước hết nói về pipeline, niềm tự hào Unix. Ví dụ về đoạn mã kiểm tra chính tả (chép từ wikipedia):

curl "http://en.wikipedia.org/wiki/Pipeline_(Unix)" | \
sed 's/[^a-zA-Z ]/ /g' | \
tr 'A-Z ' 'a-z\n' | \
grep '[a-z]' | \
sort -u | \
comm -23 - /usr/share/dict/words

Giải thích ngắn gọn:

  • curl lấy nội dung trang web
  • sed chuyển kí tự không phải chữ cái và khoảng trắng thành khoảng trắng
  • tr chuyển chữ hoa thành chữ thường và khoảng trắng thành dấu xuống dòng
  • grep bỏ các dòng trống
  • sort xếp từ theo thứ tự và bỏ từ lặp
  • comm để lấy ra từ không có trong từ điển

Cái triết lí của Unix “Write programs that do one thing and do it well" cũng từ pipeline mà ra. Chính xác hơn, là từ câu nói của Doug Mcllroy, người phát minh ra pipe trong Unix:

This is the Unix philosophy: Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.

Trong quyển “Pattern-oriented software architecture”, khái niệm này còn có tên có tuổi hẳn hoi: Pipes and Filters pattern, một trong ba architectural pattern (pattern để xây dựng kiến trúc tổng quát cho phần mềm).

Mà kể ra thì pipeline nhìn cũng đẹp thật, gọn gàng, hiệu quả. Cho nên các Linux fanboy (như mình ngày xưa) hay lấy nó làm cái cớ hạ Windows xuống và vênh mặt lên tự hào (nói nữa sợ mất tính khách quan). Tiếc là “Write programs that do one thing and do it well” chỉ còn là quá khứ. Bạn thử nhìn vào hình dưới và đọc những dòng màu xanh nước biển:

 Untitled1

Tất nhiên là gần như không thể đọc được. Bởi vì cái ls này đã quá ham hố làm hai thứ: liệt kê thư mục và định dạng dữ liệu xuất (chia cột, tô màu). Mà nó lại không làm tốt việc tô màu.

Không phải mình tự nghĩ ra đâu, mà là Rob Pike và Brian W. Kernighan (đồng tác giả “The C Programming Language”) nói thế trong paper “Program design in the UNIX environment”:

An example of the first approach is Berkeley’s version of the ls command, which lists the filenames in a directory. Let us call it lsc to avoid confusion. The 7th Edition ls command lists filenames in a single column, so for a large directory, the list of filenames disappears off the top of the screen at great speed. lsc prints in columns across the screen (which is assumed to be 80 columns wide), so there are typically four to eight times as many names on each line, and thus the output usually fits on one screen. The option -1 can be used to get the old single-column behavior.

…The automatic columnation in lsc is reminiscent of the ‘‘wild cards’’ found in some systems that provide filename pattern matching only for a particular program. The experience with centralized processing of wild cards in the UNIX shell shows overwhelmingly how important it is to centralize the function where it can be used by all programs.

One solution for the ls problem is obvious — a separate program for columnation, so that columnation into say 5 columns is just ls | 5…

Thế nên đừng tường cứ có pipe được là thành “do one thing and do it well”.

Chuyện này có từ thời BSD, nhưng thôi cứ chửi GNU cho tiện (vì bây giờ GNU rất phổ biến trên Linux). Vậy là từ cái đống lệnh pipe phức tạp nhưng dễ quản lí (vì mỗi người một việc) bây giờ chúng ta có một đống lệnh pipe phức tạp và khó quản lí (nhiều người cũng làm được một việc, chọn ai bây giờ). Tốt nhất là cứ theo kiểu Windows, thân ai người đấy lo, thiếu chức năng thì viết lệnh mới.

Dùng câu này trong “Pattern-oriented software architecture” để kết là hợp nhất:

The Pipes and Filters pattern, in contrast, is less often used, but is attractive in areas where data streams can be processed incrementally. Surprisingly, some system families modelled in this fashion turn out to be poor candidates for this paradigm, neglecting areas where this pattern could be used more beneficially.

Checked exception trong Java

Tiếp chuyện hôm trước, nhưng hôm nay không có C#.

Không quan tâm tới chuyện checked exception trong Java hay dở thế nào nữa, tại vì chúng ta không phải là Sun. Họ làm sao là quyền của họ. Việc của chúng ta là làm theo ý họ theo cách tốt nhất có thể.

Sun có một bài dài về checked exception, và họ kết luận:

Generally speaking, do not throw a RuntimeException or create a subclass of RuntimeException simply because you don’t want to be bothered with specifying the exceptions your methods can throw.

Here’s the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.

Tức là với Sun, lập trình viên phải ưu tiên xem xét checked exception trước. Trong quyển Effective Java: Programming Language Guide, Josh Bloch (một trong những người tham gia thiết kế Java) cụ thể hóa các chỉ dẫn trên một chút:

  • Item 39: Use exceptions only for exceptional conditions. That is, do not use exceptions for control flow, such as catching NoSuchElementException  when calling Iterator.next() instead of first checking Iterator.hasNext().
  • Item 40: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors. Here, Bloch echoes the conventional Sun wisdom — that runtime exceptions should be used only to indicate programming errors, such as precondition violations.
  • Item 41: Avoid unnecessary use of checked exceptions. In other words, don’t use checked exceptions for conditions from which the caller could not possibly recover, or for which the only foreseeable response would be for the program to exit.
  • Item 43: Throw exceptions appropriate to the abstraction. In other words, exceptions thrown by a method should be defined at an abstraction level consistent with what the method does, not necessarily with the low-level details of how it is implemented. For example, a method that loads resources from files, databases, or JNDI should throw some sort of ResourceNotFound exception when it cannot find a resource (generally using exception chaining to preserve the underlying cause), rather than the lower-level IOException, SQLException, or NamingException.

Tuy nhiên, như đã nói, các ông trùm đều cho rằng checked exception theo kiểu Java phá vỡ tính bao đóng, làm code dài, khó bảo trì, v.v cho nên một hướng dẫn chung chung thế kia có lẽ là chưa đủ. Có hai lựa chọn có vẻ đáng chú ý.

Thứ nhất là quan điểm “cực đoan” của Bruce Eckel (người viết mấy quyển Thinking in), chỉ nên có checked exception. Bruce Eckel đề xuất việc mở rộng RuntimeException thành ExceptionAdapter có tác dụng:

Here, you’re still able to catch the specific type of exception but you’re not forced to put in all the exception specifications and try-catch clauses everywhere between the origin of the exception and the place that it’s caught. An even more importantly, no one writing code is tempted to swallow the exception and thus erase it. If you forget to catch some exception, it will show up at the top level. If you want to catch exceptions somewhere in between, you can.

Thứ hai là quan điểm “ôn hòa” của Rod Johnson trong J2EE Design and Development. Giống như Anders Hejlsberg, ông này cũng cho là checked exception là cần thiết, vấn đề là dùng sao cho hợp lí (trong điều kiện vẫn chưa tìm ra giải pháp nào phù hợp về mặt ngôn ngữ):

Checked exceptions are much superior to error return codes (as used in many older languages). Sooner or later (probably sooner) someone will fail to check an error return value; it’s good to use the compiler to enforce correct error handling. Such checked exceptions are as integral to an object’s API as parameters and return values.

However, I don’t recommend using checked exceptions unless callers are likely to be able to handle them. In particular, checked exceptions shouldn’t be used to indicate that something went horribly wrong, which the caller can’t be expected to handle.

Rod Johnson còn đề xuất cả một hướng dẫn cụ thể:

Question Example Recommendation if the answer is yes
Should all callers handle this problem? Is the exception essentially a second return value for the method? Spending limit exceeded in a processInvoice() method Define and used a checked exception and take advantage of Java’s compile-time support.
Will only a minority of callers want to handle this problem? JDO exceptions Extend RuntimeException. This leaves callers the choice of catching the exception, but doesn’t force all callers to catch it.
Did something go horribly wrong? Is the problem unrecoverable? A business method fails because it can’t connect to the application database Extend RuntimeException. We know that callers can’t do anything useful besides inform the user of the error.
Still not clear?   Extend RuntimeException. Document the exceptions that may be thrown and let callers decide which, if any, they wish to catch.

Decide at a package level how each package will use checked or unchecked exceptions. Document the decision to use unchecked exceptions, as many developers will not expect it.

The only danger in using unchecked exceptions is that the exceptions may be inadequately documented. When using unchecked exceptions, be sure to document all exceptions that may be thrown from each method, allowing calling code to choose to catch even exceptions that you expect will be fatal. Ideally, the compiler should enforce Javdoc-ing of all exceptions, checked and unchecked.

If allocating resources such as JDBC connections that must be released under all circumstances, remember to use a finally block to ensure cleanup, whether or not you need to catch checked exceptions. Remember that a finally block can be used even without a catch block.

Quyển J2EE Design and Development còn trình bày rất nhiều về exception và các vấn đề đáng quan tâm khác trong Java.

Checked exception trong Java và C#

Có một thứ mình từng thấy rất khó chịu khi dùng C#, là nó không hỗ trợ checked exception. Trong Java, checked exception nôm na là các exception không phải do lỗi lập trình, mà là những tình huống đặc biệt, như không tìm được file, v.v. Bạn bắt buộc phải catch các exception này, hoặc throw lại nó. Với C#, tất cả các exception đều là unchecked. Điều đó có nghĩa là bạn phải dựa vào tài liệu hỗ trợ hoặc xem trực tiếp code để biết các exception mà một method có thể throw. Và việc không chắc chắn phải catch các exception có vẻ như sẽ gây trở ngại trong việc xây dựng một chương trình tốt.

Nhưng theo Anders Hejlsberg (kiến trúc sư trưởng của C#), thì cái cách Java hiện thực checked exception có vẻ không tốt. Tức là, checked exception vẫn là một ý tưởng hữu ích:

Frankly, they look really great up front, and there’s nothing wrong with the idea. I completely agree that checked exceptions are a wonderful feature. It’s just that particular implementations can be problematic. By implementing checked exceptions the way it’s done in Java, for example, I think you just take one set of problems and trade them for another set of problems. In the end it’s not clear to me that you actually make life any easier. You just make it different.

Nhưng giải pháp Java đưa ra thì có vấn đề:

The concern I have about checked exceptions is the handcuffs they put on programmers. You see programmers picking up new APIs that have all these throws clauses, and then you see how convoluted their code gets, and you realize the checked exceptions aren’t helping them any. It is sort of these dictatorial API designers telling you how to do your exception handling. They should not be doing that.

Alan Griffiths cũng đồng ý rằng giải pháp của Java gây ra một số vấn đề, trong đó có việc phá vỡ tính bao đóng (mà cũng không biết ông này là ông nào, có điều thấy bài viết cũng được). Nhưng ngắn gọn lại thì cũng như quan điểm của Hejlsberg—lập trình viên sớm muộn sẽ catch và throw một loạt exception Exception:

The throws clause, at least the way it’s implemented in Java, doesn’t necessarily force you to handle the exceptions, but if you don’t handle them, it forces you to acknowledge precisely which exceptions might pass through. It requires you to either catch declared exceptions or put them in your own throws clause. To work around this requirement, people do ridiculous things. For example, they decorate every method with, "throws Exception." That just completely defeats the feature, and you just made the programmer write more gobbledy gunk. That doesn’t help anybody.

Cái này mình cũng đồng ý, vì mình cũng hay làm thế cho nhanh. Vấn đề là Exception thì bao gồm cả RuntimeException, là những exception mà chúng ta không nên tự xử lí. Mình thấy tốt hơn hết là để cho lập trình viên catch những exception họ muốn/cần catch, và phần còn lại sẽ được throw tự động.

Có một lời giải thích khá thú vị cho giải pháp C# đưa ra—không hỗ trợ exception, hay chính xác hơn là không đưa ra giải pháp thực sự nào. Đó là vì họ chưa nghĩ ra.

C# is basically silent on the checked exceptions issue. Once a better solution is known—and trust me we continue to think about it—we can go back and actually put something in place. I’m a strong believer that if you don’t have anything right to say, or anything that moves the art forward, then you’d better just be completely silent and neutral, as opposed to trying to lay out a framework.

And so, when you take all of these issues, to me it just seems more thinking is needed before we put some kind of checked exceptions mechanism in place for C#. But that said, there’s certainly tremendous value in knowing what exceptions can get thrown, and having some sort of tool that checks. I don’t think we can construct hard and fast rules down to, it is either a compiler error or not. But I think we can certainly do a lot with analysis tools that detect suspicious code, including uncaught exceptions, and points out those potential holes to you.

Cũng là một lời khuyên hay. Nếu không thể làm cho tình huống tốt hơn thì hãy để nó nguyên như thế, ít nhất nó cũng sẽ không tệ đi.

Vẫn còn bảy bài phỏng vấn với ông trùm ngôn ngữ lập trình này, phải ráng đọc cho hết.

Nhà vệ sinh

Tại sao một bao hot dog có mười cây còn một bao bánh mì hot dog lại chỉ có tám cái? Cuộc sống không phải lúc nào cũng như ý muốn của chúng ta, nên hãy biết hài lòng với những thứ mình có, vì bạn sẽ luôn có hot dog để ăn :). Câu này trong phim Thầy tu chống đạn, và nó lãng xẹt, nghe chả triết lí gì hết. Nhưng mà nó có liên quan đến nhà vệ sinh nên tạm cho vào đây.

Quay lại nhà vệ sinh đã. Không muốn đưa chủ đề này lên blog, nhưng tại vì bức xúc quá thể. Đây là những gì thu nhặt được sau hơn một tuần buộc phải lê lết ở trường vì đói Internet:

  • Số nhà vệ sinh nữ ít hơn rất nhiều so với số nhà vệ sinh nam. Nếu bạn định rủ bạn gái tới BK, tốt hơn hết là nên lập trước bản đồ, để lúc đấy đỡ phải lên Google map. Và nếu có thể thì vào thử luôn ;), vì chuyện thứ ba.
  • Các nhà vệ sinh đều không có hệ thống xả nước tự động, ngoại trừ các thực thể sinh học thông minh có khả năng tự di chuyển và cọ rửa khoảng 12 hoặc 24 giờ một lần. Tuy nhiên rất nhiều sinh viên nam (vì nữ HM không thể biết được) mặc định rằng hệ thống ấy tồn tại.
  • Hệ quả của chuyện trên là khi vào nhà vệ sinh HM phải chuyển sang chế độ “tiết kiệm năng lượng”, cắt giảm các hoạt động không cần thiết trong cơ thể, để giảm bớt nhu cầu hô hấp tới mức tối đa.
  • Nếu bạn có từng vào nhà vệ sinh ở BK, có lẽ bạn sẽ nhận ra sự đan xen hài hòa giữa nhiều trường phái kiến trúc Đông Tây, mà điển hình là ở B1 và B9. Nhà vệ sinh ở B1 lấy ý tưởng đề cao sự giao lưu cộng đồng kiểu châu Âu (chắc thế, vì người ta hay nói ở tây hay tham gia hoạt động xã hội), cho nên cánh cửa được thiết kế sao cho chỉ cần hé mở một chút, thì sẽ không còn có sự ngăn cách nào giữa người đi vệ sinh và người đi bộ ngoài hành lang. Trái lại, nhà vệ sinh B9 lại được xây dựng đậm chất châu Á. Cụ thể hơn, là kiểu Pol Pot, không có gì là của cá nhân, tất cả đều thuộc về tập thể. Khó tả quá. Đại để là, chung một dòng sông.

Thế nên phải vào nhà vệ sinh ở BK là cả một sự khổ nhục. Chỉ đến sáng nay, khi phải qua cơ sở 2 đại học sư phạm có chút chuyện HM mới nhận ra, có một cái nhà vệ sinh gắn vòi rửa tay như BK quả là một mơ ước đáng để ước mơ của sinh viên ở cơ sở này. Đúng là cuộc sống không phải lúc nào cũng như ý mình nên hãy biết hạnh phúc với những thứ đang có.

Nếu lúc nào bạn có chuyện gì buồn bã, tuyệt vọng thì hãy vào nhà vệ sinh ở BK, sau đó làm lại điều đó ở cơ sở 2 đại học sư phạm, nhất định bạn sẽ thấy nhẹ nhõm phần nào.

Getter/setter

Ngày xưa khi học C++ hay được dạy phải để field của object là private, sau đó tạo hai method public là get…() và set…(…) để truy cập và thay đổi các field này. Nhưng không ai dặn mình phải dùng nó một cách cẩn thận. Có quá nhiều lí do để get/set là dấu hiệu của một thiết kế yếu kém.

Thường thì chúng ta get một field để lấy thông tin của đối tượng, và thao tác trên thông tin đó. Vấn đề là tại sao phải trực tiếp làm việc trên thông tin này. Xét cho cùng, đối tượng là một thực thể mà tất cả những gì bên ngoài (nên) được biết về nó là các “dịch vụ” mà nó cung cấp. Cho nên, thay vì lấy thông tin từ đối tượng để thực hiện một công việc, hãy bảo đối tượng này làm nó cho chúng ta. Nếu điều này là không thể, thì có vẻ như thiết kế của chương trình có vấn đề, các đối tượng đã không được xây dựng để làm các công việc của nó một cách tách bạch, rõ ràng. Và khi sự coupling giữa các đối tượng càng cao, việc bảo trì và mở rộng trong tương lai sẽ càng khó khăn, phức tạp.

Mặt khác, khi get hay set một field nào đó, thường chúng ta đã có giả định về kiểu và ý nghĩa của nó. Nói cách khác, chúng ta đã có hiểu biết nhất định về hiện thực bên trong của đối tượng. Có điều, hiện thực của một đối tượng không phải là một bất biến. Khó có thể đảm bảo trong tương lai, khi đối tượng được xây dựng lại, các private field đó sẽ giữ nguyên kiểu và ý nghĩa như hôm nay, thậm chí nó có thể không tồn tại nữa. Hệ quả là, những đoạn mã có sử dụng get/set cũng trở nên vô dụng. Điều đó đồng nghĩa với việc không đạt được tính dễ bảo trì và mở rộng—mục tiêu mà lập trình hướng đối tượng hướng tới.

Tất nhiên, nói như thế không có nghĩa là chúng ta phải tránh xa get/set. Get/set sẽ tồn tại nếu chúng thực sự là những “dịch vụ” cần thiết, và không phụ thuộc và thay đổi của bất kì cách hiện thực nào của đối tượng. Nhưng lúc đó thì get/set lại không hẳn liên quan đến cái get/set người ta hay dạy cho học sinh nữa.