首 页   · 站长博客 · 用户注册 · 会员登陆  · 会员排行  ·最新主题  ·最近回复  精华区  版权声明  ·论坛管理
  当前登录身份:游客,请先登录。  笔名: 口令: 验证码:   
楼 主  index »  PHP相关资源下载区 » 《PHP设计模式介绍》第十六章 数据映射模式  


  作者:lxshark
  注册时间:2009-08-06
  主题/回复:49/50
  积分:639
  等级:★★★(六级)
  称号:声名鹊起

  lxshark@yeah.net..
  593993745
  hi.baidu.com/mythicsky

 

 发表:2009-08-08 20:58:52 阅读 1982 次 回复 1 次 得分0  |   字号 字色
《PHP设计模式介绍》第十六章 数据映射模式
前两章――动态数据模式与表数据网关模式各自展示对记录与每个表进行抽象的策略。这些模式都很有用,但每一个模式的执行都与底层的数据库结构结合过于紧密,因此基于以上模式的解决方案就存在一定的问题。比如,你的代码用字段名作为数组的关键字或是行数据对象的属性,则你的应用就受到数据库结构的约束,并且每当表结构发生哪怕是很小的变化,你都不得不在你的PHP程序中做大量的修改。

因为代码与数据库结构在开发过程经常变更,甚至在部署后也会发生。将代码与其数据库尽可能的分离,隔绝二者间的相互依赖性并减少因某一方的变化而产生的修改工作是非常有益的。

问题

你怎样才能将你的应用类与所属的数据库之间的结合度降至最低?例如,当你的数据表字段名发生变化时,你怎样将与此相关的修改工作降至最低?

解决方案

数据映射模式将对象的属性与存储它们的表字段间的结合密度降低。数据映射模式的本质就是一个类,它映射或是翻译类的属性或是方法到数据库的相应字段,反之亦然。数据映射的作用(工作)就在于能对双方所呈现出的信息的理解,并能对信息的存取进行控制,如根据存储在数据表中的信息重建新的域对象,或是用域对象的信息来更新或删除数据表中的相关数据。

对于面向对象代码与数据库表和字段间的映射关系的存储有多种实现方式。其中一种可能的方法就通过手工编码将这种映射关系存储在数据映射类中。另一种可选的方法是用PHP的数组并将其编码为类本身。这个类也能外源获取数据,如INI或是XML文件。

下图展示了一个数据映射类图,该类应用于解决存储URL书签(在前两章里已应用到)这类问题域。在图中,Bookmark对象是域对象,BookmarkMapper是数据映射模式的一个实现(执行)。Bookmark应该包含业务逻辑如校验URLs。BookmarkMapper则完全是一个在Bookmark的getter与setter方法与bookmark表字段结构间的交叉参照物。

这两个为关系很密切:BookmarkMapper充当了一个工厂,来实例化Bookmark,并且接受

Bookmark类的实例作为很多BookmarkMapper操作的参数。

 


样本代码

用UML示图作为路标,让我们来实现Bookmark与BookmarkMapper类。

首先,正如上面所提及,需要某种配置文件来处理表字段与对象方法间的映射。在本例中,我们用XML作为配置文件。

这个配置的主要目的是列示Bookmark表的字段,并指定哪个方法用于从Bookmark对象中存储与获取各自的信息。一个非常简单的XML结构就足够了,由一个<bookmark>根元素与一系列的<field>元素构成,如下所示


<field>
<name>url</name>
<accessor>getUrl</accessor>
<mutator>setUrl</mutator>
</field> 


<name>元素存储实际的物理字段名。<accessor>元素存储了获取属性数据方法的名称,它是可选项,因为一些字段如时间戳是不需要映射的。<mutaror>则存储了Bookmark类中完成填充对象值的方法名。另一些信息也能添加到这个映射表中,例如,你能声明每个字段的thetype 和size元素,这使得你能用这些信息动态的生成SQL来建立数据表。如果你的应用有一个用PHP写的安装包,则你会对此特别感兴趣,这样你就可以通过这个映射表来建立表结构。当设定基于以上信息的PHP对象属性时,你也能自动的设定其数据值。)

完整的XML文件如下:


<bookmark>
<field>
<name>id</name>
<accessor>getId</accessor>
<mutator>setId</mutator>
</field>
<field>
<name>url</name>
<accessor>getUrl</accessor>
<mutator>setUrl</mutator>
</field>
The Data Mapper Pattern 263
<field>
<name>name</name>
<accessor>getName</accessor>
<mutator>setName</mutator>
</field>
<field>
<name>description</name>
<accessor>getDesc</accessor>
<mutator>setDesc</mutator>
</field>
<field>
<name>tag</name>
<accessor>getGroup</accessor>
<mutator>setGroup</mutator>
</field>
<field>
<name>created</name>
<mutator>setCrtTime</mutator>
</field>
<field>
<name>updated</name>
<mutator>setModTime</mutator>
</field>
</bookmark>
 
可用PHP5中名为simpleXML的功能函数,来读取与解析这个文件。你所需要做的就是调用simplexml_load_file(‘bookmark.xml’),就生成了一个包含这个XML文件所有信息的SimpleXMLElement 对象集。这儿,结果看上去类似: 


object(SimpleXMLElement)#21 (1) {
[“field”]=>
array(7) {
[0]=>
object(SimpleXMLElement)#15 (3) {
[“name”]=>
string(2) “id”
[“accessor”]=>
string(5) “getId”
[“mutator”]=>
string(5) “setId”
}
[1]=>
object(SimpleXMLElement)#19 (3) {
[“name”]=>
string(3) “url”
[“accessor”]=>
string(6) “getUrl”
[“mutator”]=>
string(6) “setUrl”
}
//...<snip>...
[4]=>
object(SimpleXMLElement)#23 (3) {
[“name”]=>
string(3) “tag”
[“accessor”]=>
string(8) “getGroup”
[“mutator”]=>
string(8) “setGroup”
}
//...<snip>...
}
 
因为此XML文件映射域空间到数据库空间,所以BookmarkMapper类在构造时会读取这个XML配置文件。在正式研究BookmarkMapper前,让我们深入研究一下Bookmark类。假设Bookmark已在已有的工程中使用,则对其修改带来的影响应是越小越好。此外,Bookmark不应该仅仅为了适应BookmarkMapper而修改。实际上,数据映射模式就是旨在亲和,域对象本身应对Data Mapper的存在保持完全的透明。(这里是意译)。 

这又导致实现Data Mapper另一个重要的要求:因为Data Mappe对域对象透明,所有相关的对象都必须对所有相关的属性提供某种公共的通道,这样Data Mapper在建立时才能正确初始化域对象,并在保存域对象时可读取其属性值。

Bookmark的属性都是保护型的,但给每个属性提供了getter和setter方法,因此这正好能满足需求。

让我们从设置与获取Bookmark类的’url’属性的代码开始。


class Bookmark {
protected $url;
// ...
public function getUrl() {
return $this->url;
}
public function setUrl($url) {
$this->url = $url;
}




你能通过反射机制来避免的单调的编写无数简单的getter和setter方法。通过对对象自身的“窥探”,你能使对象测试某个特定的属性是否具有getters和setters以及是否这样命名的。

让我们来一些测试。


class BookmarkTestCase extends BaseTestCase {
//...
function testAccessorsAndMutators() {
$bookmark = new Bookmark(false);
$props = array(‘Url’, ‘Name’, ‘Desc’,
‘Group’, ‘CrtTime’, ‘ModTime’);
foreach($props as $prop) {
$getprop = “get$prop”;
$setprop = “set$prop”;
$this->assertNull($bookmark->$getprop());
$val1 = ‘some_val’;
$bookmark->$setprop($val1);
$this->assertEqual($val1,
$bookmark->$getprop());
$val2 = ‘other_val’;
$bookmark->$setprop($val2);
$this->assertNotEqual($val1,
$bookmark->$getprop());
$this->assertEqual($val2,
$bookmark->$getprop());
}
}



对每一个书签的属性,测试都通过mutaror方法设定了一个值,并与通过accessor方法返回的值进行校验。值再次被改变并校验。


这段代码代码基于常例而不是某种直接的映射。获取与变更方法的名称由get与set开头,并由属性名组成(小写)。例如,获取’url’方法的名称为getUrl(),修改url方法的名称则为setUrl()。

这里是实现动态获取与修改方法的一些代码示例。


class Bookmark {
protected $url;
protected $name;
protected $desc;
protected $group;
protected $crttime;
protected $modtime;
//...
public function __call($name, $args) {
if (preg_match(‘/^(get|set)(\w+)/’, strtolower($name), $match)
&& $attribute = $this->validateAttribute($match[2])) {
if (‘get’ == $match[1]) {
return $this->$attribute;
} else {
$this->$attribute = $args[0];
}
}
}
protected function validateAttribute($name) {
if (in_array(strtolower($name),
array_keys(get_class_vars(get_class($this))))) {
return strtolower($name);
}
}
}
 
这段代码基于PHP5的魔术方法__call(),当调用未定义(未在类中明确定义)的实例方法时__call()方法被调用。方法(无定义)的名称作为第一个参数传给__call(),而方法本身所需的参数作为__call方法的第二参数以数组的形式被传入。


要达到动态生成getter 和setter方法,则方法名被分析,看是否是以’get’或是’set’开头,并且是否以一个正确的对象属性名结束。如果是这样,属性值就能正确的修改与返回。这个动态方法就可以代替手工实现的geturl()与geturl()方法了,这样,这些手工代码就可以删除了。

这儿有一个副作用要注意,如果用本代码调用了别的方法,则不会有错误信息给出。为了防止这种情况,让我们对错误的调用抛出一个例外出理。


class Bookmark {
//...
public function __call($name, $args) {
if (preg_match(‘/^(get|set)(\w+)/’, strtolower($name), $match)
&& $attribute = $this->validateAttribute($match[2])) {
if (‘get’ == $match[1]) {
return $this->$attribute;
} else {
$this->$attribute = $args[0];
}
} else {
throw new Exception(
‘Call to undefined method Bookmark::’.$name.’()’);
}
}
}
 
你能通过以下代码测试上述例外代码



class BookmarkTestCase extends BaseTestCase {
//...
function testBadGetSetExceptions() {
$mapper = new BookmarkMapper($this->conn);
$this->addSeveralBookmarks($mapper);
$bookmark = $mapper->findById(1);
try {
$this->assertNull($bookmark->getFoo());
$this->fail(‘no exception thrown’);
}
catch (Exception $e) {
$this->assertWantedPattern(‘/undefined.*getfoo/i’,
$e->getMessage());
}
try {
$this->assertNull($bookmark->setFoo(‘bar’));
$this->fail(‘no exception thrown’);
}
catch (Exception $e) {
$this->assertWantedPattern(‘/undefined.*setfoo/i’,
$e->getMessage());
}
}
}
 



这儿还有另一个要注意的事项:一但当$id属性设置好后就不能变动它,让我们建立一个试验来说明上述事项。调用一次SetId()来设置ID值后,就可以用getid()反复获取其值,但随后再次调用setid()就应该无效。

class BookmarkTestCase extends BaseTestCase {
//...
function testUnsetIdIsNull() {
$bookmark = new Bookmark;
$this->assertNull($bookmark->getId());
}
function testIdOnlySetOnce() {
$bookmark = new Bookmark;
$id = 10; //just a random value we picked
$bookmark->setId($id);
$this->assertEqual($id, $bookmark->getId());
$another_id = 20; // another random value, != $id
//state the obvious
$this->assertNotEqual($id, $another_id);
$bookmark->setId($another_id);
// still the old id
$this->assertEqual($id, $bookmark->getId());
}


应记住一个非常重要的规则:在类中显示定义的方法将会重载由_call()执行的同名方法。你可以专门定义一个同名但行为功能不一样的方法,如setid()来重载任何借助__call()来执行的同名方法。 



class Bookmark {
protected $id;
//...
public function setId($id) {
if (!$this->id) {
$this->id = $id;
}
}
}
 

到目前为止, 我们只是有了基本数据对象,让我们增加一些业务逻辑到里面,毕竟,应用数据映射模式的原因之一就是在于将业务逻辑与数据存取分离。为与设计原则保持一致(告知,而不是提问),增加一个fetch()方法来获得实际的(html)书签内容的页面。


以下代码测试了这种能力


class BookmarkTestCase extends BaseTestCase {
//...
function testFetch() {
$bookmark = new Bookmark;
$bookmark->setUrl(‘http://www.google.com/’);
$page = $bookmark->fetch();
$this->assertWantedPattern(
‘~<input[^>]*name=q[^>]*>~im’, $page);
}
}
class Bookmark {
//...
public function fetch() {
return file_get_contents($this->url);
}
}
 
现在,完整的类如下所示:



class Bookmark {
protected $id;
protected $url;
protected $name;
protected $desc;
protected $group;
protected $crttime;
protected $modtime;
public function setId($id) {
if (!$this->id) {
$this->id = $id;
}
}
public function __call($name, $args) {
if (preg_match(‘/^(get|set)(\w+)/’, strtolower($name), $match)
&& $attribute = $this->validateAttribute($match[2])) {
if (‘get’ == $match[1]) {
return $this->$attribute;
} else {
$this->$attribute = $args[0];
}
} else {
throw new Exception(
‘Call to undefined method Bookmark::’.$name.’()’);
}
}
protected function validateAttribute($name) {
if (in_array(strtolower($name),
array_keys(get_class_vars(get_class($this))))) {
return strtolower($name);
}
}
public function fetch() {
return file_get_contents($this->url);
}
}
 
拥有了Bookmark类后,我们来看下BookmarkMapper 类。BookmarkMapper 类核心的工作是从数据库取出数据并创建Bookmark类。


首先,我们为了实现BookmarkMapper,需要增加新的数据库记录。

在数据映射模式里面,域对象是对数据映射是透明的,但是它包含了所有的商业逻辑和创建对象潜在的规则。其中一个创建数据记录规则就是创建一个新的Bookmark对象实例,设置属性,还有让BookmarkMapper来保存新创建的对象实例。好,现在我们来看下如何实现这个接口。

BookmarkMapper 必须能够与数据库进行交互。就像在前面两个章节中所说的一样,我们使用

ADOdb 来访问数据库。此外,在构造BookmarkMapper的时候,把ADOdb的连接传递过去。


//代码
class BookmarkMapper {
protected $conn;
public function __construct($conn) {
$this->conn = $conn;
}




BookmarkMapper 还必须导入刚才提到的XML 文件。为了让XML更方便使用,我们把映射存储为一些类的名字=> simplexml 元素。我们把这个加在构造函数里面:


class BookmarkMapper {
protected $map = array();
protected $conn;
public function __construct($conn) {
$this->conn = $conn;
foreach(simplexml_load_file(‘bookmark.xml’) as $field) {
$this->map[(string)$field->name] = $field;
}
}
}
 
现在你可以创建一个测试来测试save() 方法。



class BookmarkMapperTestCase extends BaseTestCase {
function testSave() {
$bookmark = new Bookmark;
$bookmark->setUrl(‘http://phparch.com/’);
$bookmark->setName(‘php|architect’);
$bookmark->setDesc(‘php|arch magazine homepage’);
$bookmark->setGroup(‘php’);
$this->assertNull($bookmark->getId());
$mapper = new BookmarkMapper($this->conn);
$mapper->save($bookmark);
$this->assertEqual(1, $bookmark->getId());
// a row was added to the database table
$this->assertEqual(1, $this->conn->getOne(
‘select count(1) from bookmark’));
}
}
 



这里,测试代码创建了一个新的Bookmark 类的实例,并设置了该类的相对应的属性,然后让一个BookmarkMapper 实例来存储(save())这个Bookmark实例。另外,这个测试还测试存储对象、设置ID、往数据库插入行的有效性。
 
 
 1#楼  
 
  回复人:lxshark
  注册时间:2009-08-06
  主题/回复:49/50
  积分:639
  等级:★★★(六级)
  称号:声名鹊起

   
 1#楼 发表于2009-08-08 21:30:57  评分:× 

回复给楼主(lxshark)
希望能对你们更好的学习PHP有所帮助,也期望大家有时间多光顾我的
点此打开链接:http://hi.baidu.com/mythicsky
  页数1/1首页 « 1 » 末页
  发表回复:您还没有登陆,无法发表回复。请先[登陆]

一起PHP技术联盟 主办:一起PHP 联系方式:站长QQ:4304410 QQ群:8423742 20159565 站长博客 E-mail: nqp@nqp.me 执行时间:0.036sec
SimsmaBBS 2008 (v6.0) Developed by 17php.com,Copyright(C)2003-2010 All rights reserved. 副本授权:一起PHP官方专用版