[php]领域模型和数据映射器
业务逻辑层使用的是领域模型,因为它能使用数据映射器中的大部分模式。
“万物皆对象”,领域模型就是对于项目中各种个体的抽象表达,就是一个类。它常常被描述为一组属性及附加的操作。它们是做某些相关事的某个东西。
领域模型的复杂性主要来自于尝试使模型纯粹(pure),即将领域模型从应用中其他层中分离出来。把领域模型的参与者从表现层分离出来不难,但将这些参与者从数据层中分离出来则不太容易。在理想情形下,领域模型应该只包含它要表达和解决的问题,但在现实中领域模型很难完全去除数据库操作。
领域模型常常映射到数据库结构上。通过将模型与数据库分离,整个层会更加容易测试,而且不会受到数据库结构的改变的影响,也不会受到存储机制的影响。领域模型只关心要完成的核心工作和承担的责任。领域模型设计的简单还是复杂取决于业务逻辑的复杂度。
先来个简单例子(之后都是用这个例子):一个Classroom有多个Student,每个Student有个Score。
sql脚本:
CREATE TABLE `classroom` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL, PRIMARY KEY (`id`) ) CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `cid` int(11) NOT NULL, `name` varchar(16) NOT NULL, PRIMARY KEY (`id`), KEY `cs_id` (`cid`), CONSTRAINT `cs_id` FOREIGN KEY (`cid`) REFERENCES `classroom` (`id`) ) CREATE TABLE `score` ( `id` int(11) NOT NULL AUTO_INCREMENT, `sid` int(11) NOT NULL, `course_name` varchar(32) NOT NULL, `score` tinyint(4) NOT NULL, PRIMARY KEY (`id`,`sid`,`course_name`), KEY `sc_id` (`sid`), CONSTRAINT `sc_id` FOREIGN KEY (`sid`) REFERENCES `student` (`id`) )
领域模型抽象基类:DomainObject
namespace demo\domain; /** * 领域模型抽象基类 */ abstract class DomainObject { protected $id; public function __construct($id = null) { if (is_null($id)) { $id = -1; } else { $this->id = $id; } } public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } // 现在比较简单,之后还会扩展 // ...... }
Score类(对应Score表):
namespace demo\domain; use demo\domain\Student; /** * Score * 对应表score */ class Score extends DomainObject { private $score; private $courseName; // Student对象引用 private $student; public function __construct($id = null, $score = 0, $courseName = 'unknown') { parent::__construct($id); $this->score = $score; $this->courseName = $courseName; } public function getScore() { return $this->score; } public function setScore($score) { $this->score = $score; } public function setCourseName($courseName) { $this->courseName = $courseName; } public function getCourseName() { return $this->courseName; } public function getStudent() { return $this->student; } public function setStudent(Student $student) { $this->student = $student; } }
之前说到领域模型最复杂的是映射到数据库结构。我们可以使用数据映射器模式。
数据映射器是一个负责将数据库中的一行数据映射到一个对象的类。
有个概念叫“对象关系阻抗不匹配”,指的是对象和关系数据库性质上的差异,比如对象可以有复杂的继承层次,对象中还可以包含另一个对象(关系型数据库的表不行吧),而表可以通过外键表示与其他表之间的关联等。
来看看Mapper的类层次图吧:
Mapper抽象基类:
namespace demo\mapper; use demo\base\AppException; use \demo\base\ApplicationRegistry; /** * Mapper */ abstract class Mapper { // PDO protected static $PDO; // config protected static $dsn, $dbUserName, $dbPassword; // PDO选项 protected static $options = array( \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', \PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION, ); public function __construct() { if (!isset(self::$PDO)) { // ApplicationRegistry获取数据库连接信息 $appRegistry = ApplicationRegistry::getInstance(); self::$dsn = $appRegistry->getDsn(); self::$dbUserName = $appRegistry->getDbUserName(); self::$dbPassword = $appRegistry->getDbPassword(); if (!self::$dsn || !self::$dbUserName || !self::$dbPassword) { throw new AppException('Mapper init failed!'); } self::$PDO = new \PDO(self::$dsn, self::$dbUserName, self::$dbPassword, self::$options); } } /** * 查找指定ID * @param int $id */ public function findById($id) { $pStmt = $this->getSelectStmt(); $pStmt->execute(array($id)); $data = $pStmt->fetch(); $pStmt->closeCursor(); $obj = null; if (!is_array($data) || !isset($data['id'])) { return $obj; } $obj = $this->createObject($data); return $obj; } /** * 插入数据 * @param \demo\domain\DomainObject $obj */ public function insert(\demo\domain\DomainObject $obj) { return $this->doInsert($obj); } /** * 删除指定ID * @param int $id */ public function deleteById($id) { $pStmt = $this->getDeleteStmt(); $flag = $pStmt->execute(array($id)); return $flag; } /** * 生成一个$data中值属性的对象 * @param array $data */ public function createObject(array $data) { $obj = $this->doCreateObject($data); return $obj; } public abstract function update(\demo\domain\DomainObject $obj); protected abstract function doInsert(\demo\domain\DomainObject $obj); protected abstract function doCreateObject(array $data); protected abstract function getSelectStmt(); protected abstract function getDeleteStmt(); }
一个具体Mapper子类ScoreMapper:
namespace demo\mapper; use demo\base\AppException; use \demo\domain\DomainObject; use \demo\domain\Score; use \demo\mapper\StuStudentMapper; /** * ScoreMapper */ class ScoreMapper extends Mapper { private static $selectStmt; private static $insertStmt; private static $updateStmt; private static $deleteStmt; private static $init = false; public function __construct() { if (!self::$init) { parent::__construct(); $selectSql = 'select * from score where id = ?'; $insertSql = 'insert into score (sid, course_name, score) values (?, ?, ?)'; $updateSql = 'update score set sid = ?, course_name = ?, score = ? where id = ?'; $deleteSql = 'delete from score where id = ?'; // 预编译生成prepareStatement对象 self::$selectStmt = self::$PDO->prepare($selectSql); self::$insertStmt = self::$PDO->prepare($insertSql); self::$updateStmt = self::$PDO->prepare($updateSql); self::$deleteStmt = self::$PDO->prepare($deleteSql); self::$init = true; } } public function update(DomainObject $obj) { // 类型安全检查 // if (!($obj instanceof Score)) { // throw new AppException('Object is not instance of Student'); // } $data = array($obj->getStudent()->getId() , $obj->getCourseName(), $obj->getScore(), $obj->getId()); $flag = self::$updateStmt->execute($data); return $flag; } protected function doInsert(DomainObject $obj) { $data = array($obj->getStudent()->getId() , $obj->getCourseName(), $obj->getScore()); $flag = self::$insertStmt->execute($data); // 数据行返回设置对象 if ($flag) { $lastId = self::$PDO->lastInsertId(); $obj->setId($lastId); } return $flag; } protected function doCreateObject(array $data) { $obj = new Score($data['id']); $obj->setScore($data['score']); $obj->setCourseName($data['course_name']); // setStudent() $stuMapper = new StudentMapper(); $stuObj = $stuMapper->findById($data['sid']); $obj->setStudent($stuObj); return $obj; } protected function getSelectStmt() { return self::$selectStmt; } protected function getDeleteStmt() { return self::$deleteStmt; } }
使用的例子:
$score = new Score(0); $scoreMapper = new ScoreMapper(); $score->setCourseName('Math'); // 插入 $scoreMapper->insert($score); // 查找 $score = $scoreMapper->findById($score->getId()); var_dump($score); $score->setCourseName('English'); // 更新 $scoreMapper->update($score); // 删除 $scoreMapper->deleteById($score->getId());
数据映射器的好处是消除了领域层和数据库操作之间的耦合,Mapper可以应用各种对象关系映射。比如insert、update的传递的参数是DomainObject对象,保存到数据库的是数据行;findById把数据库的数据行转换成DomainObject对象。 而它的缺点是需要创建大量的具体的映射器类,不过大部分都是相似的代码,也可以通过反射机制来生成这些相似的代码。
findById是获取一条数据,而findAll是获取一个数据集,那么需要一个什么对象来保持和数据集的映射才比较好呢?接下来介绍Collection对象。