4

I'm busy parsing xml documents (google docs api) and putting individual documents into objects.

There are different types of documents (documents, spreadsheets, presentations). Most information about these documents is the same, but some is different.

The idea was to create a base document class which holds all the shared information, while using subclasses for each specific document type.

The problem is creating the right classes for the different types. There are two ways to differentiate the type of the document. Each entry has a category element where I can find the type. Another method that will be used is by the resourceId, in the form of type:id.

The most naive option will be to create an if-statement (or switch-statement) checking the type of the entry, and create the corresponding object for it. But that would require to edit the code if a new type would be added.

Now i'm not really sure if there is another way to solve this, so that's the reason I'm asking it here. I could encapsulate the creation of the right type of object in a factory method, so the amount of change needed is minimal.

Right now, I have something like this:

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

So my question is, is there another method I'm not seeing to handle this situation?

Edit: Added example code

Ikke
  • 99,403
  • 23
  • 97
  • 120

3 Answers3

4

Updated answer with your code example

Here is your new factory :

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

I'm not sure that I got exactly your point... To rephrase the question, I understood "how can I create the right object without hardcoding the selection in my client code"

With autoload

So let's start with the base client code

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

Now, let's see if we can change this to avoid the if / switch statments (not always necessary, but can be)

We're here gonna use PHP Autoload capabilities.

First, consider the autoload is in place, here is our new Factory

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

Now we have to add the autoload capability

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

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

And you just have to register your autoloader like

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

Now, everytime you'll have to write a new handler for Types, it will be automatically added.

With chain of responsability

You may wan't to write something more "dynamic" in your factory, with a subclass that handles more than one Type.

eg

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

In the BaseFactory code, you could load all your handlers and do something like

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']);
    }
}
Gérald Croës
  • 3,799
  • 2
  • 18
  • 20
  • so please can you detail more ? give us examples and tell us what you exactly want to avoid ? (I added a Chain of responsability example in my answer.... maybe it's what you're looking for ?) – Gérald Croës Jun 09 '11 at 09:17
  • I have added one example. I'm going to look into the chain of responsibility. – Ikke Jun 09 '11 at 09:24
  • My first example with the "autoloaded" handlers is ok based on your example. just replace your factory method with $classname = 'Model_Google_'.$category[0]['label']; if (class_exists($classname)) return new $classname($element); – Gérald Croës Jun 09 '11 at 09:32
  • Updated my answer with a new factory – Gérald Croës Jun 09 '11 at 09:54
  • I'm accepting your answer. I'm not sure if I'm going to use the string based class lookup, or a predefined list, but it at least gave me an idea. – Ikke Jun 09 '11 at 10:52
1

I agree with Oktopus, there are generally two methods without hardcoding; the first would be to find the classname by dynamically appending strings, and make sure your classes are properly named, the second way would be to load all handler classes, and use the one that says it can handle that type. I'd go for the first. With your example code, this would look something like the following:

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

On the chain of responsibility: although that is a beautiful example of how to write it, I find that a chain of responsibility means that all possible handlers have to be loaded and asked if they can handle that type. The resulting code is cleaner and more explicit from my point of view, but there is both a performance hit and added complexity. That's why I'd go for dynamic classname resolution.

Berry Langerak
  • 18,561
  • 4
  • 45
  • 58
  • Did not see your answer before updating mine. For the chain of responsability, I agree with you, it is really not necessary on this example, I'd also go on dynamic classname resolution. – Gérald Croës Jun 09 '11 at 09:42
0

Maybe you could apply a xsl-tranformation on your input file (in the factory method). The Result of the transformation should then be a uniform xml file giving input about what class to use?

I know this not really better than a lot of if's but this way, you'll at least avoid rewriting your code.

Yoshi
  • 54,081
  • 14
  • 89
  • 103
  • I don't see how that would be of any help. I can easily see of what type an entry is. I would still need a bunch of if-statements to check what object would be created. – Ikke Jun 09 '11 at 08:59
  • @Ikke Thats not what I meant. The idea was to transform the input file (using xsl) to something like a config-xml file. This xml should then allways have the same structure giving info about what class (and maybe parameters) to use. The advantage would be, that you don't have to change the factory class if a new document becomes available. Instead you'll only have to handle this new document in the xsl-tranformation file. – Yoshi Jun 09 '11 at 09:15