Đây thực sự là một câu hỏi quan trọng và nó thường được thực hiện sai vì nó không đủ tầm quan trọng mặc dù nó là một phần cốt lõi của khá nhiều ứng dụng. Dưới đây là hướng dẫn của tôi:
Lớp cấu hình của bạn, chứa tất cả các cài đặt sẽ chỉ là một kiểu dữ liệu cũ, struct / class:
class Config {
int prop1;
float prop2;
SubConfig subConfig;
}
Nó không cần phải có các phương thức và không nên liên quan đến kế thừa (trừ khi đó là lựa chọn duy nhất bạn có trong ngôn ngữ của mình để thực hiện một trường biến thể - xem đoạn tiếp theo). Nó có thể và nên sử dụng thành phần để nhóm các cài đặt thành các lớp cấu hình cụ thể nhỏ hơn (ví dụ: subConfig ở trên). Nếu bạn làm theo cách này, sẽ rất lý tưởng để vượt qua trong các bài kiểm tra đơn vị và ứng dụng nói chung vì nó sẽ có các phụ thuộc tối thiểu.
Bạn có thể sẽ cần sử dụng các loại biến thể, trong trường hợp cấu hình cho các thiết lập khác nhau có cấu trúc không đồng nhất. Chúng tôi chấp nhận rằng bạn sẽ cần đặt một động lực vào một lúc nào đó khi bạn đọc giá trị để chuyển nó sang lớp cấu hình (phụ) bên phải và không nghi ngờ gì điều này sẽ phụ thuộc vào cài đặt cấu hình khác.
Bạn không nên lười biếng trong việc nhập tất cả các cài đặt dưới dạng các trường bằng cách thực hiện điều này:
class Config {
Dictionary<string, string> values;
};
Điều này rất hấp dẫn vì nó có nghĩa là bạn có thể viết một lớp tuần tự hóa tổng quát mà không cần biết nó đang xử lý những lĩnh vực nào, nhưng nó sai và tôi sẽ giải thích tại sao ngay lập tức.
Việc tuần tự hóa cấu hình được thực hiện trong một lớp hoàn toàn riêng biệt. Bất kể API hoặc thư viện nào bạn sử dụng để thực hiện việc này, phần thân của chức năng tuần tự hóa của bạn sẽ chứa các mục cơ bản tương đương với bản đồ từ đường dẫn / khóa trong tệp đến trường trên đối tượng. Một số ngôn ngữ cung cấp khả năng hướng nội tốt và có thể thực hiện điều này cho bạn, những ngôn ngữ khác bạn sẽ phải viết rõ ràng ánh xạ, nhưng điều quan trọng là bạn chỉ phải viết ánh xạ một lần. Ví dụ, hãy xem xét trích xuất này, tôi đã điều chỉnh từ tài liệu phân tích cú pháp tùy chọn chương trình tăng cường c ++:
struct Config {
int opt;
} conf;
po::options_description desc("Allowed options");
desc.add_options()
("optimization", po::value<int>(&conf.opt)->default_value(10);
Lưu ý rằng về cơ bản dòng cuối cùng nói "tối ưu hóa" ánh xạ tới Config :: opt và cũng có một tuyên bố về loại mà bạn mong đợi. Bạn muốn đọc cấu hình không thành công nếu kiểu không như bạn mong đợi, nếu tham số trong tệp không thực sự là dấu phẩy hoặc int hoặc không tồn tại. Lỗi thất bại sẽ xảy ra khi bạn đọc tệp vì vấn đề là do định dạng / xác thực của tệp và bạn nên ném mã ngoại trừ / trả lại và báo cáo vấn đề chính xác. Bạn không nên trì hoãn điều này để sau này trong chương trình. Đó là lý do tại sao bạn không nên bắt tất cả các kiểu từ điển Conf như đã đề cập ở trên, điều này sẽ không thất bại khi tệp được đọc - vì quá trình truyền bị trì hoãn cho đến khi cần giá trị.
Bạn nên làm cho lớp Cấu hình chỉ đọc theo kiểu nào đó - thiết lập nội dung của lớp một lần khi bạn tạo và khởi tạo nó từ tệp. Nếu bạn cần phải có các cài đặt động trong ứng dụng của mình thay đổi, cũng như các cài đặt không thay đổi, bạn nên có một lớp riêng để xử lý các cài đặt động thay vì cố gắng cho phép các bit của lớp cấu hình của bạn không chỉ đọc .
Lý tưởng nhất là bạn đọc tệp ở một nơi trong chương trình của bạn, tức là bạn chỉ có một thể hiện của " ConfigReader
". Tuy nhiên, nếu bạn đang vật lộn để đưa phiên bản Cấu hình được chuyển đến nơi bạn cần, thì tốt hơn là nên có một Trình cấu hình thứ hai hơn là giới thiệu một cấu hình toàn cầu (mà tôi đoán là OP có nghĩa là "tĩnh" "), Điều này dẫn tôi đến điểm tiếp theo của tôi:
Tránh bài hát tiếng còi quyến rũ của người độc thân: "Tôi sẽ cứu bạn phải vượt qua lớp học đó, tất cả các nhà xây dựng của bạn sẽ đáng yêu và sạch sẽ. Tiếp tục, nó sẽ rất dễ dàng." Sự thật là với một kiến trúc có thể kiểm tra được thiết kế tốt, bạn sẽ khó có thể vượt qua lớp Cấu hình hoặc các phần của nó qua nhiều lớp ứng dụng của bạn. Những gì bạn sẽ tìm thấy, trong lớp cấp cao nhất, hàm main () hoặc bất cứ thứ gì, bạn sẽ làm sáng tỏ các giá trị riêng lẻ, mà bạn sẽ cung cấp cho các lớp thành phần của mình dưới dạng các đối số mà sau đó bạn đặt lại với nhau (phụ thuộc thủ công mũi tiêm). Một conf đơn / toàn cầu / tĩnh sẽ làm cho đơn vị kiểm tra ứng dụng của bạn khó thực hiện và hiểu hơn - ví dụ: nó sẽ gây nhầm lẫn cho các nhà phát triển mới cho nhóm của bạn, những người sẽ không biết họ phải đặt trạng thái toàn cầu để kiểm tra nội dung.
Nếu ngôn ngữ của bạn hỗ trợ các thuộc tính, bạn nên sử dụng chúng cho mục đích này. Lý do là nó có nghĩa là sẽ rất dễ dàng để thêm các cài đặt cấu hình 'dẫn xuất' phụ thuộc vào một hoặc nhiều cài đặt khác. ví dụ
int Prop1 { get; }
int Prop2 { get; }
int Prop3 { get { return Prop1*Prop2; }
Nếu ngôn ngữ của bạn không hỗ trợ thành ngữ thuộc tính, nó có thể có một cách giải quyết để đạt được hiệu quả tương tự hoặc bạn chỉ cần tạo một lớp bao bọc cung cấp các cài đặt phần thưởng. Nếu bạn không thể mang lại lợi ích của các thuộc tính thì thật lãng phí thời gian để viết thủ công và sử dụng getters / setters chỉ đơn giản cho mục đích làm hài lòng một vị thần OO. Bạn sẽ tốt hơn với một lĩnh vực cũ đơn giản.
Bạn có thể cần một hệ thống để hợp nhất và lấy nhiều cấu hình từ các vị trí khác nhau theo thứ tự ưu tiên. Thứ tự ưu tiên đó phải được xác định rõ và được hiểu bởi tất cả các nhà phát triển / người dùng, ví dụ như xem xét sổ đăng ký HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE. Bạn nên thực hiện kiểu chức năng này để bạn có thể giữ cấu hình của mình chỉ đọc, tức là:
final_conf = merge(user_conf, machine_conf)
thay vì:
conf.update(user_conf)
Cuối cùng tôi cũng nên thêm rằng tất nhiên nếu khung / ngôn ngữ đã chọn của bạn cung cấp các cơ chế cấu hình nổi tiếng, tích hợp sẵn của riêng bạn, bạn nên xem xét các lợi ích của việc sử dụng đó thay vì tự lăn.
Vì thế. Rất nhiều khía cạnh để xem xét - làm cho đúng và nó sẽ ảnh hưởng sâu sắc đến kiến trúc ứng dụng của bạn, giảm lỗi, khiến mọi thứ dễ kiểm tra và buộc bạn phải sử dụng thiết kế tốt ở nơi khác.