Vâng, cách tiếp cận chung để lưu trữ dữ liệu trong java là, như bạn đã lưu ý, không phải ở tất cả các hướng đối tượng. Điều này trong và của chính nó không phải là xấu hay tốt: "hướng đối tượng" không phải là lợi thế hay bất lợi, nó chỉ là một trong nhiều mô hình, mà đôi khi giúp với thiết kế kiến trúc tốt (và đôi khi không).
Lý do DAO trong java thường không hướng đối tượng chính xác là những gì bạn muốn đạt được - thư giãn sự phụ thuộc của bạn trên cơ sở dữ liệu cụ thể. Trong một ngôn ngữ được thiết kế tốt hơn, cho phép nhiều thừa kế, điều này, hoặc khóa học, có thể được thực hiện rất tao nhã theo cách hướng đối tượng, nhưng với java, nó dường như có nhiều rắc rối hơn nó đáng giá.
Theo nghĩa rộng hơn, cách tiếp cận không phải của OO giúp tách dữ liệu cấp ứng dụng của bạn khỏi cách dữ liệu được lưu trữ. Điều này không chỉ phụ thuộc vào các chi tiết cụ thể của một cơ sở dữ liệu cụ thể, mà còn là các lược đồ lưu trữ, đặc biệt quan trọng khi sử dụng cơ sở dữ liệu quan hệ (không bắt đầu với ORM): liền mạch dịch sang mô hình ứng dụng OO bởi DAO của bạn.
Vì vậy, hầu hết các DAO trong java ngày nay chủ yếu là những gì bạn đã đề cập trong các lớp học đầu tiên, đầy đủ các phương pháp tĩnh. Một sự khác biệt là, thay vì làm cho tất cả các phương thức tĩnh, tốt hơn là nên có một "phương thức factory" tĩnh (có thể là trong một lớp khác), trả về một cá thể (singleton) của DAO của bạn, thực hiện một giao diện cụ thể , được sử dụng bởi mã ứng dụng để truy cập cơ sở dữ liệu:
public interface GreatDAO {
User getUser(int id);
void saveUser(User u);
}
public class TheGreatestDAO implements GreatDAO {
protected TheGeatestDAO(){}
...
}
public class GreatDAOFactory {
private static GreatDAO dao = null;
protected static synchronized GreatDao setDAO(GreatDAO d) {
GreatDAO old = dao;
dao = d;
return old;
}
public static synchronized GreatDAO getDAO() {
return dao == null ? dao = new TheGreatestDAO() : dao;
}
}
public class App {
void setUserName(int id, String name) {
GreatDAO dao = GreatDAOFactory.getDao();
User u = dao.getUser(id);
u.setName(name);
dao.saveUser(u);
}
}
Tại sao cách này ngược lại với phương pháp tĩnh? Vâng, nếu bạn quyết định chuyển sang cơ sở dữ liệu khác thì sao? Đương nhiên, bạn sẽ tạo một lớp DAO mới, thực hiện logic cho bộ nhớ mới của bạn. Nếu bạn đang sử dụng các phương pháp tĩnh, bây giờ bạn sẽ phải đi qua tất cả các mã của bạn, acessing DAO, và thay đổi nó để sử dụng lớp học mới của bạn, phải không? Đây có thể là một nỗi đau lớn. Và nếu bạn thay đổi ý định và muốn chuyển về db cũ thì sao? Với cách tiếp cận này, tất cả những gì bạn cần làm là thay đổi GreatDAOFactory.getDAO()
và làm cho nó tạo ra một thể hiện của một lớp khác, và tất cả mã ứng dụng của bạn sẽ sử dụng cơ sở dữ liệu mới mà không có bất kỳ thay đổi nào. Trong thực tế, điều này thường được thực hiện mà không có bất kỳ thay đổi nào về mã: phương thức factory nhận tên lớp triển khai thông qua cài đặt thuộc tính và khởi tạo nó bằng cách sử dụng sự phản chiếu, vì vậy, tất cả những gì bạn cần làm để chuyển đổi các triển khai là chỉnh sửa tệp thuộc tính.Thực tế, có các khung công tác - như spring
hoặc guice
- quản lý cơ chế "phụ thuộc tiêm" này cho bạn, nhưng tôi sẽ không đi sâu vào chi tiết, đầu tiên, bởi vì nó thực sự nằm ngoài phạm vi câu hỏi của bạn, và nhất thiết phải thuyết phục rằng lợi ích bạn nhận được từ việc sử dụng các khung công tác đó là đáng để gặp rắc rối khi tích hợp với chúng cho hầu hết các ứng dụng.
Một (có thể, nhiều khả năng được lợi dụng) lợi ích của "cách tiếp cận nhà máy" này trái ngược với tĩnh là testability. Hãy tưởng tượng, rằng bạn đang viết một bài kiểm tra đơn vị, mà nên kiểm tra logic của lớp App
của bạn độc lập với bất kỳ DAO cơ bản nào. Bạn không muốn nó sử dụng bất kỳ kho lưu trữ cơ bản nào vì nhiều lý do (tốc độ, phải cài đặt và dọn dẹp các từ sau, có thể va chạm với các thử nghiệm khác, khả năng gây ô nhiễm kết quả kiểm tra với các vấn đề trong DAO, không liên quan đến App
, thực sự đang được thử nghiệm, v.v.) Để thực hiện việc này, bạn muốn có một khung kiểm tra, như Mockito
, cho phép bạn "loại bỏ" chức năng của bất kỳ đối tượng hoặc phương pháp nào, thay thế nó bằng đối tượng "giả", với hành vi được xác định trước (tôi sẽ bỏ qua các chi tiết, bởi vì, điều này một lần nữa nằm ngoài phạm vi). Vì vậy, bạn có thể tạo đối tượng giả này thay thế DAO của bạn và làm cho số GreatDAOFactory
trả lại giả của bạn thay vì thực tế bằng cách gọi GreatDAOFactory.setDAO(dao)
trước khi thử nghiệm (và khôi phục sau). Nếu bạn đã sử dụng các phương thức tĩnh thay vì lớp thể hiện, điều này sẽ không thể thực hiện được.
Một lợi ích nữa, đó là kinda tương tự như chuyển đổi cơ sở dữ liệu tôi mô tả ở trên là "pimping lên" dao của bạn với chức năng bổ sung. Giả sử rằng ứng dụng của bạn trở nên chậm hơn khi lượng dữ liệu trong cơ sở dữ liệu phát triển và bạn quyết định rằng bạn cần một lớp bộ nhớ cache. Thực hiện một lớp bao bọc, sử dụng cá thể dao thực (được cung cấp cho nó như một tham số hàm khởi tạo) để truy cập cơ sở dữ liệu và lưu trữ các đối tượng mà nó đọc trong bộ nhớ, để chúng có thể được trả về nhanh hơn. Sau đó, bạn có thể tạo GreatDAOFactory.getDAO
khởi tạo trình bao bọc này, để ứng dụng tận dụng nó.
(Điều này được gọi là "mẫu ủy quyền" ... và có vẻ như đau ở mông, đặc biệt là khi bạn có nhiều phương pháp được xác định trong DAO của bạn: bạn sẽ phải thực hiện tất cả chúng trong trình bao, thậm chí thay đổi Ngoài ra, bạn có thể chỉ đơn giản là phân lớp dao của bạn, và thêm bộ nhớ đệm vào nó theo cách này.Điều này sẽ là rất ít nhàm chán mã hóa trả trước, nhưng có thể trở thành vấn đề khi bạn quyết định thay đổi cơ sở dữ liệu, hoặc tệ hơn, có tùy chọn chuyển đổi triển khai qua lại).
Một đều sử dụng rộng rãi (nhưng, theo ý kiến của tôi, kém) thay thế cho phương pháp "nhà máy" đang thực hiện dao
một biến thành viên trong tất cả các lớp học mà cần nó:
public class App {
GreatDao dao;
public App(GreatDao d) { dao = d; }
}
Bằng cách này, các mã mà instantiates các lớp cần phải nhanh chóng đối tượng dao (vẫn có thể sử dụng nhà máy), và cung cấp nó như là một tham số constructor. Các khuôn khổ tiêm phụ thuộc tôi đã đề cập ở trên, thường làm một cái gì đó tương tự như thế này.
Điều này cung cấp tất cả lợi ích của phương pháp "phương pháp nhà máy", mà tôi đã giải thích trước đó, nhưng, như tôi đã nói, không tốt như ý kiến của tôi. Những nhược điểm ở đây là phải viết một hàm khởi tạo cho mỗi lớp ứng dụng của bạn, thực hiện cùng một điều chính xác, và không thể khởi tạo các lớp dễ dàng khi cần, và một số mất khả năng đọc: với một cơ sở mã đủ lớn , người đọc mã của bạn, không quen thuộc với nó, sẽ có thời gian khó hiểu việc thực hiện dao nào được thực hiện, cách nó được khởi tạo, cho dù đó là singleton, thực hiện an toàn luồng, cho dù nó giữ trạng thái hay cache bất cứ điều gì, làm thế nào các quyết định lựa chọn một thực hiện cụ thể được thực hiện vv
Tôi thực sự sẽ trả về một '' 'boolean''' để lưu/xóa các hoạt động để chỉ ra rằng tồn tại trên đĩa/cơ sở dữ liệu đã được thực hiện thành công. – ambodi
Không, bạn muốn ném một ngoại lệ nếu nó thất bại: nó cho phép bạn giao tiếp rất nhiều thông tin có giá trị về sự thất bại trở lại người gọi, so với chỉ trả về false, và cũng làm cho việc xử lý lỗi ở phía người gọi ít cồng kềnh : thay vì kiểm tra trạng thái của mọi cuộc gọi và tuyên truyền tất cả các con đường lên ngăn xếp, họ sẽ chỉ phải bắt ngoại lệ một lần, ở cấp độ cần xử lý. – Dima
Đây là câu trả lời được đánh giá thấp, được thực hiện tốt! Một câu hỏi, tại sao 'setDAO' trả về DAO cũ/cũ hơn là chỉ trả lại không có gì? Làm thế nào mà có thể được sử dụng? –