OOP应用程序中的参数管理


15

我正在用C ++编写一个中等大小的OOP应用程序,作为实践OOP原理的一种方法。

我的项目中有几个类,其中一些需要访问运行时配置参数。这些参数是在应用程序启动期间从多个来源读取的。有些是从用户home-dir中的配置文件读取的,有些是命令行参数(argv)。

所以我创建了一个类ConfigBlock。此类读取所有参数源并将其存储在适当的数据结构中。示例是路径和文件名,用户可以在配置文件或--verbose CLI标志中更改它们。然后,可以调用ConfigBlock.GetVerboseLevel()以读取此特定参数。

我的问题:在一个类中收集所有此类运行时配置数据是一种好习惯吗?

然后,我的班级需要访问所有这些参数。我可以想到几种方法来实现这一目标,但是我不确定该采取哪种方法。类的构造函数可以是对我的ConfigBlock的给定引用,例如

public:
    MyGreatClass(ConfigBlock &config);

或者,它们仅包含标头“ CodingBlock.h”,其中包含我的CodingBlock的定义:

extern CodingBlock MyCodingBlock;

然后,仅类.cpp文件需要包含和使用ConfigBlock内容。
.h文件不会将此接口引入类的用户。但是,ConfigBlock的接口仍然存在,但是从.h文件中隐藏了该接口。

这样隐藏起来好吗?

我希望接口尽可能小,但最后,我想每个需要配置参数的类都必须与我的ConfigBlock连接。但是,这种连接应该是什么样的?

Answers:


10

我非常务实,但是我主要关心的是您可能允许它ConfigBlock以可能不好的方式支配您的界面设计。当你有这样的事情:

explicit MyGreatClass(const ConfigBlock& config);

...更合适的界面可能是这样的:

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

...而不是仅仅foo/bar/baz从大量的樱桃中挑选这些领域ConfigBlock

惰性接口设计

从好的方面来说,这种设计使为构造函数设计一个稳定的接口变得容易,例如,因为如果最终需要一些新的东西,则可以将其加载到ConfigBlock(可能无需任何代码更改)中,然后进行cherry-选择所需的任何新内容,而无需进行任何类型的界面更改,只需更改的实现即可MyGreatClass

因此,从优点和缺点上来说,这使您不必设计更仔细考虑的界面,仅接受实际需要的输入。它采用的思路是:“只要给我这么大量的数据,我就会从中挑出我需要的东西”,而不是诸如“这些精确的参数就是该接口需要工作的东西”之类的东西

因此,这里肯定有一些优点,但是缺点可能会大大超过它们。

耦合

在这种情况下,从ConfigBlock实例构造的所有此类类最终都具有其依赖关系,如下所示:

在此处输入图片说明

例如,如果要Class2在此图中单独进行单元测试,则可以成为PITA 。您可能必须表面上模拟ConfigBlock包含Class2感兴趣字段的各种输入,以便能够在各种条件下对其进行测试。

在任何类型的新环境中(无论是单元测试还是整个新项目),任何此类类最终都将成为(重用)更多的负担,因为我们最终不得不总是随身携带ConfigBlock并进行设置相应地。

可重用性/可部署性/可测试性

相反,如果您适当地设计这些接口,我们可以将它们从中解耦,ConfigBlock并得到如下所示的结果:

在此处输入图片说明

如果您在上面的图表中注意到,所有类别将变为独立的(它们的传入/传出耦合减少1)。

这导致了更多的独立类(至少独立于ConfigBlock),在新的场景/项目中可以更轻松地(重用)/测试这些类。

现在,此Client代码最终成为了必须依赖于所有内容并将其组装在一起的代码。负担最终被转移到此客户端代码,以从a读取适当的字段ConfigBlock并将它们作为参数传递到适当的类中。然而,此类客户端代码通常是针对特定上下文进行狭义设计的,并且无论如何,其重用潜力通常都会变得零落或关闭(它可能是应用程序的main入口点功能或类似功能)。

因此,从可重用性和测试的角度来看,它可以帮助使这些类更加独立。从使用类的人员的界面角度来看,它还可以帮助明确地声明所需的参数,而不是仅一个庞大的参数即可ConfigBlock建模所有内容所需的整个数据字段。

结论

通常,这种基于类的设计依赖于具有所需要的一切的整体,因此倾向于具有这些特性。结果,它们的适用性,可部署性,可重用性,可测试性等可能会大大降低。但是,如果我们尝试对其进行积极调整,它们可以简化界面设计。由您来衡量这些利弊,并决定权衡是否值得。通常,对此类设计犯错误的做法要安全得多,在这种设计中,您从整体中挑选樱桃,通常是为了对更通用和更广泛应用的模型建模。

最后但并非最不重要的:

extern CodingBlock MyCodingBlock;

...就上述特性而言,这比依赖注入方法可能更糟(更偏斜?),因为它最终不仅将您的类耦合到ConfigBlocks,而且还直接耦合到它的特定实例。这进一步降低了适用性/可部署性/可测试性。

我的一般建议是在设计不依赖于此类整体的接口以提供其参数方面,至少对于您设计的最普遍适用的类,这是错误的。如果可以的话,请避免使用没有依赖项注入的全局方法,除非您确实有很强的自信理由不这样做。


1

通常,应用程序的配置主要由工厂对象使用。任何依赖于配置的对象都应从这些工厂对象之一生成。您可以利用Abstract Factory Pattern来实现一个接受整个ConfigBlock对象的类。此类将公开公共方法以返回其他工厂对象,并且仅传递ConfigBlock与该特定工厂对象相关的部分。这样,配置设置就会从ConfigBlock对象到其成员,再从工厂从工厂到工厂。

我将使用C#,因为我对语言有所了解,但这应该可以轻松转移到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);
    }
}

之后,您需要某种类作为在主过程中实例化的“应用程序”的类:

// 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
    }
}

最后一点,在.NET语言中,这称为“提供程序对象”。.NET中的提供程序对象似乎将配置数据嫁接到工厂对象,这实际上是您要在此处执行的操作。

另请参阅初学者的提供者模式。同样,这是面向.NET开发的,但是由于C#和C ++都是面向对象的语言,因此该模式应该可以在两者之间进行转换。

与此模式有关的另一本好书:Provider Model

最后,对这种模式的批评:提供者不是一种模式


除了指向提供程序模型的链接之外,其他所有内容都很好。c ++不支持反射,因此将无法正常工作。
2015年

@BЈовић:正确。类反射不存在,但是您可以构建一个手动解决方法,该方法基本上可以演变成一个switch语句或if针对从配置文件读取的值进行测试的语句。
格雷格·伯格哈特

0

第一个问题:在一个类中收集所有此类运行时配置数据是一种好习惯吗?

是。最好集中运行时常量和值以及读取它们的代码。

类的构造函数可以作为对我的ConfigBlock的引用

这很不好:您的大多数构造函数都不需要大多数值。而是为所有不容易构造的事物创建接口:

旧代码(您的建议):

MyGreatClass(ConfigBlock &config);

新代码:

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

实例化MyGreatClass:

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

这里current_config_block是您的ConfigBlock类的一个实例(包含所有值MyGreatClassGreatClassData实例),并且该类收到一个实例。换句话说,仅将所需的数据传递给构造函数,并向您添加设施ConfigBlock以创建该数据。

或者,它们仅包含标头“ CodingBlock.h”,其中包含我的CodingBlock的定义:

 extern CodingBlock MyCodingBlock;

然后,仅类.cpp文件需要包含和使用ConfigBlock内容。.h文件不会将此接口引入类的用户。但是,ConfigBlock的接口仍然存在,但是从.h文件中隐藏了该接口。这样隐藏起来好吗?

该代码建议您将拥有一个全局CodingBlock实例。不要这样做:通常,您应该在应用程序使用的任何入口点(主函数,DllMain等)中全局声明一个实例,并在需要的地方将其作为参数传递(但如上所述),您不应传递整个类,只需公开数据周围的接口,然后传递这些接口)。

另外,请勿将您的客户类别(您的MyGreatClass)绑定到CodingBlock; 的类型。这意味着,如果您MyGreatClass接受一个字符串和五个整数,则比传入一个字符串和整数更好CodingBlock


我认为将工厂与配置分离是个好主意。配置实现应该知道如何实例化组件是不令人满意的,因为这必然导致2向依赖,而以前只有2向依赖。这在扩展代码时具有重要意义,尤其是在使用接口真正重要的共享库时
Joel Cornett 2015年

0

简短答案:

不需要代码中每个模块/类的所有设置。如果这样做,那么面向对象的设计就会出问题。尤其是在进行单元测试时,设置所有不需要的变量,并且传递该对象将无助于读取或维护。


这样,我可以将解析器代码(解析命令行和配置文件)收集在一个中央位置。然后,每个类都可以从那里挑选它们的相关参数。您认为什么是好的设计?
lugge86

也许我只是写错了-我的意思是(对于您来说,这是一种很好的做法)对来自配置文件/环境变量的所有设置进行通用抽象-可能是您的ConfigBlock类。这里的重点是在这种情况下,不提供所有系统状态的上下文,只是提供特定的所需值。
戴维德·普拉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.