博客列表 >MVC实现和依赖注入、服务容器和门面的探讨

MVC实现和依赖注入、服务容器和门面的探讨

吾逍遥
吾逍遥原创
2020年12月15日 15:06:45980浏览

一、MVC设计模式工作原理及简单实现

  • M: Model(模型层),最bottom一层,是核心数据层,程序需要操作的数据或信息.
  • V:View (视图层),最top一层,直接面向最终用户,视图层提供操作页面给用户,被誉为程序的外壳.
  • C:Controller(控制层),是middile层, 它负责根据用户从”视图层”输入的指令,选取”数据层”中的数据,然后对其进行相应的操作,产生最终结果。

mvc

其实日常开发中无论是仿站还是自己开发都基本按这个思路。如进行仿站一般都是从View开始,然后Model,最后想着Controller从Model数据库中读取数据展示到View中给客户浏览。而自己开发则是先考虑数据库Model的设计,怎么展示给客户View和如何Controller将数据库中数据传递给View。

下面就是简单的实现:

  1. //Model.php(模型)
  2. class Model{
  3. function getData(){
  4. $dsn="mysql:host=localhost;dbname=test;charset=UTF8";
  5. try{
  6. $pdo=new PDO($dsn,'root','root');
  7. $sql="SELECT * from user";
  8. return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
  9. }catch(PDOException $e){
  10. echo '数据库连接错误:'.$e->getMessage();
  11. exit();
  12. }
  13. }
  14. }
  1. //View.php(视图)
  2. class View{
  3. function fetch(array $data){
  4. $html="<table border=1 cellspacing='0'>";
  5. $html.="<thead><tr bgColor='lightgray'><th>ID号</th><th>帐号</th><th>密码</th></tr></thead><tbody>";
  6. foreach($data as $user):
  7. $html.="<tr><td>{$user['id']}</td>";
  8. $html.="<td>{$user['uname']}</td>";
  9. $html.="<td>{$user['pwd']}</td></tr>";
  10. endforeach;
  11. $html.="</tbody></table>";
  12. return $html;
  13. }
  14. }
  1. //Controller.php(控制器)
  2. require_once 'Model.php';
  3. require_once 'View.php';
  4. class Controller
  5. {
  6. // 参数注入当前方法中,仅给当前方法使用
  7. function index(Model $model, View $view)
  8. {
  9. return $view->fetch($model->getData());
  10. }
  11. }
  12. // 测试输出代码
  13. $model=new Model();
  14. $view=new View();
  15. echo (new Controller())->index($model,$view);

二、依赖注入(参数注入)

在MVC模式中控制器Controller类中依赖模型类Model实例对象和视图类View实例对象function index(Model $model, View $view),此时我们可以在 当前成员方法中使用参数注入的方式,解决在当前类中使用其它类实例对象的问题,参数注入其实就是参数传递,它可以传递普通变量,也可是对象、闭包。

若是其它类的实例对象需要在当前类的众多成员方法中使用时( 共享或复用 ),若仍然采用将对象参数注入到成员方法中,将要多处写参数注入,此时建议方案就是能完 构造函数的参数注入 方式,将其它类的实例对象赋值给类的成员变量,这样当前类的所有成员方法都可访问,从而实现了其它类的共享或复用。

  1. // 类的共享或复用
  2. class Controller
  3. {
  4. private $model=null;
  5. private $view=null;
  6. // 参数注入构造函数中,赋值给类的成员变量,便于类的成员方法访问(共享或复用)
  7. function __construct(Model $model, View $view)
  8. {
  9. $this->model=$model;
  10. $this->view=$view;
  11. }
  12. function index()
  13. {
  14. return $this->view->fetch($this->model->getData());
  15. }
  16. }
  17. $model=new Model();
  18. $view=new View();
  19. echo (new Controller($model,$view))->index();

补充: 以前看框架时,说优势之一就是使用依赖注入,后来经老师演示,原来就是参数传递,不得不说,好多文档说明故意提高了理解的门槛,看来理解基础再来学框架是最正确的学习路径。

三、服务容器(Container)

如果当前类依赖的对象较多,或者项目中有许多对象和类需要在项目中反复调用,可以将当前依赖的外部对象放到一个”服务容器”中进行统一管理。如TP6框架的Container类,服务容器最基本要有三个成员:对象容器(对象数组)、绑定对象方法和取对象方法。

  1. // 如果当前类依赖的对象较多,可以将当前依赖的外部对象放到一个"服务容器"中进行统一管理
  2. // 服务容器: 一个自动产生类/对象的工厂
  3. class Container
  4. {
  5. // 1、对象容器(对象数组)
  6. protected $instance = [];
  7. /**
  8. * 2、绑定对象:对象容器中添加对象
  9. * $abstract :类标识, 接口, 是外部对象在当前容器数组中的键名/别名 alias *
  10. * $concrete : 要绑定的类, 闭包或者实例, 传入一个对象或者一个闭包,前者需要我们先实例化对象才能往对象容器中添加, 闭包优势:我们使用这个对象的时候,才实例化对象
  11. */
  12. function bind($abstract, Closure $concrete)
  13. {
  14. $this->instance[$abstract] = $concrete;
  15. }
  16. //3.从对象容器中取出对象, 调用
  17. function make($abstract, $args = [])
  18. {
  19. return call_user_func_array($this->instance[$abstract], $args);
  20. }
  21. }

此时依赖注入就不是一个个类了,而是包含这些类的服务容器类Container,其实质仍然是引用类的实例对象,只不过这些实例对象通过bind方法保存到容器类中对象数组中,使用时使用make方法取出即可。

  1. // Controller2.php
  2. require_once 'Model.php';
  3. require_once 'View.php';
  4. require_once '../Container.php';
  5. // 类的共享或复用
  6. class Controller
  7. {
  8. private $model=null;
  9. private $view=null;
  10. // 参数注入构造函数中,赋值给类的成员变量,便于类的成员方法访问(共享或复用)
  11. function __construct(Container $container)
  12. {
  13. $this->model=$container->make('model');
  14. $this->view=$container->make('view');
  15. }
  16. function index()
  17. {
  18. return $this->view->fetch($this->model->getData());
  19. }
  20. }
  21. $container=new Container();
  22. $container->bind('model',function(){
  23. return new Model();
  24. });
  25. $container->bind('view',function(){
  26. return new View();
  27. });
  28. echo (new Controller($container))->index();

四、self、parent和static探讨

本来要讲Facade(门面)技术的,但它静态实现依赖static的后期静态绑定,自PHP 5.3.0起,PHP增加了一个叫做后期静态绑定的功能,用于在继承范围内引用静态调用的类。在群里也咨询老师了,老师的解释是后期静态绑定,是在继承上下文中,self只能和声明类进行绑定,并不能和调用类进行绑定。,同时也查阅了PHP官方的解释,最终梳理成以下几个理解点:

1、self、parent和static到底代表谁?

首先三者都是相当于魔术变量,在实际编译时会指向具体的类,都不能在类外调用,只能在类中使用。至于老师说的声明类和调用类,如何区分声明类和调用类就不好界定。通过代码测试,我的发现它们区别就是 查找类中成员范围起点不同

  • self:从当前类中开始查找成员,若有(重写时)则结束查找,没有(未重写时)则依次向父类查找成员,直到找到为止。如下面代码中self::who();就是先从B类中查找成员,刚好有就输出B
  • parent:直接跳转当前类中成员,即使重写也忽略,从父类开始查找成员,后面查找同self,唯一不同就是起点。如下面代码中parent::who();虽然B类已经定义who方法,但它直接从父类A开始查找,找到who方法,输出为A。
  • static:若是static访问成员,则是从明确指定类名开始查找方法。如下面代码中parent::foo();self::foo();按parent和self规则其实最终找到就是A类中foo方法,但它是static访问成员,所以它要用明确指明类来替换,而下面代码是C::test()导致这种查找,因为C类中没有tes方法,它从父类B中找到,此时明确调用类是C不是B,这点最容易误导。

下面是基于PHP官方区分self、parent和static的代码进行修改版,可以认清三者的查找成员时起点:

  1. class A {
  2. public static function foo() {
  3. static::who();
  4. }
  5. public static function who() {
  6. echo __CLASS__."<br>";
  7. }
  8. }
  9. class B extends A {
  10. public static function test() {
  11. // 指明具体类
  12. A::foo();
  13. // 调用static后期绑定静态成员
  14. parent::foo();
  15. self::foo();
  16. // parent和self区别
  17. parent::who();
  18. self::who();
  19. }
  20. public static function who() {
  21. echo __CLASS__."<br>";
  22. }
  23. }
  24. class C extends B {
  25. public static function who() {
  26. echo __CLASS__."<br>";
  27. }
  28. }
  29. C::test();

static

补充: 正如图片中所说,static也同self和parent,只是指明 查找成员起点不同 而已。若上面代码中C类没有重写who方法,即注释掉C类中who方法,此时由于C类没有who方法,C类调用父类B中who方法,此时输出是:A B B A B。

2、转发调用与非转发调用

  • 转发调用:指的是通过以下几种方式进行的静态调用self::,parent::,static::以及 forward_static_call()
  • 非转发调用:明确指定类名的静态调用 。例如Foo::foo();

上面是某网文对两种调用的解释,其中第一个是官方的解释,但我认为它们只是表面认识。经测试 两种调用都是针对是否重写继承父类的成员 来说的。若是重写了父类的成员,则明确指定类名或self::访问都直接访问当前类中成员,终止了向上查找,是非转发调用。而没有重写父类的成员则无论是指定类名,还是上面几种都是转发调用,上面代码中C::test()虽然指定类名了,但它转发调用B类中test方法。

3、后期静态绑定

官方解释说:存储了在上一个“非转发调用”(non-forwarding call)的类名。其实我觉得关键词是两个”非转发调用”和”类名”,上面代码中C::test()输出static::who();是从C类开始查找who成员方法,结果找到了,所以此时”非转发调用的类名”是C,若是C类中没有重写who方法(即注释掉C类的who方法),在B类中找到who成员方法,此时”非转发调用的类名”是B。所以 “非转发调用的类名”是由指定类名开始的,由继承关系的类组成的队列。下面是官方代码的解析过程,可以具体理解下三者作用。

  1. * C::test(); //非转发调用 ,进入test()调用后,“上一次非转发调用”存储的类名为C
  2. *
  3. * //当前的“上一次非转发调用”存储的类名为C
  4. * public static function test() {
  5. * A::foo(); //非转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为A,然后实际执行代码A::foo(), 转 0-0
  6. * parent::foo(); //转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为C, 此处的parent解析为A ,转1-0
  7. * self::foo(); //转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为C, 此处self解析为B, 转2-0
  8. * }
  9. *
  10. * 0-0
  11. * //当前的“上一次非转发调用”存储的类名为A
  12. * public static function foo() {
  13. * static::who(); //转发调用, 因为当前的“上一次非转发调用”存储的类名为A, 故实际执行代码A::who(),即static代表A,进入who()调用后,“上一次非转发调用”存储的类名依然为A,因此打印 “A”
  14. * }
  15. *
  16. * 1-0
  17. * //当前的“上一次非转发调用”存储的类名为C
  18. * public static function foo() {
  19. * static::who(); //转发调用, 因为当前的“上一次非转发调用”存储的类名为C, 故实际执行代码C::who(),即static代表C,进入who()调用后,“上一次非转发调用”存储的类名依然为C,因此打印 “C”
  20. * }
  21. *
  22. * 2-0
  23. * //当前的“上一次非转发调用”存储的类名为C
  24. * public static function foo() {
  25. * static::who(); //转发调用, 因为当前的“上一次非转发调用”存储的类名为C, 故实际执行代码C::who(),即static代表C,进入who()调用后,“上一次非转发调用”存储的类名依然为C,因此打印 “C”
  26. * }

4、后期静态绑定解决单例继承问题

  1. class A
  2. {
  3. protected static $_instance = null;
  4. static public function getInstance()
  5. {
  6. if (self::$_instance === null) {
  7. self::$_instance = new self();
  8. }
  9. return self::$_instance;
  10. }
  11. }
  12. class B extends A
  13. {
  14. }
  15. class C extends A{
  16. }
  17. $a = A::getInstance();
  18. $b = B::getInstance();
  19. $c = C::getInstance();
  20. var_dump($a);
  21. var_dump($b);
  22. var_dump($c);

static1

上面单例继承问题可通过static::解决,即getInstance()方法中self都替换成static就可以。在TP6框架的服务容器Container类中也是通过static::解决单例继承问题的。

  1. //TP6的Container.php中实例方法
  2. public static function getInstance()
  3. {
  4. if (is_null(static::$instance)) {
  5. static::$instance = new static;
  6. }
  7. if (static::$instance instanceof Closure) {
  8. return (static::$instance)();
  9. }
  10. return static::$instance;
  11. }

五、Facade(门面)

门面类Facade其实相当于中间层,先对想静态调用的类定义继承于门面类的子类,然后在该子类中定义相应的静态方法,实质是再调用对应类的非静态方法。为了相同类名,我参考了TP6框架的做法,将门面类统一命名空间为Facade。下面演示是基于服务容器类的门面类,单独类需要门面类似定义即可。

  1. // Facade.php
  2. namespace Facade;
  3. require_once 'Container.php';
  4. // 门面类
  5. class Facade
  6. {
  7. protected static $container = null;
  8. static function instance(\Container $container)
  9. {
  10. static::$container = $container;
  11. }
  12. }
  13. // 需要静态调用的类继承门面类,为了统一,和原类名相同
  14. class Model extends Facade
  15. {
  16. static function getData()
  17. {
  18. return static::$container->make('model')->getData();
  19. }
  20. }
  21. class View extends Facade
  22. {
  23. static function fetch($data)
  24. {
  25. return static::$container->make('view')->fetch($data);
  26. }
  27. }

上面已经定义了基于服务容器类Container的门面类,将服务容器的对象数组成员赋值给Facade的静态成员,从而被所有继承Facade类的子类共享。

  1. // Controller3.php
  2. require_once 'Facade.php';
  3. require_once 'Model.php';
  4. require_once 'View.php';
  5. use Facade\Facade;
  6. use Facade\Model;
  7. use Facade\View;
  8. // 类的共享或复用
  9. class Controller
  10. {
  11. function __construct(Container $container)
  12. {
  13. Facade::instance($container);
  14. }
  15. function index()
  16. {
  17. return View::fetch(Model::getData());
  18. }
  19. }
  20. $container=new Container();
  21. $container->bind('model',function(){
  22. return new \Model();
  23. });
  24. $container->bind('view',function(){
  25. return new \View();
  26. });
  27. echo (new Controller($container))->index();

补充: 在我的Githubhttps://github.com/woxiaoyao81/phpcn13或Giteehttps://gitee.com/freegroup81/phpcn13可以下载到参考TP6运行模式的《PHP从零开始写MVC》压缩包源码,我是在从TP官网论坛下载到的,可以作为本博文的进阶练习。

声明:本文内容转载自脚本之家,由网友自发贡献,版权归原作者所有,如您发现涉嫌抄袭侵权,请联系admin@php.cn 核实处理。
全部评论
文明上网理性发言,请遵守新闻评论服务协议
吾逍遥2020-12-15 16:25:282楼
声明类关注点是范围解析符::后面的方法,而调用类关注点是::前面调用类,理解了
灭绝师太2020-12-15 15:27:541楼
声明类和调用类很好区分, 声明类就是指一个静态方法的声明类, 如果当前类被继承, 使用子类去访问该静态方法, 那么子类就是该静态方法的调用类.