Về cơ bản, sự phản ánh có nghĩa là sử dụng mã chương trình của bạn làm dữ liệu.
Do đó, sử dụng sự phản chiếu có thể là một ý tưởng tốt khi mã chương trình của bạn là một nguồn dữ liệu hữu ích. (Nhưng có sự đánh đổi, vì vậy nó có thể không phải lúc nào cũng là một ý tưởng hay.)
Ví dụ, hãy xem xét một lớp đơn giản:
public class Foo {
public int value;
public string anotherValue;
}
và bạn muốn tạo XML từ nó. Bạn có thể viết mã để tạo XML:
public XmlNode generateXml(Foo foo) {
XmlElement root = new XmlElement("Foo");
XmlElement valueElement = new XmlElement("value");
valueElement.add(new XmlText(Integer.toString(foo.value)));
root.add(valueElement);
XmlElement anotherValueElement = new XmlElement("anotherValue");
anotherValueElement.add(new XmlText(foo.anotherValue));
root.add(anotherValueElement);
return root;
}
Nhưng đây là rất nhiều mã soạn sẵn và mỗi khi bạn thay đổi lớp, bạn phải cập nhật mã. Thực sự, bạn có thể mô tả những gì mã này làm
- tạo một phần tử XML với tên của lớp
- cho mỗi tài sản của lớp
- tạo một phần tử XML với tên của thuộc tính
- đặt giá trị của thuộc tính vào phần tử XML
- thêm phần tử XML vào thư mục gốc
Đây là một thuật toán và đầu vào của thuật toán là lớp: chúng ta cần tên của nó, và tên, loại và giá trị của các thuộc tính của nó. Đây là nơi phản ánh đến: nó cho phép bạn truy cập vào thông tin này. Java cho phép bạn kiểm tra các loại bằng cách sử dụng các phương thức của Class
lớp.
Một số trường hợp sử dụng khác:
- xác định URL trong máy chủ web dựa trên tên phương thức của lớp và tham số URL dựa trên đối số phương thức
- chuyển đổi cấu trúc của một lớp thành một định nghĩa kiểu GraphQL
- gọi mọi phương thức của một lớp có tên bắt đầu bằng "test" là trường hợp kiểm thử đơn vị
Tuy nhiên, phản ánh đầy đủ có nghĩa là không chỉ nhìn vào mã hiện có (mà bản thân nó được gọi là "introspection"), mà còn sửa đổi hoặc tạo mã. Có hai trường hợp sử dụng nổi bật trong Java cho việc này: proxy và giả.
Giả sử bạn có một giao diện:
public interface Froobnicator {
void froobnicateFruits(List<Fruit> fruits);
void froobnicateFuel(Fuel fuel);
// lots of other things to froobnicate
}
và bạn có một triển khai thực hiện một điều thú vị:
public class PowerFroobnicator implements Froobnicator {
// awesome implementations
}
Và trên thực tế, bạn cũng có một triển khai thứ hai:
public class EnergySaverFroobnicator implements Froobnicator {
// efficient implementations
}
Bây giờ bạn cũng muốn một số đầu ra đăng nhập; bạn chỉ muốn một thông điệp tường trình bất cứ khi nào một phương thức được gọi. Bạn có thể thêm đầu ra nhật ký vào mọi phương thức một cách rõ ràng, nhưng điều đó sẽ gây khó chịu và bạn phải thực hiện hai lần; một lần cho mỗi lần thực hiện. (Vì vậy, thậm chí nhiều hơn khi bạn thêm nhiều triển khai.)
Thay vào đó, bạn có thể viết proxy:
public class LoggingFroobnicator implements Froobnicator {
private Logger logger;
private Froobnicator inner;
// constructor that sets those two
public void froobnicateFruits(List<Fruit> fruits) {
logger.logDebug("froobnicateFruits called");
inner.froobnicateFruits(fruits);
}
public void froobnicateFuel(Fuel fuel) {
logger.logDebug("froobnicateFuel( called");
inner.froobnicateFuel(fuel);
}
// lots of other things to froobnicate
}
Tuy nhiên, một lần nữa, có một mẫu lặp đi lặp lại có thể được mô tả bằng thuật toán:
- proxy logger là một lớp thực hiện một giao diện
- nó có một hàm tạo để thực hiện một giao diện khác và một logger
- cho mọi phương thức trong giao diện
- việc thực hiện ghi lại một thông báo "$ methodname được gọi"
- và sau đó gọi cùng một phương thức trên giao diện bên trong, chuyển qua tất cả các đối số
và đầu vào của thuật toán này là định nghĩa giao diện.
Reflection cho phép bạn xác định một lớp mới bằng thuật toán này. Java cho phép bạn thực hiện điều này bằng cách sử dụng các phương thức của java.lang.reflect.Proxy
lớp và có những thư viện cung cấp cho bạn nhiều sức mạnh hơn nữa.
Vậy nhược điểm của sự phản ánh là gì?
- Mã của bạn trở nên khó hiểu hơn. Bạn là một mức độ trừu tượng được loại bỏ thêm khỏi các hiệu ứng cụ thể của mã của bạn.
- Mã của bạn trở nên khó gỡ lỗi hơn. Đặc biệt với các thư viện tạo mã, mã được thực thi có thể không phải là mã bạn đã viết, nhưng mã bạn đã tạo và trình gỡ lỗi có thể không hiển thị cho bạn mã đó (hoặc cho phép bạn đặt các điểm dừng).
- Mã của bạn trở nên chậm hơn. Tự động đọc thông tin loại và truy cập các trường bằng cách xử lý thời gian chạy thay vì truy cập mã hóa chậm hơn. Tạo mã động có thể giảm thiểu hiệu ứng này, với chi phí thậm chí còn khó hơn để gỡ lỗi.
- Mã của bạn có thể trở nên dễ vỡ hơn. Trình truy cập phản xạ động không được kiểm tra kiểu bởi trình biên dịch, nhưng ném lỗi khi chạy.