我已经开发了一个使用ZendFramework 1.1的应用程序,这个应用程序在两年的大部分时间里已经开始了,并且因此我已经看到了一些从我学习或尝试新事物的重构的不同阶段。在目前的状态下,我觉得我的结构非常好因为我可以快速完成任务,但肯定可以在某些方面使用一些改进 - 我觉得有很多膨胀和尴尬的依赖。
在我的应用程序中放置一些示例代码时,请耐心等待。我将使用Order
对象的示例,该对象具有OrderItem
个实例,这些实例也必须保存。我将解释实例化和保存的所有必要部分。
就我的理解而言,我在这里所发生的事情更符合ActiveRecord设计模式而不是Domain Models,尽管我认为我有两种做法。 ..
class Order extends BaseObject {
/** @var OrderItem array of items on the order */
public $items = array();
public function __construct($data = array()){
// Define the attributes for this model
$schema = array(
"id" => "int", // primary key
"order_number" => "string", // user defined
"order_total" => "float", // computed
// etc...
);
// Get datamapper and validator classes
$mf = MapperFactory::getInstance();
$mapper = $mf->get("Order");
$validator = new Order_Validator();
$table = new Application_DbTable_Order();
// Construct parent
parent::__construct($schema, $mapper, $validator, $table);
// If data was provided then parse it
if(count($data)){
$this->parseData($data);
}
// return the instance
return $this;
}
// Runs before a new instance is saved, does some checks
public function addPrehook(){
$orderNumber = $this->getOrderNumber();
if($this->mapper->lookupByOrderNumber($orderNumber)){
// This order number already exists!
$this->addError("An order with the number $orderNumber already exists!");
return false;
}
// all good!
return true;
}
// Runs after the primary data is saved, saves any other associated objects e.g., items
public function addPosthook(){
// save order items
if($this->commitItems() === false){
return false;
}
// all good!
return true;
}
// saves items on the order
private function commitItems($editing = false){
if($editing === true){
// delete any items that have been removed from the order
$existingOrder = Order::getById($this->getId());
$this->deleteRemovedItems($existingOrder);
}
// Iterate over items
foreach($this->items as $idx => $orderItem){
// Ensure the item's order_id is set!
$orderItem->setOrderId($this->getId());
// save the order item
$saved = $orderItem->save();
if($saved === false){
// add errors from the order item to this instance
$this->addError($orderItem->getErrors());
// return false
return false;
}
// update the order item on this instance
$this->items[$idx] = $saved;
}
// done saving items!
return true;
}
/** @return Order|boolean The order matching provided ID or FALSE if not found */
public static function getById($id){
// Get the Order Datamapper
$mf = MapperFactory::getInstance();
$mapper = $mf->get("Order");
// Look for the primary key in the order table
if($mapper->lookup($id)){
return new self($mapper->fetchObjectData($id)->toArray());
}else{
// no order exists with this id
return false;
}
}
}
解析数据,保存以及几乎所有适用于所有模型的东西(一个更合适的术语可能是实体)都存在于BaseObject中,如下所示:
class BaseObject {
/** @var array Array of parsed data */
public $data;
public $schema; // valid properties names and types
public $mapper; // datamapper instance
public $validator; // validator instance
public $table; // table gateway instance
public function __construct($schema, $mapper, $validator, $table){
// raise an error if any of the properties of this method are missing
$this->schema = $schema;
$this->mapper = $mapper;
$this->validator = $validator;
$this->table = $table;
}
// parses and validates $data to the instance
public function parseData($data){
foreach($data as $key => $value){
// If this property isn't in schema then skip it
if(!array_key_exists($key, $this->schema)){
continue;
}
// Get the data type of this
switch($this->schema[$key]){
case "int": $setValue = (int)$value; break;
case "string": $setValue = (string)$value; break;
// etc...
default: throw new InvalidException("Invalid data type provided ...");
}
// Does our validator have a handler for this property?
if($this->validator->hasProperty($key) && !$this->validator->isValid($key, $setValue)){
$this->addError($this->validator->getErrors());
return false;
}
// Finally, set property on model
$this->data[$key] = $setValue;
}
}
/**
* Save the instance - Inserts or Updates based on presence of ID
* @return BaseObject|boolean The saved object or FALSE if save fails
*/
public function save(){
// Are we editing an existing instance, or adding a new one?
$action = ($this->getId()) ? "edit" : "add";
$prehook = $action . "Prehook";
$posthook = $action . "Posthook";
// Execute prehook if its there
if(is_callable(array($this, $prehook), true) && $this->$prehook() === FALSE){
// some failure occured and errors are already on the object
return false;
}
// do the actual save
try{
// mapper returns a saved instance with ID if creating
$saved = $this->mapper->save($this);
}catch(Exception $e){
// error occured saving
$this->addError($e->getMessage());
return false;
}
// run the posthook if necessary
if(is_callable(array($this, $posthook), true) && $this->$posthook() === FALSE){
// some failure occured and errors are already on the object
return false;
}
// Save complete!
return $saved;
}
}
基础DataMapper
类包含save
,insert
和update
的非常简单的实现,因为{{从不重载每个对象定义1}}。我觉得这有点不稳定,但我觉得它有效吗? $schema
的子类基本上只提供特定于域的查找程序功能,例如BaseMapper
或lookupOrderByNumber
以及其他类似的内容。
findUsersWithLastName
我觉得我拥有的并不一定是可怕的,但我也觉得这里有一些不太好的设计。我的担忧主要是:
模型有一个非常严格的结构,它与数据库表模式紧密耦合,使得从模型或数据库表中添加/删除属性是一个完全痛苦的屁股!我觉得将所有保存到数据库中的对象保存到构造函数中的class BaseMapper {
public function save(BaseObject $obj){
if($obj->getId()){
return $this->update($obj);
}else{
return $this->insert($obj);
}
}
private function insert(BaseObject $obj){
// Get the table where the object should be saved
$table = $obj->getTable();
// Get data to save
$saveData = $obj->getData();
// Do the insert
$table->insert($saveData);
// Set the object's ID
$obj->setId($table->getAdapter()->getLastInsertId());
// Return the object
return $obj;
}
}
和$table
是一个坏主意...... 如何避免这种情况?我该怎么做才能避免定义$mapper
?
验证似乎有点古怪,因为它与模型上的属性名称紧密相关,这些属性名称也对应于数据库中的列名。这使得任何数据库或模型更改变得更加复杂! 是否有更合适的验证位置?
DataMappers 除了提供一些复杂的查找程序功能外,并没有做太多的事情。保存复杂对象完全由对象类本身处理(例如,在我的示例中为$schema
类。除了“复杂对象”之外,还有适合这种类型对象的术语吗?我说我的{{1对象是“复杂的”,因为它必须保存Order
个对象。 DataMapper应该处理Order
类中当前存在的保存逻辑吗?
非常感谢您的时间和投入!
答案 0 :(得分:1)
尽可能多地分离对象之间的关注点是一种很好的做法。有一个负责输入验证,其他负责执行业务逻辑,数据库操作等。为了保持2个对象松散耦合,他们不应该只知道彼此的实现。这是通过接口定义的。
我建议您阅读这篇文章http://www.javaworld.com/article/2072302/core-java/more-on-getters-and-setters.html和其他人。他有一本书值得一读http://www.amazon.com/Holub-Patterns-Learning-Looking-Professionals/dp/159059388X。
如果可能的订单和商品,我会分开,我不太了解您的应用程序,但如果您只需要显示包含订单号的20个订单的列表,那么这些数据库调用和处理订单项目将是浪费如果不分开。这当然不是唯一的方法。
首先,您需要知道订单属性是什么,并封装了将这些属性提供给订单的方法,并且还有一个订单将该数据公开给其他对象。
interface OrderImporter {
public function getId();
public function getOrderNumber();
public function getTotal();
}
interface OrderExporter {
public function setData($id, $number, $total);
}
为了将业务逻辑与数据库分开,我们需要封装该行为,如此
interface Mapper {
public function insert();
public function update();
public function delete();
}
另外,我会定义一个特定的映射器,其职责是处理有关订单的DB操作。
interface OrderMapper extends Mapper {
/**
* Returns an object that captures data from an order
* @return OrderExporter
*/
public function getExporter();
/**
* @param string $id
* @return OrderImporter
*/
public function findById($id);
}
最后,订单需要能够通过一些消息与所有这些对象进行通信。
interface Order {
public function __construct(OrderImporter $importer);
public function export(OrderExporter $exporter);
public function save(OrderMapper $orderRow);
}
到目前为止,我们有一种向订单提供数据的方法,一种从订单中提取数据的方法以及与数据库交互的方式。
下面我提供了一个非常简单的示例实现,远非完美。
class OrderController extends Zend_Controller_Action {
public function addAction() {
$requestData = $this->getRequest()->getParams();
$orderForm = new OrderForm();
if ($orderForm->isValid($requestData)) {
$orderForm->populate($requestData);
$order = new ConcreteOrder($orderForm);
$mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
$order->save($mapper);
}
}
public function readAction() {
//if we need to read an order by id
$mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
$order = new ConcreteOrder($mapper->findById($this->getRequest()->getParam('orderId')));
}
}
/**
* Order form can be used to perform validation and as a data provider
*/
class OrderForm extends Zend_Form implements OrderImporter {
public function init() {
//TODO setup order input validators
}
public function getId() {
return $this->getElement('orderID')->getValue();
}
public function getOrderNumber() {
return $this->getElement('orderNo')->getValue();
}
public function getTotal() {
return $this->getElement('orderTotal')->getValue();
}
}
/**
* This mapper also serves as an importer and an exporter
* but clients don't know that :)
*/
class ZendOrderMapper implements OrderMapper, OrderImporter, OrderExporter {
/**
* @var Zend_Db_Table_Abstract
*/
private $table;
private $data;
public function __construct(Zend_Db_Table_Abstract $table) {
$this->table = $table;
}
public function setData($id, $number, $total) {
$this->data['idColumn'] = $id;
$this->data['numberColumn'] = $number;
$this->data['total'] = $total;
}
public function delete() {
return $this->table->delete(array('id' => $this->data['id']));
}
public function insert() {
return $this->table->insert($this->data);
}
public function update() {
return $this->table->update($this->data, array('id' => $this->data['id']));
}
public function findById($id) {
$this->data = $this->table->fetchRow(array('id' => $id));
return $this;
}
public function getId() {
return $this->data['idColumn'];
}
public function getOrderNumber() {
return $this->data['numberColumn'];
}
public function getTotal() {
return $this->data['total'];
}
public function getExporter() {
return $this;
}
}
class ConcreteOrder implements Order {
private $id;
private $number;
private $total;
public function __construct(OrderImporter $importer) {
//initialize this object
$this->id = $importer->getId();
$this->number = $importer->getOrderNumber();
$this->total = $importer->getTotal();
}
public function export(\OrderExporter $exporter) {
$exporter->setData($this->id, $this->number, $this->total);
}
public function save(\OrderMapper $mapper) {
$this->export($mapper->getExporter());
if ($this->id === null) {
$this->id = $mapper->insert();
} else {
$mapper->update();
}
}
}