Là một đối tượng cấu hình duy nhất là một ý tưởng tồi?


43

Trong hầu hết các ứng dụng của tôi, tôi có một đối tượng "cấu hình" đơn hoặc tĩnh, chịu trách nhiệm đọc các cài đặt khác nhau từ đĩa. Hầu như tất cả các lớp sử dụng nó, cho các mục đích khác nhau. Về cơ bản, nó chỉ là một bảng băm của các cặp tên / giá trị. Nó chỉ đọc, vì vậy tôi đã không quá quan tâm đến thực tế là tôi có quá nhiều trạng thái toàn cầu. Nhưng bây giờ khi tôi bắt đầu thử nghiệm đơn vị, nó bắt đầu trở thành một vấn đề.

Một vấn đề là bạn thường không muốn kiểm tra với cùng cấu hình mà bạn chạy cùng. Có một vài giải pháp cho vấn đề này:

  • Cung cấp cho đối tượng cấu hình một setter CHỈ được sử dụng để thử nghiệm, do đó bạn có thể vượt qua trong các cài đặt khác nhau.
  • Tiếp tục sử dụng một đối tượng cấu hình duy nhất, nhưng thay đổi nó từ một cá thể thành một cá thể mà bạn đi khắp nơi cần thiết. Sau đó, bạn có thể xây dựng nó một lần trong ứng dụng của mình và một lần trong các thử nghiệm của mình với các cài đặt khác nhau.

Nhưng dù bằng cách nào, bạn vẫn gặp phải vấn đề thứ hai: hầu như bất kỳ lớp nào cũng có thể sử dụng đối tượng cấu hình. Vì vậy, trong một bài kiểm tra, bạn cần thiết lập cấu hình cho lớp đang được kiểm tra, nhưng cũng TẤT CẢ các phụ thuộc của nó. Điều này có thể làm cho mã thử nghiệm của bạn xấu xí.

Tôi bắt đầu đi đến kết luận rằng loại đối tượng cấu hình này là một ý tưởng tồi. Bạn nghĩ sao? Một số lựa chọn thay thế là gì? Và làm thế nào để bạn bắt đầu tái cấu trúc một ứng dụng sử dụng cấu hình ở mọi nơi?


Cấu hình được cài đặt bằng cách đọc một tệp trên đĩa, phải không? Vậy tại sao không chỉ có một tập tin "test.config" mà nó có thể đọc được?
Anon.

Điều đó giải quyết vấn đề đầu tiên, nhưng không phải là vấn đề thứ hai.
JW01

@ JW01: Có phải tất cả các bài kiểm tra của bạn đều cần một cấu hình khác biệt rõ ràng không? Bạn sẽ phải thiết lập cấu hình đó ở đâu đó trong khi thử nghiệm, phải không?
Anon.

Thật. Nhưng nếu tôi tiếp tục sử dụng một nhóm cài đặt duy nhất (với một nhóm khác để thử nghiệm), thì tất cả các thử nghiệm của tôi sẽ kết thúc bằng cùng một cài đặt. Và vì tôi có thể muốn kiểm tra hành vi với các cài đặt khác nhau, điều này không lý tưởng.
JW01

"Vì vậy, trong một thử nghiệm, bạn cần thiết lập cấu hình cho lớp đang được thử nghiệm, nhưng cũng TẤT CẢ các phụ thuộc của nó. Điều này có thể làm cho mã thử nghiệm của bạn trở nên xấu xí." Có một ví dụ? Tôi có cảm giác rằng đó không phải là cấu trúc của toàn bộ ứng dụng của bạn kết nối với lớp cấu hình mà là cấu trúc của chính lớp cấu hình đang làm cho mọi thứ trở nên "xấu xí". Không nên thiết lập cấu hình của một lớp tự động cấu hình các phụ thuộc của nó?
AndrewKS

Câu trả lời:


35

Tôi không gặp vấn đề với một đối tượng cấu hình duy nhất và có thể thấy lợi thế trong việc giữ tất cả các cài đặt ở một nơi.

Tuy nhiên, việc sử dụng đối tượng đơn lẻ đó ở mọi nơi sẽ dẫn đến mức độ khớp nối cao giữa đối tượng cấu hình và các lớp sử dụng nó. Nếu bạn cần thay đổi lớp cấu hình, bạn có thể phải truy cập mọi trường hợp sử dụng và kiểm tra xem bạn có bị hỏng gì không.

Một cách để giải quyết vấn đề này là tạo ra nhiều giao diện phơi bày các phần của đối tượng cấu hình cần thiết cho các phần khác nhau của ứng dụng. Thay vì cho phép các lớp khác truy cập vào đối tượng cấu hình, bạn chuyển các thể hiện của giao diện cho các lớp cần nó. Theo cách đó, các phần của ứng dụng sử dụng cấu hình phụ thuộc vào bộ sưu tập các trường nhỏ hơn trong giao diện thay vì toàn bộ lớp cấu hình. Cuối cùng, để kiểm tra đơn vị, bạn có thể tạo một lớp tùy chỉnh thực hiện giao diện.

Nếu bạn muốn khám phá những ý tưởng này hơn nữa, tôi khuyên bạn nên đọc về các nguyên tắc RẮN , đặc biệt là Nguyên tắc phân chia giao diệnNguyên tắc đảo ngược phụ thuộc .


7

Tôi tách riêng các nhóm cài đặt liên quan với giao diện.

Cái gì đó như:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

Vân vân.

Bây giờ, đối với một môi trường thực, một lớp duy nhất sẽ thực hiện nhiều giao diện này. Lớp đó có thể lấy từ DB hoặc cấu hình ứng dụng hoặc những gì có bạn, nhưng thường thì nó biết cách lấy hầu hết các cài đặt.

Nhưng việc tách biệt bằng giao diện làm cho các phụ thuộc thực tế rõ ràng hơn và chi tiết hơn, và đây là một cải tiến rõ ràng cho khả năng kiểm tra.

Nhưng .. tôi không luôn muốn bị buộc phải tiêm hoặc cung cấp các cài đặt như một phần phụ thuộc. Có một lập luận có thể được đưa ra rằng tôi nên, cho sự nhất quán hoặc nhân chứng. Nhưng trong cuộc sống thực, nó dường như là một hạn chế không cần thiết.

Vì vậy, tôi sẽ sử dụng một lớp tĩnh làm mặt tiền, cung cấp mục nhập dễ dàng từ mọi nơi đến các cài đặt và trong lớp tĩnh đó, tôi sẽ phục vụ việc xác định vị trí thực hiện giao diện và nhận cài đặt.

Tôi biết vị trí dịch vụ nhận được sự đồng ý nhanh chóng từ hầu hết, nhưng hãy đối mặt với nó, cung cấp sự phụ thuộc thông qua một nhà xây dựng là một trọng lượng, và đôi khi trọng lượng đó là nhiều hơn tôi quan tâm. Vị trí dịch vụ là giải pháp để duy trì khả năng kiểm tra thông qua lập trình cho các giao diện và cho phép thực hiện nhiều lần trong khi vẫn cung cấp sự thuận tiện (trong một vài tình huống được đo lường cẩn thận và phù hợp) của điểm nhập cảnh đơn lẻ.

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Tôi thấy sự pha trộn này là tốt nhất của tất cả các thế giới.


3

Có, như bạn đã nhận ra, một đối tượng cấu hình toàn cầu làm cho việc kiểm tra đơn vị khó khăn. Có một trình thiết lập "bí mật" để kiểm tra đơn vị là một cách nhanh chóng, mặc dù không hay, có thể rất hữu ích: nó cho phép bạn bắt đầu viết các bài kiểm tra đơn vị, để bạn có thể cấu trúc lại mã của mình theo thiết kế tốt hơn theo thời gian.

(Tham chiếu bắt buộc: Hoạt động hiệu quả với Mã kế thừa . Nó chứa điều này và nhiều thủ thuật vô giá khác để kiểm tra mã kế thừa đơn vị.)

Theo kinh nghiệm của tôi, tốt nhất là có càng ít phụ thuộc vào cấu hình toàn cầu càng tốt. Điều này không nhất thiết phải bằng không - tất cả phụ thuộc vào hoàn cảnh. Có một vài lớp "trình tổ chức" cấp cao truy cập cấu hình chung và truyền các thuộc tính cấu hình thực tế cho các đối tượng mà chúng tạo và sử dụng có thể hoạt động tốt, miễn là các nhà tổ chức chỉ làm điều đó, ví dụ như không chứa nhiều mã có thể kiểm tra được. Điều này cho phép bạn kiểm tra hầu hết hoặc tất cả các chức năng quan trọng có thể kiểm tra đơn vị của ứng dụng, trong khi vẫn không phá vỡ hoàn toàn lược đồ cấu hình vật lý hiện có.

Điều này đã gần với một giải pháp tiêm phụ thuộc chính hãng, như Spring cho Java. Nếu bạn có thể di chuyển đến một khung như vậy, tốt thôi. Nhưng trong cuộc sống thực và đặc biệt là khi giao dịch với một ứng dụng cũ, thường thì việc tái cấu trúc chậm và tỉ mỉ đối với DI là sự thỏa hiệp có thể đạt được tốt nhất.


Tôi thực sự thích cách tiếp cận của bạn và tôi sẽ không đề xuất rằng ý tưởng về một setter nên được giữ bí mật hoặc coi là "hack nhanh" cho vấn đề đó. Nếu bạn chấp nhận lập trường rằng các bài kiểm tra đơn vị là một người dùng / người tiêu dùng / một phần quan trọng của ứng dụng của bạn, thì việc có test_các thuộc tính và phương thức không còn là điều tồi tệ nữa, phải không? Tôi đồng ý rằng giải pháp đích thực cho vấn đề này là sử dụng khung DI, nhưng trong các ví dụ như của bạn, khi làm việc với mã kế thừa hoặc các trường hợp đơn giản khác, không áp dụng mã kiểm tra một cách sạch sẽ.
Filip Dupanović

1
@kRON, đây là những thủ thuật thực tế và hữu ích để làm cho đơn vị mã kế thừa có thể kiểm tra được và tôi cũng đang sử dụng chúng rộng rãi. Tuy nhiên, lý tưởng là mã sản xuất không được chứa bất kỳ tính năng nào được giới thiệu chỉ nhằm mục đích thử nghiệm. Về lâu dài, đây là nơi tôi tái cấu trúc hướng tới.
Péter Török

2

Theo kinh nghiệm của tôi, trong thế giới Java, đây là ý nghĩa của Spring. Nó tạo và quản lý các đối tượng (bean) dựa trên các thuộc tính nhất định được đặt trong thời gian chạy và mặt khác là trong suốt đối với ứng dụng của bạn. Bạn có thể đi với tệp test.config mà Anon. đề cập hoặc bạn có thể bao gồm một số logic trong tệp cấu hình mà Spring sẽ xử lý để đặt thuộc tính dựa trên một số khóa khác (ví dụ: tên máy chủ hoặc môi trường).

Liên quan đến vấn đề thứ hai của bạn, bạn có thể giải quyết vấn đề đó bằng một số kiến ​​trúc tìm kiếm nhưng nó không nghiêm trọng như vẻ ngoài của nó. Điều này có nghĩa là gì trong trường hợp của bạn là, ví dụ, bạn sẽ không có một đối tượng Cấu hình toàn cầu mà nhiều lớp khác sử dụng. Bạn chỉ cần cấu hình các lớp khác thông qua Spring và sau đó không có đối tượng cấu hình vì mọi thứ cần cấu hình đều có được thông qua Spring, vì vậy bạn có thể sử dụng các lớp đó trực tiếp trong mã của mình.


2

Câu hỏi này thực sự là về một vấn đề chung hơn so với cấu hình. Ngay khi đọc từ "singleton", tôi nghĩ ngay đến tất cả các vấn đề liên quan đến mô hình đó, không ít trong số đó là khả năng kiểm tra kém.

Mẫu Singleton "được coi là có hại". Có nghĩa, nó không phải luôn luôn là điều sai, nhưng nó thường là. Nếu bạn đã từng cân nhắc sử dụng một mẫu singleton cho bất cứ điều gì , hãy dừng lại để xem xét:

  • Bạn sẽ bao giờ cần phải phân lớp nó?
  • Bạn có bao giờ cần lập trình đến một giao diện không?
  • Bạn có cần phải chạy thử nghiệm đơn vị chống lại nó?
  • Bạn sẽ cần phải sửa đổi nó thường xuyên?
  • Ứng dụng của bạn có nhằm hỗ trợ nhiều nền tảng / môi trường sản xuất không?
  • Bạn thậm chí có một chút quan tâm về việc sử dụng bộ nhớ?

Nếu câu trả lời của bạn là "có" cho bất kỳ câu hỏi nào trong số này (và có thể là một vài điều khác mà tôi không nghĩ đến), thì có lẽ bạn không muốn sử dụng một đơn. Các cấu hình thường cần rất nhiều tính linh hoạt hơn so với một singleton (hoặc đối với vấn đề đó, bất kỳ lớp không có khả năng nào) có thể cung cấp.

Nếu bạn muốn gần như tất cả các lợi ích của một người độc thân mà không có bất kỳ đau đớn nào, thì hãy sử dụng khung tiêm phụ thuộc như Spring hoặc Castle hoặc bất cứ điều gì có sẵn cho môi trường của bạn. Bằng cách đó, bạn chỉ phải khai báo một lần và container sẽ tự động cung cấp một thể hiện cho bất kỳ nhu cầu nào.


0

Một cách tôi đã xử lý điều này trong C # là có một đối tượng singleton khóa trong quá trình tạo cá thể và khởi tạo tất cả dữ liệu cùng một lúc để sử dụng cho tất cả các đối tượng khách. Nếu singleton này có thể xử lý các cặp khóa / giá trị, bạn có thể lưu trữ bất kỳ cách dữ liệu nào kể cả các khóa phức tạp để sử dụng cho nhiều loại máy khách và loại máy khách khác nhau. Nếu bạn muốn giữ cho nó động và tải dữ liệu khách hàng mới khi cần, bạn có thể xác minh sự tồn tại của khóa chính của máy khách và, nếu thiếu, hãy tải dữ liệu cho máy khách đó, nối nó vào từ điển chính. Cũng có thể là singleton cấu hình chính có thể chứa các tập hợp dữ liệu khách trong đó bội số của cùng một loại máy khách sử dụng cùng một dữ liệu được truy cập thông qua tập tin cấu hình chính. Phần lớn tổ chức đối tượng cấu hình này có thể phụ thuộc vào cách máy khách cấu hình của bạn cần truy cập thông tin này và thông tin đó là tĩnh hay động. Tôi đã sử dụng cả cấu hình khóa / giá trị cũng như API đối tượng cụ thể tùy thuộc vào nhu cầu cần thiết.

Một trong những cấu hình singletons của tôi có thể nhận tin nhắn để tải lại từ cơ sở dữ liệu. Trong trường hợp này, tôi tải vào bộ sưu tập đối tượng thứ hai và chỉ khóa bộ sưu tập chính để trao đổi bộ sưu tập. Điều này ngăn chặn việc đọc trên các chủ đề khác.

Nếu tải từ tệp cấu hình, bạn có thể có cấu trúc phân cấp tệp. Cài đặt trong một tệp có thể chỉ định tệp nào khác sẽ tải. Tôi đã sử dụng cơ chế này với các dịch vụ Windows C # có nhiều thành phần tùy chọn, mỗi thành phần có cài đặt cấu hình riêng. Các mẫu cấu trúc tệp giống nhau trên các tệp, nhưng được tải hoặc không dựa trên cài đặt tệp chính.


0

Lớp bạn đang mô tả nghe có vẻ giống như một đối tượng của Chúa ngoại trừ dữ liệu thay vì các hàm. Đó là vấn đề. Bạn có thể nên đọc và lưu trữ dữ liệu cấu hình trong đối tượng thích hợp trong khi chỉ đọc lại dữ liệu nếu cần thiết vì một số lý do.

Ngoài ra, bạn đang sử dụng Singleton vì một lý do không phù hợp. Singletons chỉ nên được sử dụng khi sự tồn tại của nhiều đối tượng sẽ tạo ra trạng thái xấu. Sử dụng Singleton trong trường hợp này là không phù hợp vì có nhiều hơn một trình đọc cấu hình sẽ không gây ra trạng thái lỗi ngay lập tức. Nếu bạn có nhiều hơn một, có lẽ bạn đã làm sai, nhưng không nhất thiết bạn chỉ có một trình đọc cấu hình.

Cuối cùng, việc tạo một trạng thái toàn cầu như vậy là vi phạm đóng gói vì bạn cho phép nhiều lớp hơn là cần biết truy cập trực tiếp vào dữ liệu.


0

Tôi nghĩ rằng nếu có nhiều cài đặt, với "các cụm" liên quan, thì nên chia chúng thành các giao diện riêng biệt với các triển khai tương ứng - sau đó đưa các giao diện đó vào nơi cần thiết với DI. Bạn có thể kiểm tra, khớp nối thấp hơn và SRP.

Tôi đã được truyền cảm hứng về vấn đề này bởi Joshua Flanagan của Los Techies; ông đã viết một bài báo về vấn đề này một thời gian trước đây.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.