我正在尝试在学习PHP OOP的同时实现最佳实践。我理解这个概念,但对正确实施有点怀疑。当我试图弄清楚基本的实现原理时,我没有在这段代码中实现 DI 容器。
结构
-
用于数据库连接的 Db 类。
设置 类,从数据库中检索设置。
语言类,检索特定语言的信息。
页面类、产品类、客户类等等。
想法
设置类需要 Db 类来检索设置。
语言类需要 Db 和设置才能根据数据库中的设置检索信息。
页面类需要 Db、设置和语言。将来可能还需要一些其他类。
简化代码
Db.php扩展了PDO
学位设置.php
class Settings
{
/* Database instance */
protected $db;
/* Cached settings */
private $settings = array();
public function __construct(Db $db)
{
$this->db = $db;
}
public function load ()
{
$selq = $this->db->query('SELECT setting, value FROM settings');
$this->settings = $selq->fetchAll();
}
}
语言.php
class Languages
{
public $language;
protected $db;
protected $settings;
private $languages = array();
public function __construct(Db $db, Settings $settings)
{
$this->db = $db;
$this->settings = $settings;
// set value for $this->language based on user choice or default settings
...
}
public function load()
{
$this->languages = array();
$selq = $this->db->query('SELECT * FROM languages');
$this->languages = $selq->fetchAll();
}
}
页.php
class Page
{
protected $db;
protected $settings;
protected $language;
public function __construct(Db $db, Settings $settings, Languages $languages)
{
$this->db = $db;
$this->settings = $settings;
$this->languages = $languages;
}
public function load()
{
// load page info from db with certain settings and in proper language
...
}
}
配置.php
$db = new Db;
/* Load all settings */
$settings = new Settings($db);
$settings->load();
/* Load all languages */
$languages = new Languages($db, $settings);
$languages->load();
/* Instantiate page */
$page = new Page($db, $settings, $languages);
我不喜欢一遍又一遍地注入相同的类的想法。这样,我将进入需要注入 10 个类的要点。所以,我的代码从一开始就是错误的。
也许,更好的方法是执行以下操作:
配置.php
$db = new Db;
/* Load all settings */
$settings = new Settings($db);
$settings->load();
/* Load all languages */
$languages = new Languages($settings);
$languages->load();
/* Instantiate page */
$page = new Page($languages);
因为"设置"已经可以访问$db,并且$languages可以访问$db和$settings。但是,以这种方式,我将不得不进行诸如$this->语言->设置->db->之类的调用...
我所有的代码架构似乎都是完全错误的:)应该怎么做?
我会尝试回答我自己的问题,因为我在研究了大量材料之后看到了它。
1. 最佳做法是创建如下对象:
$db = new Db();
$settings = new Settings ($db);
$languages = new Languages ($db, $settings);
// ...
2. 使用 DI 容器。
如果无法编写,请使用现有。有一些,称自己为DI容器而不是它们,例如Pimple(本网站上有几个关于此的帖子(。有些往往更慢,更复杂(Zend,Symfony(,然后是其他的,但也提供了更大的功能。如果你正在阅读这篇文章,那么你可能应该选择更简单的,比如Aura,Auryn,Dice,PHP-DI(按字母顺序(。同样重要的是要知道,适当的 DI 容器(在我看来(应该能够递归遍历依赖项,这意味着查找某个对象所需的依赖项。它们还应提供共享同一对象(如实例$db(的能力。
3. 手动注入依赖项会在尝试动态创建对象时(如果使用前端控制器和路由(时导致很多问题。这就是为什么看到第2点。
在这里查看很好的例子:
https://github.com/rdlowrey/Auryn#user-content-recursive-dependency-instantiation
https://github.com/rdlowrey/Auryn#instance-sharing
要观看的视频:
https://www.youtube.com/watch?v=RlfLCWKxHJ0(这不是PHP,但试着得到这个想法(
有一些想法可以摆脱那些丑陋的链依赖。想想三个具体的A,B,C类,其中A需要B,B需要C,所以隐含的A需要C。这是非常糟糕的软件设计。测试更加复杂,因为
- 如果需要一些集成测试,则需要这三个对象
- 如果你想测试A隔离,那么你需要一个模拟B并禁用ctor,或者也必须模拟C
在这一点上,你应该停止你的设计。要将 A 与 C 分离,您应该创建一个 BI A 依赖和 B 实现的接口。
从:
A -> B
B -> C
我们现在有:
A -> BI
B impl BI
B -> C
我们可以将它们与另一个接口 CI 完全解耦:
A -> BI
B impl BI
B -> CI
C impl CI
现在我们的具体类是解耦的,但是如果我们像将它们分开一样连接起来(例如通过 DI(,则没有利润。因此,下一步是缩小接口,例如A类仅依赖于仅具有A需要的方法的BI。
假设这已经完成,我们需要一个需要一些 B 方法的类 A2。你会发现实际上BI对A2来说很好,因为BI有一些A2需要的方法(我们现在把C和CI排除在外(:
A -> BI
A2 -> BI
B -> BI
几天后,您发现我们的界面 BI 实际上对于 A 来说太大了,您希望重构 BI 以保持 BI 尽可能小。但是您无法重构它,因为 A2 使用了一些您想要删除的方法。
这也可能是糟糕的设计。我们现在能做的是(接口隔离原则(:
A -> BI
A2 -> BI2
B impl BI
B impl BI2
在这种情况下,我们现在可以独立更改接口。无论您的项目使用多少个类,我总是建议对应用程序的某些部分使用某种类型的创建模式,以便从实现 CI 接口的未知实现类中获取(参考第一个示例(对象。
这现在是真正的OOP。今天,您可能有一些过度设计的依赖链来获取 CI,但一夜之间,您梦想着一个开创性的想法,即如何获取 CI 并以不需要更改任何使用 CI 的代码的方式更改它!这很重要。
我总是说:这一切都是为了隐藏实现(当然是在正确的地方(。
现在不要开始让每个类都有一个接口,而是让一些类的耦合发生,然后在你看到需要的地方重构它们。
应用于你的课程,我认为设置是一个getter/setter怪物。让只需要一些设置的类依赖于设置不是一个好主意。我认为这样更好:
Settings -> DB
Settings impl PageSettings
Settings impl CustomerSettings
Settings impl ProductSettings
Page -> PageSettings
Customer -> CustomerSettings
Product -> ProductSettings
我不知道你如何使用你的语言课,但我希望你现在有一个设计软件的想法。当然还有更多。
如果您在另一个对象中使用一个对象的依赖项,那么您将在这两个组件之间创建依赖项。这意味着,如果您需要更改Settings
类的依赖项,它会破坏依赖于具有它的Settings
类的所有内容。
依赖注入库
如果您担心每次都必须手动构建新对象,请查看依赖注入库来处理对象创建(如 Pimple 或 AuraPHP DI(。
使用疙瘩:
// Define this once in your bootstrap
use Pimple'Container;
$container = new Container()
$container['Db'] = function ($c) {
return new Db();
};
$container['Settings'] = function ($c) {
return new Settings($c['Db']);
};
$container['Languages'] = function ($c) {
return new Languages($c['Db'], $c['Settings']);
};
$container['Page'] = function ($c) {
return new Page($c['Db'], $c['Settings'], $c['Languages']);
};
// Where ever you have access to your $container you can use this
// (and it knows how to build your object for you every time).
$page = $container['Page'];
使用访问器注入依赖项
您可以使用访问器函数来设置依赖项:
class Settings {
protected $db;
function setDb(Db $db) {
$this->db = $db;
}
// ...
}
$settings = new Settings();
$settings->setDb(new Db());
AuraPHP DI 支持使用访问器进行依赖注入。
use Aura'Di'Container;
use Aura'Di'Factory;
$di = new Container(new Factory());
$di->set('db', new Db());
$di->set('settings', new Settings());
$di->setter['settings']['setDb'] = $di->get('db');
您可以进一步扩展它,并通过根据您扩展的接口进行注入来自动注入您不想在每个类上手动设置的常见依赖项(如 PSR-3 记录器(。
use Aura'Di'Container;
use Aura'Di'Factory;
use Psr'Log'LoggerAwareTrait;
class Db {
use LoggerAwareTrait;
// ...
}
$di = new Container(new Factory());
$di->set('logger', new MyCustomLogger());
$di->set('db', new Db());
$di->setter['LoggerAwareTrait']['setLogger'] = $di->get('logger');
// $db->logger will contain an instance of MyCustomLogger
// so will any other class that uses LoggerAwareTrait
$db = $di->get('db');
单一责任原则
当然,如果你的类有 10 个不同的依赖项,那么问题可能出在设计上。一个类应该有一个单一的责任。
可能违反单一责任的类的症状 原则:
- 该类有许多实例变量
- 该类有许多公共方法
- 类的每个方法都使用其他实例变量
- 特定任务委托给私有方法
摘自Matthias Noback的"包装设计原理">