3

最近我一直在尝试创建自己的 PHP 框架,只是为了从中学习(我们可能会研究一些更大、更健壮的生产框架)。我目前的一个设计理念是,大多数核心类主要处理类中的静态函数。

现在几天前,我看到了几篇关于“静态方法是可测试性的死亡”的文章。这让我很担心..是的..我的类主要包含静态方法..我使用静态方法的主要原因是很多类永远不需要多个实例,并且静态方法很容易在全局范围内使用. 现在我知道静态方法实际上并不是最好的做事方式,我正在寻找更好的选择。

想象一下下面的代码来获取一个配置项:

$testcfg = Config::get("test"); // Gets config from "test"
echo $testcfg->foo; // Would output what "foo" contains ofcourse.

/*
 * We cache the newly created instance of the "test" config,
 * so if we need to use it again anywhere in the application,
 * the Config::get() method simply returns that instance.
 */

这是我目前拥有的一个例子。但是根据一些文章,这很糟糕。
现在,我可以这样做,例如,CodeIgniter 这样做,使用:

$testcfg = $this->config->get("test");
echo $testcfg->foo;

就个人而言,我觉得这很难阅读。这就是为什么我更喜欢另一种方式。

所以简而言之,我想我需要一种更好的方法来上课。我不希望配置类有多个实例,保持可读性并轻松访问该类。有任何想法吗?

请注意,我正在寻找一些最佳实践或包括代码示例在内的东西,而不是一些随机的想法。另外,如果我绑定到 $this->class->method 样式模式,那么我会有效地实现它吗?

4

2 回答 2

4

回应 Sébastien Renauld 的评论:这是一篇关于依赖注入 (DI) 和控制反转 (IoC) 的文章,其中包含一些示例,以及一些关于好莱坞原则的额外文字(在框架上工作时非常重要)。

说你的类只需要一个实例并不意味着静态是必须的。事实上,远非如此。如果您浏览此站点,并阅读处理单例“模式”的 PHP 问题,您很快就会发现为什么单例有点被禁止。

我不会详细介绍,但测试和单例不会混用。依赖注入绝对值得仔细研究。我暂时就这样吧。

回答你的问题:
你的例子(Config::get('test'))意味着你在Config某个地方的类中有一个静态属性。现在,如果您已经这样做了,正如您所说,以方便访问给定的数据,想象一下调试您的代码将是多么噩梦,如果该值在某处发生变化......它是一个静态的,所以改变一次,而且到处都变了。找出更改的位置可能比您预期的要难。即便如此,与使用您的代码的人在相同情况下会遇到的问题相比,这算不了什么。 然而,真正的
只有当使用您的代码的人想要测试他/她所做的任何事情时,问题才会开始:如果您想访问给定对象中的实例,该实例已在某个类中实例化,有很多方法可以做所以(尤其是在框架中):

class Application
{//base class of your framework
    private $defaulDB = null;
    public $env = null;
    public function __construct($env = 'test')
    {
        $this->env = $env;
    }
    private function connectDB(PDO $connection = null)
    {
        if ($connection === null)
        {
            $connection = new PDO();//you know the deal...
        }
        $this->defaultDB = $connection;
    }
    public function getDB(PDO $conn = null)
    {//get connection
        if ($this->defaultDB === null)
        {
            $this->connectDB($conn);
        }
        return $this->defaultDB;
    }
    public function registerController(MyConstroller $controller)
    {//<== magic!
         $controller->registerApplication($this);
         return $this;
    }
}

如您所见,Application该类有一个方法可以将Application实例传递给您的控制器,或者您希望授予对Application类范围的访问权限的框架的任何部分。
请注意,我已将该defaultDB属性声明为私有属性,因此我使用的是 getter。如果我愿意,我可以将连接传递给那个吸气剂。当然,你可以通过这种连接做更多的事情,但我不会费心编写一个完整的框架来向你展示你可以在这里做的一切:)。

基本上,你所有的控制器都会扩展这个MyController类,它可能是一个看起来像这样的抽象类:

abstract class MyController
{
    private $app = null;
    protected $db = null;
    public function __construct(Application $app = null)
    {
        if ($app !== null)
        {
            return $this->registerApplication($app);
        }
    }
    public function registerApplication(Application $app)
    {
        $this->app = $app;
        return $this;
    }
    public function getApplication()
    {
        return $this->app;
    }
}

因此,在您的代码中,您可以轻松地执行以下操作:

$controller = new MyController($this);//assuming the instance is created in the Application class
$controller = new MyController();
$controller->registerApplication($appInstance);

在这两种情况下,您都可以像这样获取单个数据库实例:

$controller->getApplication()->getDB();

如果在这种情况下未设置属性,您可以通过将不同的数据库连接传递给该getDB方法来轻松测试您的框架。defaultDB通过一些额外的工作,您可以同时注册多个数据库连接并随意访问它们:

$controller->getApplication->getDB(new PDO());//pass test connection here...

这绝不是完整的解释,但我想在你最终得到一个巨大的静态(因此无用)代码库之前很快得到这个答案。

回应OP的评论:

关于我将如何应对Config课堂。老实说,我几乎会做与defaultDB上面显示的属性相同的事情。但我可能会允许更有针对性地控制哪些类可以访问配置的哪些部分:

class Application
{
    private $config = null;
    public function __construct($env = 'test', $config = null)
    {//get default config path or use path passed as argument
        $this->config = new Config(parse_ini_file($config));
    }
    public function registerController(MyController $controller)
    {
        $controller->setApplication($this);
    }
    public function registerDB(MyDB $wrapper, $connect = true)
    {//assume MyDB is a wrapper class, that gets the connection data from the config
        $wrapper->setConfig(new Config($this->config->getSection('DB')));
        $this->defaultDB = $wrapper;
        return $this;
    }
}

class MyController
{
    private $app = null;
    public function getApplication()
    {
        return $this->app;
    }
    public function setApplication(Application $app)
    {
        $this->app = $app;
        return $this;
    }
    //Optional:
    public function getConfig()
    {
        return $this->app->getConfig();
    }
    public function getDB()
    {
        return $this->app->getDB();
    }
}

最后两种方法并不是真正需要的,您也可以编写如下内容:

$controller->getApplication()->getConfig();

同样,这个片段有点混乱和不完整,但它确实向您展示了您可以通过将对该类的引用传递给另一个类来“公开”一个类的某些属性。即使属性是私有的,您也可以使用 getter 来访问它们。您还可以使用各种注册方法来控制允许看到的注册对象是什么,就像我在代码片段中使用 DB-wrapper 所做的那样。DB 类不应该处理视图脚本和命名空间,或自动加载器。这就是为什么我只注册配置的 DB 部分。

基本上,您的许多主要组件最终都会共享许多方法。换句话说,他们最终会实现一个给定的接口。对于每个主要组件(假设经典 MVC 模式),您将拥有一个抽象基类和 1 或 2 级子类的继承链:Abstract Controller> DefaultController> ProjectSpecificController
同时,所有这些类都可能期望在构造时将另一个实例传递给它们。看看index.php任何 ZendFW 项目的:

$application = new Zend_Application(APPLICATION_ENV);
$application->bootstrap()->run();

这就是你所能看到的,但在应用程序内部,所有其他类都被实例化了。这就是为什么您可以从任何地方访问 neigh 的原因:所有类都在另一个类中实例化,如下所示:

public function initController(Request $request)
{
    $this->currentController = $request->getController();
    $this->currentController = new $this->currentController($this);
    return $this->currentController->init($request)
                                   ->{$request->getAction().'Action'}();
}

通过传递$this给控制器​​类的构造函数,该类可以使用各种 getter 和 setter 来获取它需要的任何东西……看看上面的例子,它可以使用getDB, 或者getConfig如果需要的话,可以使用该数据。
这就是我修补或使用功能的大多数框架的方式:应用程序启动并确定需要做什么。这就是好莱坞原则或控制反转:应用程序启动,应用程序确定它何时需要什么类。在我提供的链接中,我相信这与创建自己的客户的商店相比:商店是建立的,并决定它想卖什么。为了出售它,它将创造它想要的客户,并为他们提供购买商品所需的手段......

而且,在我忘记之前:是的,这一切都可以在没有一个静态变量的情况下完成,更不用说函数了。我已经建立了自己的框架,而且我从来没有觉得除了“静态”之外别无他法。一开始我确实使用了工厂模式,但很快就放弃了。
恕我直言,一个好的框架是模块化的:你应该能够毫无问题地使用它的一部分(比如 Symfony 的组件)。使用工厂模式会让你假设太多。您假设X 类将可用,这不是给定的。
注册那些可用的类使得组件更加可移植。考虑一下:

class AssumeFactory
{
    private $db = null;
    public function getDB(PDO $db = null)
    {
        if ($db === null)
        {
            $config = Factory::getConfig();//assumes Config class
            $db = new PDO($config->getDBString());
        }
        $this->db = $db;
        return $this->db;
    }
}

相对于:

class RegisteredApplication
{//assume this is registered to current Application
    public function getDB(PDO $fallback = null, $setToApplication = false)
    {
        if ($this->getApplication()->getDB() === null)
        {//defensive
            if ($setToApplication === true && $fallback !== null)
            {
                $this->getApplication()->setDB($fallback);
                return $fallback;//this is current connection
            }
            if ($fallback === null && $this->getApplication()->getConfig() !== null)
            {//if DB is not set @app, check config:
                $fallback = $this->getApplication()->getConfig()->getSection('DB');
                $fallback = new PDO($fallback->connString, $fallback->user, $fallback->pass);
                return $fallback;
            }
            throw new RuntimeException('No DB connection set @app, no fallback');
        }
        if ($setToApplication === true && $fallback !== null)
        {
            $this->getApplication()->setDB($fallback);
        }
        return $this->getApplication()->getDB();
    }
}

尽管后一个版本需要编写更多的工作,但很清楚两者中哪一个更好。第一个版本只是假设太多,并且不允许安全网。这也是相当独裁的:假设我已经编写了一个测试,并且我需要将结果转到另一个数据库。因此,我需要更改整个应用程序的数据库连接(用户输入、错误、统计信息……它们都可能存储在数据库中)。
仅出于这两个原因,第二个片段是更好的候选者:我可以传递另一个数据库连接,它会覆盖应用程序默认值,或者,如果我不想这样做,我可以使用默认连接,或者尝试创建默认连接。存储我刚刚建立的连接,或者不存储......选择完全是我的。如果没有任何效果,我只是得到一个RuntimeException扔给我,但这不是重点。

于 2013-05-22T20:16:20.347 回答
0

魔术方法__get()会帮助你:查看关于和的例子__set()

你还应该看看命名空间:它可以帮助你摆脱一些只使用静态方法的类。

于 2013-05-22T20:12:03.057 回答