Quản lý tham số trong ứng dụng OOP


15

Tôi đang viết một ứng dụng OOP cỡ trung bình trong C ++ như một cách để thực hành các nguyên tắc OOP.

Tôi có một vài lớp trong dự án của mình và một số trong chúng cần truy cập các tham số cấu hình thời gian chạy. Các tham số này được đọc từ một số nguồn trong quá trình khởi động ứng dụng. Một số được đọc từ một tệp cấu hình trong thư mục gốc của người dùng, một số là đối số dòng lệnh (argv).

Vì vậy, tôi đã tạo ra một lớp học ConfigBlock. Lớp này đọc tất cả các nguồn tham số và lưu trữ nó trong một cấu trúc dữ liệu thích hợp. Ví dụ là tên đường dẫn và tên tệp có thể được thay đổi bởi người dùng trong tệp cấu hình hoặc cờ --verbose CLI. Sau đó, người ta có thể gọi ConfigBlock.GetVerboseLevel()để đọc thông số cụ thể này.

Câu hỏi của tôi: Có tốt không khi thu thập tất cả dữ liệu cấu hình thời gian chạy như vậy trong một lớp?

Sau đó, các lớp học của tôi cần truy cập vào tất cả các tham số này. Tôi có thể nghĩ ra một số cách để đạt được điều này, nhưng tôi không chắc nên chọn cách nào. Hàm tạo của lớp có thể là một tham chiếu đã cho cho ConfigBlock của tôi, như

public:
    MyGreatClass(ConfigBlock &config);

Hoặc chúng chỉ bao gồm một tiêu đề "CodingBlock.h" chứa định nghĩa về CodingBlock của tôi:

extern CodingBlock MyCodingBlock;

Sau đó, chỉ có tệp .cpp lớp cần bao gồm và sử dụng công cụ ConfigBlock.
Tệp .h không giới thiệu giao diện này cho người dùng của lớp. Tuy nhiên, giao diện cho ConfigBlock vẫn còn đó, tuy nhiên, nó bị ẩn khỏi tệp .h.

Có tốt để che giấu nó theo cách này?

Tôi muốn giao diện càng nhỏ càng tốt, nhưng cuối cùng, tôi đoán mỗi lớp cần tham số cấu hình phải có kết nối với ConfigBlock của tôi. Nhưng, kết nối này nên như thế nào?

Câu trả lời:


10

Tôi khá là người thực dụng, nhưng mối quan tâm chính của tôi ở đây là bạn có thể cho phép điều này ConfigBlockchi phối các thiết kế giao diện của bạn theo một cách có thể xấu. Khi bạn có một cái gì đó như thế này:

explicit MyGreatClass(const ConfigBlock& config);

... một giao diện phù hợp hơn có thể như thế này:

MyGreatClass(int foo, float bar, const string& baz);

... Trái ngược với việc anh đào hái những foo/bar/bazcánh đồng này trong số lượng lớn ConfigBlock.

Thiết kế giao diện lười biếng

Về mặt tích cực, kiểu thiết kế này giúp bạn dễ dàng thiết kế giao diện ổn định cho nhà xây dựng của mình, ví dụ: nếu cuối cùng bạn cần một cái gì đó mới, bạn có thể tải nó vào ConfigBlock(có thể không có bất kỳ thay đổi mã nào) và sau đó là cherry- chọn bất cứ thứ gì mới mà bạn cần mà không có bất kỳ loại thay đổi giao diện nào, chỉ thay đổi cách thực hiện MyGreatClass.

Vì vậy, cả hai đều là một chuyên gia và lừa đảo rằng điều này giải phóng bạn trong việc thiết kế một giao diện được suy nghĩ cẩn thận hơn, chỉ chấp nhận các đầu vào mà nó thực sự cần. Nó áp dụng suy nghĩ, "Chỉ cần cung cấp cho tôi kho dữ liệu khổng lồ này, tôi sẽ chọn ra những gì tôi cần từ nó" trái ngược với một cái gì đó giống như, "Những thông số chính xác này là những gì giao diện này cần để hoạt động."

Vì vậy, chắc chắn có một số ưu điểm ở đây, nhưng chúng có thể vượt trội hơn nhiều so với nhược điểm.

Khớp nối

Trong kịch bản này, tất cả các lớp như vậy được xây dựng từ một ConfigBlockthể hiện cuối cùng có các phụ thuộc của chúng trông như thế này:

nhập mô tả hình ảnh ở đây

Điều này có thể trở thành PITA, ví dụ, nếu bạn muốn kiểm tra đơn vị Class2trong sơ đồ này một cách cô lập. Bạn có thể phải mô phỏng bề ngoài các ConfigBlockđầu vào khác nhau có chứa các trường có liên quan quan Class2tâm để có thể kiểm tra nó trong nhiều điều kiện khác nhau.

Trong bất kỳ bối cảnh mới nào (dù là thử nghiệm đơn vị hay toàn bộ dự án mới), bất kỳ lớp nào như vậy đều có thể trở thành gánh nặng cho (tái) sử dụng, vì cuối cùng chúng ta phải luôn mang ConfigBlocktheo khi đi xe, và thiết lập nó phù hợp.

Khả năng sử dụng lại / Khả năng triển khai / Khả năng kiểm tra

Thay vào đó, nếu bạn thiết kế các giao diện này một cách thích hợp, chúng ta có thể tách chúng ra ConfigBlockvà kết thúc bằng một cái gì đó như thế này:

nhập mô tả hình ảnh ở đây

Nếu bạn chú ý trong sơ đồ trên, tất cả các lớp sẽ trở nên độc lập (các khớp nối hướng tâm / hướng ra của chúng giảm đi 1).

Điều này dẫn đến các lớp độc lập hơn (ít nhất là độc lập ConfigBlock), có thể dễ dàng hơn rất nhiều để sử dụng / kiểm tra lại trong các kịch bản / dự án mới.

Bây giờ Clientmã này kết thúc là mã phải phụ thuộc vào mọi thứ và lắp ráp tất cả lại với nhau. Gánh nặng cuối cùng được chuyển đến mã máy khách này để đọc các trường thích hợp từ a ConfigBlockvà chuyển chúng vào các lớp thích hợp làm tham số. Tuy nhiên, mã khách hàng như vậy thường được thiết kế hẹp cho một bối cảnh cụ thể và tiềm năng tái sử dụng của nó thường sẽ là zilch hoặc đóng lại (dù sao đó có thể là mainchức năng điểm vào ứng dụng của bạn hoặc đại loại như thế).

Vì vậy, từ quan điểm tái sử dụng và thử nghiệm, nó có thể giúp làm cho các lớp này độc lập hơn. Từ quan điểm giao diện cho những người sử dụng các lớp của bạn, nó cũng có thể giúp nêu rõ các tham số họ cần thay vì chỉ một khối lớn ConfigBlockmô hình toàn bộ vũ trụ của các trường dữ liệu cần thiết cho mọi thứ.

Phần kết luận

Nói chung, loại thiết kế hướng lớp này phụ thuộc vào một khối nguyên khối có mọi thứ cần thiết có xu hướng có các loại đặc điểm này. Khả năng ứng dụng, khả năng triển khai, tái sử dụng, khả năng kiểm tra của họ, vv có thể bị suy giảm đáng kể do đó. Tuy nhiên, họ có thể đơn giản hóa thiết kế giao diện nếu chúng ta thử một vòng quay tích cực trên nó. Tùy thuộc vào bạn để đo lường những ưu và nhược điểm đó và quyết định xem sự đánh đổi có xứng đáng hay không. Thông thường, sẽ an toàn hơn nhiều khi nhầm lẫn với kiểu thiết kế này khi bạn chọn anh đào từ một khối nguyên khối trong các lớp thường được dự định để mô hình hóa một thiết kế chung hơn và có thể áp dụng rộng rãi.

Cuối cùng nhưng không kém phần quan trọng:

extern CodingBlock MyCodingBlock;

... điều này thậm chí còn tệ hơn (sai lệch hơn?) Về các đặc điểm được mô tả ở trên so với phương pháp tiêm phụ thuộc, vì nó kết thúc việc ghép các lớp của bạn không chỉ với ConfigBlocks, mà trực tiếp với một thể hiện cụ thể của nó. Điều đó càng làm giảm khả năng ứng dụng / khả năng triển khai / kiểm tra.

Lời khuyên chung của tôi sẽ sai về phía thiết kế giao diện không phụ thuộc vào các loại nguyên khối này để cung cấp các tham số của chúng, ít nhất là cho các lớp áp dụng chung nhất mà bạn thiết kế. Và tránh cách tiếp cận toàn cầu mà không cần tiêm phụ thuộc nếu bạn có thể trừ khi bạn thực sự có một lý do rất mạnh mẽ và tự tin để không tránh nó.


1

Thông thường cấu hình của một ứng dụng được tiêu thụ chủ yếu bởi các đối tượng nhà máy. Bất kỳ đối tượng nào dựa vào cấu hình nên được tạo từ một trong các đối tượng xuất xưởng đó. Bạn có thể sử dụng Mẫu nhà máy trừu tượng để triển khai một lớp có trong toàn bộ ConfigBlockđối tượng. Lớp này sẽ đưa ra các phương thức công khai để trả về các đối tượng nhà máy khác và sẽ chỉ chuyển vào một phần của ConfigBlockđối tượng nhà máy cụ thể đó. Bằng cách đó, các thiết lập cấu hình "nhỏ giọt" từ ConfigBlockđối tượng đến các thành viên của nó và từ nhà máy của Nhà máy đến các nhà máy.

Tôi sẽ sử dụng C # vì tôi biết ngôn ngữ tốt hơn, nhưng điều này có thể dễ dàng chuyển sang C ++.

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

Sau đó, bạn cần một số loại lớp hoạt động như "ứng dụng" được khởi tạo trong thủ tục chính của bạn:

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

Như một lưu ý cuối cùng, đây được gọi là "đối tượng nhà cung cấp" trong .NET speak. Các đối tượng nhà cung cấp trong .NET dường như kết hợp dữ liệu cấu hình với các đối tượng xuất xưởng, về cơ bản là những gì bạn muốn làm ở đây.

Xem thêm Mẫu nhà cung cấp cho người mới bắt đầu . Một lần nữa, điều này hướng đến sự phát triển .NET, nhưng với C # và C ++ đều là ngôn ngữ hướng đối tượng, mẫu nên chủ yếu có thể chuyển đổi giữa hai loại.

Một cách đọc tốt khác có liên quan đến mẫu này: Mô hình nhà cung cấp .

Cuối cùng, một bài phê bình về mẫu này: Nhà cung cấp không phải là một mẫu


Tất cả đều tốt, ngoại trừ các liên kết đến các mô hình nhà cung cấp. Sự phản chiếu không được hỗ trợ bởi c ++ và điều đó sẽ không hoạt động.
BЈовић

@ BЈовић: Đúng. Sự phản chiếu của lớp không tồn tại, tuy nhiên bạn có thể xây dựng một cách giải quyết thủ công, về cơ bản sẽ phá hủy một switchcâu lệnh hoặc một ifcâu lệnh kiểm tra đối với một giá trị được đọc từ các tệp cấu hình.
Greg Burghardt

0

Câu hỏi đầu tiên: có tốt không khi thu thập tất cả dữ liệu cấu hình thời gian chạy như vậy trong một lớp?

Đúng. Tốt hơn là tập trung các hằng số và giá trị thời gian chạy và mã để đọc chúng.

Hàm tạo của lớp có thể là một tham chiếu đã cho cho Cấu hình khóa của tôi

Điều này là xấu: hầu hết các nhà xây dựng của bạn sẽ không cần hầu hết các giá trị. Thay vào đó, hãy tạo giao diện cho mọi thứ không tầm thường để xây dựng:

mã cũ (đề xuất của bạn):

MyGreatClass(ConfigBlock &config);

Mã mới:

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

khởi tạo MyGreatClass:

auto x = MyGreatClass{ current_config_block.great_class_values() };

Ở đây, current_config_blocklà một thể hiện của ConfigBlocklớp của bạn (lớp chứa tất cả các giá trị của bạn) và MyGreatClasslớp nhận được một GreatClassDatathể hiện. Nói cách khác, chỉ chuyển cho các nhà xây dựng dữ liệu họ cần và thêm các phương tiện vào của bạn ConfigBlockđể tạo dữ liệu đó.

Hoặc chúng chỉ bao gồm một tiêu đề "CodingBlock.h" chứa định nghĩa về CodingBlock của tôi:

 extern CodingBlock MyCodingBlock;

Sau đó, chỉ có tệp .cpp lớp cần bao gồm và sử dụng công cụ ConfigBlock. Tệp .h không giới thiệu giao diện này cho người dùng của lớp. Tuy nhiên, giao diện cho ConfigBlock vẫn còn đó, tuy nhiên, nó bị ẩn khỏi tệp .h. Có tốt để che giấu nó theo cách này?

Mã này gợi ý rằng bạn sẽ có một phiên bản CodingBlock toàn cầu. Đừng làm điều đó: thông thường bạn nên có một cá thể được khai báo trên toàn cầu, trong bất kỳ điểm nhập nào mà ứng dụng của bạn sử dụng (hàm chính, DLLMain, v.v.) và chuyển xung quanh đó thành một đối số bất cứ nơi nào bạn cần (nhưng như đã giải thích ở trên, bạn không nên vượt qua toàn bộ lớp xung quanh, chỉ hiển thị các giao diện xung quanh dữ liệu và truyền các giao diện đó).

Ngoài ra, không buộc các lớp khách hàng của bạn (của bạn MyGreatClass) với loại CodingBlock; Điều này có nghĩa là, nếu bạn MyGreatClassnhận một chuỗi và năm số nguyên, bạn sẽ tốt hơn khi chuyển vào chuỗi và số nguyên đó, hơn là bạn sẽ truyền vào a CodingBlock.


Tôi nghĩ rằng đó là một ý tưởng tốt để tách các nhà máy khỏi cấu hình. Điều không thỏa đáng là việc triển khai cấu hình phải biết cách khởi tạo các thành phần, vì điều này nhất thiết dẫn đến sự phụ thuộc 2 chiều trong đó trước đây, chỉ tồn tại phụ thuộc 1 chiều. Điều này có ý nghĩa rất lớn khi mở rộng mã của bạn, đặc biệt là khi sử dụng các thư viện dùng chung nơi giao diện thực sự quan trọng
Joel Cornett

0

Câu trả lời ngắn:

Bạn không cần tất cả các cài đặt cho từng mô-đun / lớp trong mã của mình. Nếu bạn làm như vậy, thì có một cái gì đó sai với thiết kế hướng đối tượng của bạn. Đặc biệt trong trường hợp kiểm thử đơn vị, thiết lập tất cả các biến mà bạn không cần và chuyển đối tượng đó sẽ không giúp ích cho việc đọc hoặc duy trì.


Bằng cách này, tôi có thể thu thập mã trình phân tích cú pháp (phân tích cú pháp dòng lệnh và tệp cấu hình) tại một vị trí trung tâm. Sau đó, mỗi lớp có thể chọn các tham số có liên quan từ đó. Theo bạn, một thiết kế tốt là gì?
lugge86

Có lẽ tôi vừa viết sai - ý tôi là bạn có (và đó là một cách thực hành tốt) để có một sự trừu tượng hóa chung với tất cả các cài đặt nhận được từ các biến cấu hình tệp / môi trường - có thể là ConfigBlocklớp của bạn . Vấn đề ở đây là không cung cấp tất cả, trong trường hợp này, bối cảnh của trạng thái hệ thống, chỉ là các giá trị cụ thể, bắt buộc để làm như vậy.
Dawid Pura
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.