业务逻辑层使用的是领域模型,因为它能使用数据映射器中的大部分模式。
“万物皆对象”,领域模型就是对于项目中各种个体的抽象表达,就是一个类。它常常被描述为一组属性及附加的操作。它们是做某些相关事的某个东西。
领域模型的复杂性主要来自于尝试使模型纯粹(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对象。