0

私は ZendFramework 1.1 を利用したアプリケーションを 2 年間にわたって開発してきましたが、新しいことを学んだり試したりするために、リファクタリングのいくつかの異なる段階を見てきました。現在の状態では、私の構造は、物事を迅速に完了できるという点でかなり優れていると感じていますが、肥大化してぎこちない依存関係がたくさんあると感じている特定の領域でいくつかの改善を使用できることは確かです.

私のアプリケーションからサンプルコードをいくつか書き出すので、ここで我慢してください。保存する必要があるインスタンスをOrder持つオブジェクトの例を使用します。OrderItemインスタンス化と保存に必要なすべての部分を説明します。

私の理解が及ぶ限り、ここで行っていることは、 Domain ModelsよりもActiveRecord設計パターンに沿っていますが、両方の慣行があると思います...

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$schemaBaseMapperlookupOrderByNumberfindUsersWithLastName

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コンストラクターで指定するのは悪い考えだと思います...どうすればこれを回避できますか? を定義しないようにするにはどうすればよい$schemaですか?

検証は、データベースの列名にも対応するモデルのプロパティ名に非常に密接に結び付けられているため、少し奇妙に思えます。これにより、データベースやモデルの変更がさらに複雑になります。検証のためのより適切な場所はありますか?

DataMappersは、いくつかの複雑な検索機能を提供する以外に、実際にはあまり機能しません。複雑なオブジェクトの保存は、オブジェクト クラス自体によって完全に処理されます (たとえば、Order私の例ではクラスです。「複雑なオブジェクト」以外に、このタイプのオブジェクトを表す適切な用語はありますかOrder?OrderItemまた、保存する必要があるオブジェクトDataMapper は、現在Orderクラスに存在する保存ロジックを処理する必要がありますか?

お時間とご意見をお寄せいただきありがとうございます。

4

1 に答える 1

1

It's a good practice to separate the concerns between objects as much as possible. Have one responsible for Input Validation, other to perform the business logic, DB operations, etc. In order to keep the 2 objects loosely coupled they should not know anything about each other’s implementation only what they can do. This is defined thru an interface.

I recommend reading this article http://www.javaworld.com/article/2072302/core-java/more-on-getters-and-setters.html and other ones from this guy. He's got a book as well worth reading http://www.amazon.com/Holub-Patterns-Learning-Looking-Professionals/dp/159059388X.

I would separate if possible order and items, I don’t know much about your app but if you need to show a list of 20 orders only with their order numbers then those DB calls and processing regarding order items would be a waste if not separated. This is of course not the only way.

So first you need to know what the order attributes are and encapsulate a way to feed those into an order and also have an order expose that data to other objects.

interface OrderImporter {

    public function getId();

    public function getOrderNumber();

    public function getTotal();
}

interface OrderExporter {

    public function setData($id, $number, $total);
}

In order to keep the business logic separate from the database we need to encapsulate that behavior as well like so

interface Mapper {

    public function insert();

    public function update();

    public function delete();
}

Also I would define a specific mapper whose duty is to handle DB operations regarding orders.

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);
}

Finally an order needs to be able to communicate with all those objects through some messages.

interface Order {

    public function __construct(OrderImporter $importer);

    public function export(OrderExporter $exporter);

    public function save(OrderMapper $orderRow);
}

So far we have a way to provide data to the Order, a way to extract data from the order and a way to interact with the db.

Below I've provided a pretty simple example implementation which is far from perfect.

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();
        }
    }

}
于 2014-06-19T05:38:29.760 に答える