Как применять принцип открытого-закрытого при создании объектов - PullRequest
4 голосов
/ 09 июня 2011

Я занят анализом XML-документов (google docs api) и помещаю отдельные документы в объекты.

Существуют различные типы документов (документы, таблицы, презентации). Большая часть информации об этих документах одинакова, но некоторые отличаются.

Идея состояла в том, чтобы создать базовый класс документа, который будет содержать всю совместно используемую информацию, при этом используя подклассы для каждого конкретного типа документа.

Проблема заключается в создании правильных классов для разных типов. Есть два способа различить тип документа. У каждой записи есть элемент категории, где я могу найти тип. Другой метод, который будет использоваться, это resourceId в форме type:id.

Наиболее наивным вариантом будет создание оператора if (или оператора switch), проверяющего тип записи, и создание для него соответствующего объекта. Но для этого потребуется отредактировать код, если будет добавлен новый тип.

Теперь я не совсем уверен, есть ли другой способ решить эту проблему, поэтому я спрашиваю об этом здесь. Я мог бы инкапсулировать создание правильного типа объекта в фабричном методе, поэтому количество необходимых изменений минимально.

Прямо сейчас у меня есть что-то вроде этого:

public static function factory(SimpleXMLElement $element)
{
    $element->registerXPathNamespace("d", "http://www.w3.org/2005/Atom");
    $category = $element->xpath("d:category[@scheme='http://schemas.google.com/g/2005#kind']");

    if($category[0]['label'] == "spreadsheet")
    {
        return new Model_Google_Spreadsheet($element);
    }
    else
    {
        return new Model_Google_Base($element);
    }
}

Итак, мой вопрос: есть ли другой метод, который я не вижу, чтобы справиться с этой ситуацией?

Edit: Добавлен пример кода

Ответы [ 3 ]

4 голосов
/ 09 июня 2011

Обновленный ответ с вашим примером кода

Вот ваш новый завод:

public static function factory(SimpleXMLElement $element)
{
    $element->registerXPathNamespace("d", "http://www.w3.org/2005/Atom");
    $category = $element->xpath("d:category[@scheme='http://schemas.google.com/g/2005#kind']");
    $className = 'Model_Google_ '.$category[0]['label'];
    if (class_exists($className)){
       return new $className($element);
    } else {
        throw new Exception('Cannot handle '.$category[0]['label']);
    }
}

Я не уверен, что понял вашу точку зрения ... Чтобы перефразировать вопрос, я понял, "как я могу создать правильный объект, не кодируя выделение в моем коде клиента"

с автозагрузкой

Итак, начнем с базового клиентского кода

class BaseFactory
{
    public function createForType($pInformations)
    {
       switch ($pInformations['TypeOrWhatsoEver']) {
           case 'Type1': return $this->_createType1($pInformations);
           case 'Type2': return $this->_createType2($pInformations);
           default : throw new Exception('Cannot handle this !');
       }
    }
}

Теперь давайте посмотрим, сможем ли мы изменить это, чтобы избежать установки if / switch (не всегда необходимо, но может быть)

Мы собираемся использовать возможности автозагрузки PHP.

Во-первых, рассмотрим автозагрузку, вот наш новый Фабрика

class BaseFactory
{
    public function createForType($pInformations)
    {
       $handlerClassName = 'GoogleDocHandler'.$pInformations['TypeOrWhatsoEver'];
       if (class_exists($handlerClassName)){
           //class_exists will trigger the _autoload
           $handler = new $handlerClassName();
           if ($handler instanceof InterfaceForHandlers){
               $handler->configure($pInformations);
               return $handler;
           } else {
               throw new Exception('Handlers should implements InterfaceForHandlers');
           }
       }  else {
           throw new Exception('No Handlers for '.$pInformations['TypeOrWhatsoEver']);
       }
   }
}

Теперь мы должны добавить возможность автозагрузки

class BaseFactory
{
    public static function autoload($className)
    {
        $path = self::BASEPATH.
                $className.'.php'; 

        if (file_exists($path){
            include($path); 
        }
    }
}

А вам просто нужно зарегистрировать свой автозагрузчик как

spl_autoload_register(array('BaseFactory', 'autoload'));

Теперь каждый раз, когда вам нужно будет написать новый обработчик для типов, он будет автоматически добавлен.

С цепочкой ответственности

Возможно, вы не захотите писать что-то более «динамичное» на своей фабрике с подклассом, который обрабатывает более одного типа.

например

class BaseClass
{
    public function handles($type);
}
class TypeAClass extends BaseClass
{
    public function handles($type){
        return $type === 'Type1';
    }
}
//....

В коде BaseFactory вы можете загрузить все свои обработчики и сделать что-то вроде

class BaseFactory
{ 
    public function create($pInformations)
    {
        $directories = new \RegexIterator(
            new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator(self::BasePath)
            ), '/^.*\.php$/i'
        );

        foreach ($directories as $file){
            require_once($fileName->getPathName());
            $handler = $this->_createHandler($file);//gets the classname and create it
            if ($handler->handles($pInformations['type'])){
                return $handler;
            }
        }
        throw new Exception('No Handlers for '.$pInformations['TypeOrWhatsoEver']);
    }
}
1 голос
/ 09 июня 2011

Я согласен с Oktopus, обычно есть два метода без жесткого кодирования;первый - найти имя класса путем динамического добавления строк и убедиться, что ваши классы правильно названы; второй способ - загрузить все классы-обработчики и использовать тот, который говорит, что он может обрабатывать этот тип.Я бы пошел на первое.С вашим примером кода это будет выглядеть примерно так:

<?php
public static function factory(SimpleXMLElement $element)
{
    $element->registerXPathNamespace("d", "http://www.w3.org/2005/Atom");
    $category = $element->xpath("d:category[@scheme='http://schemas.google.com/g/2005#kind']");

    $classname = sprintf( 'Model_Google_%s', ucfirst( strtolower( (string) $category[0]['label'] ) ) );

    if( class_exists( $classname, true /* true means, do autoloading here */ ) ) {
        return new $classname( $element );
    }
    return new Model_Google_Base($element);
}

На цепочке ответственности: хотя это прекрасный пример того, как его написать, я считаю, что цепочка ответственности означает, что все возможные обработчики должны быть загружены и спросить, могут ли они обрабатывать этот тип.Получившийся код, на мой взгляд, чище и понятнее, но при этом наблюдается как снижение производительности, так и дополнительная сложность.Вот почему я бы выбрал динамическое разрешение имени класса.

0 голосов
/ 09 июня 2011

Может быть, вы могли бы применить xsl-transformation к вашему входному файлу (в заводском методе).Результатом преобразования должен быть однородный xml-файл, содержащий информацию о том, какой класс использовать?избегайте переписывания кода.

...