创建对象时如何应用开闭原则

Posted

技术标签:

【中文标题】创建对象时如何应用开闭原则【英文标题】:How to apply open-closed principle when creating objects 【发布时间】:2011-06-09 08:40:43 【问题描述】:

我正忙于解析 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);
    

所以我的问题是,有没有其他我看不到的方法来处理这种情况?

编辑: 添加示例代码

【问题讨论】:

【参考方案1】:

用您的代码示例更新答案

这是你的新工厂:

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

【讨论】:

请问您能详细说明一下吗?给我们举个例子,告诉我们你到底想避免什么? (我在答案中添加了一个责任链示例......也许这就是你要找的东西?) 我添加了一个例子。我要调查责任链。 根据您的示例,我使用“自动加载”处理程序的第一个示例是可以的。只需将您的工厂方法替换为 $classname = 'Model_Google_'.$category[0]['label']; if (class_exists($classname)) return new $classname($element); 用新工厂更新了我的答案 我接受你的回答。我不确定我是要使用基于字符串的类查找还是预定义的列表,但它至少给了我一个想法。【参考方案2】:

我同意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);

关于责任链:虽然这是一个如何编写的漂亮示例,但我发现责任链意味着必须加载所有可能的处理程序并询问它们是否可以处理那种。从我的角度来看,生成的代码更清晰、更明确,但同时会降低性能并增加复杂性。这就是我选择动态类名解析的原因。

【讨论】:

在更新我的之前没有看到您的答案。对于责任链,我同意你的看法,在这个例子中真的没有必要,我也会继续动态类名解析。【参考方案3】:

也许您可以对输入文件应用 xsl 转换(在工厂方法中)。 转换的结果应该是一个统一的 xml 文件,提供关于使用什么类的输入?

我知道这并不比很多 if 更好,但这样,您至少可以避免重写代码。

【讨论】:

我看不出这有什么帮助。我可以很容易地看到一个条目是什么类型。我仍然需要一堆 if 语句来检查将创建什么对象。 @Ikke 这不是我的意思。这个想法是将输入文件(使用 xsl)转换为类似于 config-xml 文件的东西。然后,此 xml 应始终具有相同的结构,提供有关要使用的类(可能还有参数)的信息。这样做的好处是,如果有新文档可用,您不必更改工厂类。相反,您只需在 xsl 转换文件中处理这个新文档。

以上是关于创建对象时如何应用开闭原则的主要内容,如果未能解决你的问题,请参考以下文章

如何应用 SOLID 原则在 React 中整理代码之开闭原则

前端用到的设计模式之开闭原则. 里氏代换原则

面向对象设计原则二:开闭原则(OCP)

开闭原则——面向对象程序设计原则

面向对象原则之一 开放封闭原则(开闭原则)

开闭原则