Lưu trữ

Posts Tagged ‘Java’

SLF4J và logback

Tháng Tám 21, 2010 Để lại phản hồi

Mặc dù unit test có thể giúp hạn chế lỗi khi phát triển phần mềm, với những chương trình phức tạp, sự cố là không thể tránh khỏi, nghĩa là vẫn phải có debug. Và việc này sẽ đơn giản hơn rất nhiều nếu trạng thái của chương trình tại các thời điểm khác nhau được ghi lại. Ngoài ra, các thông tin này có thể giúp cải tiến chương trình về giao diện sử dụng, tốc độ, v.v. Đó là lí do chúng ta có các logging framework.

Tại sao không dùng println cho việc logging? Bởi vì chúng quá thô sơ. Một giải pháp logging, có lẽ phải đáp ứng các yêu cầu sau:

  • Tùy biến:
    • Bật, tắt logging (toàn bộ hoặc theo loại thông điệp.
    • Vị trí xuất log (màn hình, tập tin, database, v.v)
    • Định dạng, lượng thông tin cần log
  • Thông tin có thể log:
    • Vị trí gọi log
    • Thời gian
    • Thông điệp, stack trace, v.v

Có nhiều logging framework trong Java, trong đó nổi tiếng nhất là Log4J. Để thuận tiện khi chuyển đổi giữa các logging framework, người ta phát triển facade chung cho chúng. Không may là cũng lại có hơn một facade :) (Java là thế). Hiện nay, các dự án có vẻ đang ưa chuộng facade SLF4J. API của SLF4J được framework logback hiện thực trực tiếp (native implementation). Ngoài tính dễ sử dụng, SLF4J còn có một lợi thế là được phát triển bởi tác giả của Log4J—tất nhiên là người hiểu rõ các hạn chế của Log4J.

Dưới đây là một ví dụ nhỏ về SLF4J. Tài liệu hướng dẫn và cả javadoc của SLF4J và logback đã rất cụ thể và đầy đủ, nên nếu bạn muốn tìm hiểu sâu hơn thì đó là nơi tốt nhất để bắt đầu.

Mặc định của logback là log ra console:

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
public class HelloWorld { 
  public static void main(String[] args) { 
    Logger logger = LoggerFactory.getLogger("HelloWorld"); 
    logger.debug("Hello world."); 
  } 
}

Kết quả (console):

22:12:35.261 [main] DEBUG HelloWorld - Hello world.

Sửa đổi tập tin cấu hình (thường là logback.xml) và ta sẽ log được ra tập tin:

<configuration> 
  <appender name="FILE" class="ch.qos.logback.core.FileAppender"> 
    <file>hw.log</file> 
    <encoder> 
      <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern> 
    </encoder> 
  </appender> 
  <root level="debug"> 
    <appender-ref ref="FILE" /> 
  </root> 
</configuration>

Kết quả (trong tập tin hw.log):

2010-08-21 21:35:57,583 DEBUG [main] HelloWorld [HelloWorld.java:7] Hello world.

Thẻ:,

JUnit

Tháng Tám 19, 2010 2 comments

Có lẽ đa số sinh viên chúng ta đều quen với việc hoàn tất chương trình, chạy thử, thấy sai, tìm và sửa lỗi, chạy thử, v.v. Điều này không có gì là sai trái, tuy nhiên nếu đợi đến khi hoàn tất chương trình rồi mới chạy thử, thì việc tìm và sửa lỗi sẽ tương đối vất vả. Quá trình này sẽ đơn giản hơn, nếu bạn có thể chia chương trình thành từng phần nhỏ, và test chúng một cách riêng rẽ. Đó là ý nghĩa của unit test.

Tuy nhiên, để unit test hiệu quả, chúng ta cần có các công cụ để tiến hành nó một cách tự động. Các công cụ này gọi là unit test framework. Với Java, framework nổi tiếng nhất là JUnit.

Nếu bạn muốn tìm hiểu về JUnit, thì bạn nên đọc “JUnit in action”. Dưới đây là một ví dụ nhỏ về JUnit để bạn tham khảo.

Kiểm tra với một test case

Giả sử chúng ta có một class Number như dưới đây. Mỗi object của class đại diện cho một số nguyên. Method isEven kiểm tra tính chẵn lẻ của số nguyên đó. Mục tiêu của chúng ta là kiểm tra tính đúng đắn của method isEvent.

public class Number { 
    private int number; 
    public Number(int number) { this.number = number; } 
    public boolean isEven() { return (number % 2 == 0)?true:false; } 
}

Việc đầu tiên là tải tập tin junit-4.x.x.jar và cho nó vào CLASSPATH. Đoạn mã dưới đây sẽ kiểm tra isEvent với 2.

import static org.junit.Assert.*; 
import org.junit.Test; 
public class NumberTest { 
    @Test public void testIsEven() { 
        Number number = new Number(2); 
        assertTrue(number.isEven()); 
    } 
}

Dịch hai đoạn mã trên và chạy lệnh java org.junit.runner.JUnitCore NumberTest. Khi đó JUnit sẽ chạy các method được đánh dấu @Test. Kết quả là:

JUnit version 4.8.2 
. 
Time: 0.008 
OK (1 test)

Kiểm tra với nhiều test case

Bây giờ ta muốn kiếm tra với nhiều số hơn. Chúng ta sẽ không chèn thêm assertTrue vào testIsEven (vì như vậy sẽ không thể biết được số nào làm cho test bị sai). Thay vào đó, chúng ta sẽ tham số hóa method testIsEven. Các tham số được lấy từ bộ test tạo ra bởi method getTestParameters.

import static org.junit.Assert.*; 
import org.junit.Test; 
import org.junit.runner.RunWith; 
import org.junit.runners.Parameterized; 
import org.junit.runners.Parameterized.Parameters; 
import java.util.Arrays; 
import java.util.Collection;  

@RunWith(value=Parameterized.class) 
public class NumberParameterizedTest { 
    private int value; 
    private boolean expected; 
    public NumberParameterizedTest(int value, boolean expected) { 
        this.value = value; 
        this.expected = expected; 
    } 
    @Parameters public static Collection&lt;Object[]&gt; getTestParameters() { 
        return Arrays.asList(new Object[][] { {2, true}, {3, false}, {5, true}, }); 
    } 
    @Test public void testIsEven() { 
        Number number = new Number(value); 
        assertEquals(number.isEven(), expected); 
    } 
}

Lần này, Junit sẽ lần lượt lấy từng mẫu ra để kiểm tra. Kết quả là (trích):

JUnit version 4.8.2 
...E 
Time: 0.022 
There was 1 failure: 
1) testIsEven[2](NumberParameterizedTest)...

Như vậy là test sai với mẫu thứ ba (5, true) (vì đếm từ 0).

Kiểm tra Exception

Trong một số trường hợp, một method sẽ throw Exception thay vì trả về kết quả tính toán. Để đảm bảo hành vi của method đúng như mong đợi, chúng ta phải test cả những trường hợp này để xem chúng có throw đúng Exception cần throw không. @Test(expected=YourException.class) là annotation phục vụ cho mục đích này.

Dùng test làm hướng dẫn sử dụng

Hãy nhìn lại đoạn code trên. Khi đọc test (nếu bạn hiểu JUnit), bạn sẽ suy ra ngay cách sử dụng method isEven: đầu tiên tạo một object Number với constructor nhận vào giá trị int, sau đó gọi isEven(). Vậy là, test code còn một tác dụng khác là làm hướng dẫn sử dụng (tương tự như các đoạn code trong tutorial trên mạng). Ưu điểm của việc lấy test làm hướng dẫn sử dụng là nhanh gọn và bám sát theo thay đổi của chương trình.

Local class trong Java

Tháng Tám 8, 2010 Để lại phản hồi

Local class là class được khai báo bên trong một method. Các object của class này có đặc điểm là chỉ truy xuất được variable của method nếu chúng là final. Để giải thích điều này, hãy xét đoạn mã dưới đây.

class Outer {
	SomeClass o;
	public void OuterMethod() {
		int a;
		class Inner {
			public void InnerMethod() {
				// Do something
			}
		}
		Inner i = new Inner;
		o = new SomeClass(i);
	}
}

Ở đây, i và a là các local variable của method OuterMethod. Khi method này kết thúc, i và a sẽ bị xóa khỏi stack. Thế còn object mà i trỏ tới liệu có bị Garbage Collector xóa? Câu trả lời là tùy từng trường hợp. Vì i đã được pass cho object o, nếu o lưu lại một reference để trỏ tới object mà i đang trỏ tới, thì object này chưa đủ điều kiện để bị thu hồi vùng nhớ. Điều đáng nói ở đây là object mà i trỏ tới có thể tồn tại lâu hơn method OuterMethod. Bây giờ, nếu trong phần mã của InnerMethod có một method nào đó sử dụng i, và method này được chạy khi method OuterMethod đã kết thúc (nghĩa là i đã bị xóa), thì nó sẽ lấy i ở đâu ra? Tất nhiên, nó không thể lấy được i ở đâu cả. Để tránh tình huống này, Java không cho mã của method-local inner class truy xuất variable của method chứa nó, trừ khi variable là final.

Xét tiếp phần “final” của vấn đề. Vì final variable không thể bị thay đổi một khi đã được gán giá trị, cho nên object của inner class có thể copy lại. Vậy tại sao không copy các variable còn lại? Rất đơn giản, nếu biến int i được copy, thì khi đó i của object và i của method không liên hệ gì với nhau, mọi thay đổi của i trong object không ảnh hưởng gì đến i của method, nghĩa là việc copy i cũng chỉ tương đương với việc chúng ta truyền i vào trong object thông qua constructor hoặc method bình thường, không hơn không kém. Hoàn toàn không cần thiết phải đưa thêm một tính năng khi nó không thực sự giúp giải quyết vấn đề gì.

Thẻ:

Overriding trong Java là single dispatch

Tháng Bảy 28, 2010 Để lại phản hồi

Không có gì đặc biệt, nhưng có lẽ cũng phải ghi lại.

Trước hết, cho hai class Animal và Cat:

class Animal {} 
class Cat extends Animal {}

Tiếp theo cho class Hunter và BetterHunter:

class Hunter { 
    public void Kill(Animal a) { System.out.println("Hunter killed Animal"); } 
    public void Kill(Cat c) { System.out.println("Hunter killed Cat"); } 
} 
class BetterHunter extends Hunter { 
    public void Kill(Animal a) { System.out.println("BetterHunter killed Animal"); } 
    public void Kill(Cat c) { System.out.println("BetterHunter killed Cat"); } 
}

Bây giờ xét tình huống sau, kết quả in ra là gì?

Animal a = new Cat(); 
Hunter h = new BetterHunter(); 
h.Kill(a);

Vì h trỏ đến BetterHunter, JVM phân giải h.Kill thành Kill của BetterHunter. Sau đó, vì a trỏ đến Cat, JVM chọn Kill(Cat). Kết quả là “BetterHunter killed Cat”.

Sai. Java chỉ hỗ trợ Single dispatch, vì overriding được hiện thực nhờ vtable. Nói cách khác JVM chỉ phân giải method dựa vào tham số đầu tiên (this). Bước thứ hai (chọn Kill nào) được thực hiện trong lúc biên dịch (overload). Vì vậy Kill của BetterHunter vẫn được gọi, nhưng là Kill(Animal). Kết quả là “BetterHunter killed Animal”.

Để có Double dispatch, tức là gọi Kill(Cat), xem Thinking in Java, hoặc pattern Visitor.

Thẻ:
Follow

Get every new post delivered to your Inbox.