Thứ Tư, 6 tháng 4, 2016

Khám phá 6 vai trò của interface trong thế giới Java

Bài gốc tại Java World.

Dành cho những ai mới học Java.

Tác giả: Jeff Friesen
Dịch giả: Minh Hiếu
Hiệu chỉnh: Lộc Hồ
Đính chính: Nhữ Đình Thuận



Những người mới làm quen ngôn ngữ Java thường hay gặp một số nhầm lẫn nhất định. Họ gặp bối rối bởi những tính năng lạ hoắc của ngôn ngữ, chẳng hạn như Generics, Lambdas trong Java. Song trên thực tế, ngay cả những thứ đơn giản như interface cũng rất có thể gây rối rắm khi muốn hiểu kỹ bản chất của chúng.

Gần đây, tôi đối mặt với một câu hỏi về lý do tại sao Java hỗ trợ interface (bởi từ khoá interface và implements). Khi tôi bắt đầu học Java trong những năm 1990, câu hỏi này thường được trả lời bằng giải thích rằng trong Java, interface được tạo ra thay thế vai trò của đa thừa kế - đặc điểm mà Java không hỗ trợ. Tuy nhiên, vai trò của interfaces phục vụ nhiều hơn một cỗ máy được lắp ráp cẩu thả chỉ mỗi mục đích đề cập trên. Trong bài này, tôi xin được trình bày sáu vai trò mà interfaces đảm nhiệm trong Java.

Định nghĩa “đa kế thừa”: Đa kế thừa thường được sử dụng để chỉ một lớp con kế thừa những hành vi và thuộc tính cho phép từ nhiều lớp cha. Trong Java, thuật ngữ cài đặt đa kế thừa cũng có nghĩa tương tự. Java hỗ trợ đa thừa kế với interface, trong đó một lớp interface con có thể kế thừa nhiều phương thức từ nhiều interface cha. Để tìm hiểu thêm về đa kế thừa (bao gồm cả các vấn đề nổi tiếng), hãy xem bài viết “Multiple inheritance” trên Wikipedia.

Vai trò 1: Khai báo kiểu dữ liệu annotation

Từ khóa interface sử dụng trong khai báo kiểu dữ liệu annotation. Ví dụ sau minh hoạ cho việc tạo một annotation có tên Stub:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Stub {
  int id(); // dấu chấm phẩy kết thúc lệnh.
  String dueDate();
  String developer() default "unassigned";
}


Stub mô tả một loại annotation (annotation type instances) với cách viết gồm kiểu dữ liệu và hàm (method) không cài đặt. Bắt đầu khai báo với phần tiền tố @ theo sau là từ khoá interface rồi đến tên của annotation. Các thuộc tính của annotation được viết tương tự như phần đầu của method, annotation gồm 3 trường:

id() - tên thuộc tính định danh cho annotation với kiểu số, bắt buộc khai báo giá trị.
dueDate() thuộc tính tên dueDate, kiểu string, bắt buộc khai báo giá trị.
developer() tên thuộc tính developer, kiểu string và có giá trị mặc định là  “unassigned” (có thể không cần gán value cho trường này).

Ví dụ 2 minh hoạ cách sử dụng Stub cho việc chú thích một lớp và hàm của nó.

@Stub(
    id = 1,
    dueDate = "12/31/2016"
    )
public class ContactMgr {
  @Stub(
      id = 2,
      dueDate = "06/31/2016",
      developer = "Marty"
      )
  public void addContact(String contactID){
  }
}

Annotation có thể dụng cho class, field và method. Stub được dùng cho cả 3 vì không chỉ định dùng cho loại nào.  Khi sử dụng annotation cho việc chú thích code, ta sử dụng tiền tố @ + tên và gán giá trị cho các trường của nó, ví dụ id = 1.

Annotation ở đây được dùng cả trong runtime - khi chạy. Một số annotation chỉ dùng trong compile, nghĩa là sau giai đoạn biên dịch, Compiler có thể loại bỏ chúng.

Ví dụ 3 minh hoạ việc đọc ra annotation từ code và dùng vào xử lý nghiệp vụ.

import java.lang.reflect.Method;

public class StubFinder {
  public static void main(String[] args) throws Exception  {
    if (args.length != 1) {
      System.err.println("usage: java StubFinder classfile");
      return;
    }

    Class clazz = Class.forName(args[0]);
    if (clazz.isAnnotationPresent(Stub.class)) {
      Stub stub = clazz.getAnnotation(Stub.class);
      System.out.println("Stub ID = " + stub.id());
      System.out.println("Stub Date = " + stub.dueDate());
      System.out.println("Stub Developer = " + stub.developer());
      System.out.println();
    }

    Method[] methods = clazz.getMethods();
    for (int i = 0; i < methods.length; i++) {
      if (methods[i].isAnnotationPresent(Stub.class)) {
        Stub stub = methods[i].getAnnotation(Stub.class);
        System.out.println("Stub ID = " + stub.id());
        System.out.println("Stub Date = " + stub.dueDate());
        System.out.println("Stub Developer = " + stub.developer());
        System.out.println();
      }
    }
  }
}

Khi chạy hàm main, Java Reflection (một thư viện trong Java) có nhiệm vụ tìm kiếm class với tên truyền vào, hàm getAnnotation với parameter là Sub.class giúp tìm kiếm xem lớp có được đánh dấu bằng Stub. Sau đó in giá trị các trường của Stub nếu có ra Console. Logic code tiếp theo sẽ tìm kiếm các method được đánh dấu bằng Stub và in ra giá trị các trường nếu có.

Khi dịch và chạy bạn nhận được output sau:

Stub ID = 1
Stub Date = 12/31/2016
Stub Developer = unassigned

Stub ID = 2
Stub Date = 06/31/2016
Stub Developer = Marty

Bạn có thể tranh cãi rằng kiểu annotation và Stub annotation trên không liên quan gì đến interfaces. Thêm nữa, lớp khai báo và từ khóa implement lại không có mặt. Tuy nhiên tôi sẽ có ý kiến như sau.

@interfaces giống class trong việc ra một type - kiểu dữ liệu. Các trường bên trong nó như các phương thức có kiểu dữ liệu trả về. Chúng ta cũng có dữ liệu mặc định trả về khi các trường đó không được gán giá trị khi dùng annotation, có vẻ tương tự như objects. Non-Default-Element bắt buộc phải khai báo giá trị. Theo đó, một chú thích từ annotation này có đặc điểm như một cài đặt của class từ một interface nào đó. Inteface có mặc định và có bắt buộc cài đặt các method.

Ghi chú từ Nhữ Đình Thuận: Thực sự thì interface không phải được thiết kế cho đa thừa kế, trong Java, khái niệm đa thừa kế không tồn tại mà chỉ bị gán ghép một cách thô bỉ từ một số người hay một số cuốn sách. Vai trò của interface là nhằm tăng cường sự đa diện cho đối tượng trong OOP cũng như OOD. Để diễn giải kỹ hơn về vấn đề này, tôi sẽ sớm viết lại một bài rành mạch về bản chất interface trong lập trình hướng đối tượng.

Cách hiểu của tác giả về interface với annotation đang bị nhầm lẫn về mặt từ khoá, còn bản chất đây là 2 thứ hoàn toàn khác nhau, không liên quan. Đây là cách diễn giải của người mới học Java hoặc không hiểu cặn kẽ về Java.

Vai trò 2: Mô tả khả năng cài đặt chung

Một số lớp khác nhau có những khả năng, đặc tính chung. Ví dụ, các lớp java.nio.CharBuffer, javax.swing.text.Segment, java.lang.String, java.lang.StringBuffer,
và java.lang.StringBuilder cùng có khả năng đọc tuần tự character - ký tự.

Khi các lớp cùng chung một khả năng thực thi, một interface đóng vai trò thể hiện khả năng này cho việc tái sử dụng. Ví dụ, ta sử dụng một interface dùng chung là java.lang.CharSequence cho các class trên, nó định nghĩa hành vi đọc tuần tự ký tự. CharSequence cung cấp hình mẫu, chuẩn chung cho việc đọc tuần tự character với nhiều hình thái Tập Ký Tự (String, StringBuffer, Segment,...).

CharSequence cung cấp khuôn mẫu, và sự truy cập cận ở mức độ chỉ đọc(read-only) tới nhiều kiểu dãy chars khác nhau.
Giả dụ bạn muốn viết một chương trình nhỏ đếm các ký tự thường trong CharBuffer, String hay StringBuilder. Sau một hồi suy nghĩ, bạn có thể cài đặt như Ví dụ 4.

import java.nio.CharBuffer;

public class Freq {
  public static void main(String[] args) {
    if (args.length != 1) {
      System.err.println("usage: java Freq text");
      return;
    }
 
    analyzeS(args[0]);
    analyzeSB(new StringBuffer(args[0]));
    analyzeCB(CharBuffer.wrap(args[0]));
  }

  static void analyzeCB(CharBuffer cb)  {
    int counts[] = new int[26];
    while (cb.hasRemaining()) {
      char ch = cb.get();
      if (ch >= 'a' && ch <= 'z')  counts[ch - 'a']++;
    }
    for (int i = 0; i < counts.length; i++)
      System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
    System.out.println();
  }

  static void analyzeS(String s) {
    int counts[] = new int[26];
    for (int i = 0; i < s.length(); i++) {
      char ch = s.charAt(i);
      if (ch >= 'a' && ch <= 'z')
        counts[ch - 'a']++;
    }
    for (int i = 0; i < counts.length; i++)
      System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
    System.out.println();
  }

  static void analyzeSB(StringBuffer sb)  {
    int counts[] = new int[26];
    for (int i = 0; i < sb.length(); i++) {
      char ch = sb.charAt(i);
      if (ch >= 'a' && ch <= 'z')
        counts[ch - 'a']++;
    }
 
    for (int i = 0; i < counts.length; i++)
      System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
    System.out.println();
  }
}

Ví dụ trên sử dụng tới 3 method cùng một nhiệm vụ đếm ký tự thường cho 3 kiểu dữ liệu khác nhau. Mặc dù String và StringBuffer được cài đặt khá giống nhau về cấu trúc dữ liệu nhưng CharBuffer lại khác nhiều.

Code cài đặt trên bộc lộ vấn đề trùng lặp mã dẫn đến một class viết quá dài và không cần thiết. Nếu cài đặt phụ thuộc vào một interface có hàm chung là CharSequence thì ví dụ 5 là một phiên bản khác chạy tương tự.

import java.nio.CharBuffer;

public class Freq {
   public static void main(String[] args) {
      if (args.length != 1) {
         System.err.println("usage: java Freq text");
         return;
      }
      analyze(args[0]);
      analyze(new StringBuffer(args[0]));
      analyze(CharBuffer.wrap(args[0]));
   }

   static void analyze(CharSequence cs) {
      int counts[] = new int[26];
      for (int i = 0; i < cs.length(); i++) {
         char ch = cs.charAt(i);
         if (ch >= 'a' && ch <= 'z')
            counts[ch - 'a']++;
      }
      for (int i = 0; i < counts.length; i++)
         System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
      System.out.println();
   }
}


Như bạn thấy, chương trình trên rõ ràng đơn giản và sạch hơn bằng việc sử dụng interface chung mà cả 3 lớp String, StringBuffer, và CharBuffer đều có thể làm đầu vào.

Tóm lại, interface đóng vai trò mô tả những đặc điểm cài đặt chung nhất. Việc viết mã nên sử dụng interface để thể hiện được sử linh hoạt trong gọi hàm, tránh mã thừa, code ngắn gọn hơn. Ở ví dụ trên, bạn có thể tiết kiệm được 50% mã.

Ghi chú từ Nhữ Đình Thuận: Nguyên lý của thiết kế mã lệnh là phụ thuộc vào trừu tượng, thiết kế, vào lớp chung chứ không phụ thuộc vào cài đặt, cụ thể. Đây là nguyên lý Dependency Inversion trong SOLID và cách diễn giải của tác giả đang thể hiện ở mức "mới học code". Design code theo interface chỉ là 1 thực nghiệm của nguyên lý thiết kế mã này.

Còn nữa









Không có nhận xét nào:

Đăng nhận xét

nhudinhthuan@gmail.com