2014-12-31 18 views
8

Tôi có một ứng dụng sử dụng cơ sở dữ liệu (MongoDB) để lưu trữ thông tin. Trong quá khứ, tôi đã sử dụng một lớp đầy đủ các phương thức tĩnh để lưu và truy xuất dữ liệu nhưng từ đó tôi đã nhận ra điều này không phải là một định hướng đối tượng-ish hoặc tương lai.Mẫu thiết kế cho lớp truy cập dữ liệu

Mặc dù rất ít khả năng tôi sẽ thay đổi cơ sở dữ liệu nhưng tôi muốn thay vì cái gì đó không gắn chặt tôi với Mongo. Tôi cũng muốn có thể lưu trữ kết quả với tùy chọn để làm mới đối tượng được lưu trong bộ nhớ cache từ cơ sở dữ liệu nhưng điều này không cần thiết và có thể được thực hiện ở một nơi khác.

Tôi đã xem xét các đối tượng truy cập dữ liệu nhưng dường như chúng không được xác định rõ ràng và tôi không thể tìm thấy bất kỳ ví dụ thực hiện tốt nào (Trong Java hoặc một ngôn ngữ tương tự). Tôi cũng có nhiều trường hợp tắt như tìm tên người dùng để hoàn thành tab có vẻ không phù hợp và sẽ làm cho DAO lớn và cồng kềnh.

Có bất kỳ mẫu thiết kế nào có thể tạo điều kiện nhận và lưu các đối tượng mà không quá cơ sở dữ liệu cụ thể không? Các ví dụ thực hiện tốt sẽ hữu ích (tốt nhất là trong Java).

Trả lời

33

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

+0

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

+1

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

+1

Đâ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? –

Các vấn đề liên quan