根据上下文调整对对象属性的写访问权限

时间:2015-04-21 11:51:21

标签: php oop design-patterns

class SomeObject {

    protected $foo,
              $bar;

    protected $context;

    public function __construct($context) {
        $this->context = $context;
    }

    public function setFoo($val) {
        if ($this->context == 'public') {
            throw new \Exception('It is impossible to modify foo property in public context!');
        }
        $this->foo = $val;
    }

    public function setBar($val) {
        if ($this->context == 'api') {
            throw new \Exception('It is impossible to modify bar property in API context!');
        }
        $this->bar = $val;
    }

}

从这段“代码”中可以看出 - 对象根据上下文值限制setter。这段代码真的很难维护。我们如何重写它以使其美观且易于维护?

我的想法是:

  • 使$context个实现接口的对象 isAllowed($object, $propertyName)
  • 在使$context成为对象后,我们必须考虑如何在$context对象中存储“限制”,同时考虑到有许多与SomeObject类似的不同对象。 / LI>
  • 在每个人都应该检查$this->context->isAllowed($this, 'foo') - 它看起来不太好。那么,我们可能想在SomeObject上添加一些“代理”吗?
  • $context传递给构造函数对我来说似乎也很难看。

您对此有何看法?

4 个答案:

答案 0 :(得分:1)

只有两个一般性意见:

  1. 可能想要将你的类分成两部分:一个不可变的基类和一个可变的扩展:

    class Foo {
        protected $bar, $baz;
    }
    
    class MutableFoo extends Foo {
    
        public function setBar($bar) {
            $this->bar = $bar;
        }
    
        ..
    
    }
    

    当在对象实例化时定义上下文并且不会发生变化时,这很容易解决问题。您可以简单地实例化该类的可变或不可变版本,而不是使用确定可变性的不同上下文进行实例化。

  2. 如果您仍需要更多的运行时检查,可能只是简单地使用断言是简化代码的最佳方法:

    public function setBar($bar) {
        $this->assertCanSet('bar');
        $this->bar = $bar;
    }
    
    protected function assertCanSet($property) {
        if (!/* can set $property */) {
            throw new Exception("Cannot set property $property");
        }
    }
    

答案 1 :(得分:0)

也许在构造上,填写一系列受限制的方法。 所以,例如:

class SomeObject {

    protected $foo,
              $bar;

    protected $context;

    protected $restrictedMethods;

    public function __construct($context) {
        $this->context = $context;
        if($this->context == 'public') {
            $this->restrictedMethods['setFoo'] = true;
        } else if ($this->context == 'api') {
            $this->restrictedMethods['setBar'] = true;
        }
    }

    public function setFoo($val) {
        if ($this->isRestricted('setFoo')) {
            throw new \Exception('It is impossible to modify foo property in '.$this->context.' context!');
        }
        $this->foo = $val;
    }

    public function setBar($val) {
        if ($this->isRestricted('setFoo')) {
            throw new \Exception('It is impossible to modify bar property in '.$this->context.' context!');
        }
        $this->bar = $val;
    }

    protected function isRestricted($methodName) {
        return array_key_exists($methodName, $this->restrictedMethods);
    }
}

答案 2 :(得分:0)

如果您正在尝试编写好的OOP,那么SOLID原则中的“Interface Segregation”可能对您有用。

interface IBase
{
    public function doMethod1();

    public function doMethod2();

    public function doMethod3();
}

interface IFoo extends IBase
{
    public function setFoo($val);
}

interface IBar extends IBase
{
    public function setBar($val);
}

function doWork(IBase $obj, $val)
{
    $obj->doMethod1();
    $obj->doMethod2();
    $obj->doMethod3();

    if ($obj instanceof IFoo) {
        $obj->setFoo($val);
    }

    if ($obj instanceof IBar) {
        $obj->setBar($val);
    }
}

我怀疑这个例子正是你所需要的,但我会用它来解释基本的想法。

一个班级应该只有“单一责任”。但是,责任所包含的内容可能会有所不同,因此通常最好尽可能将课程的功能限制在一个关注区域。

如果你想遵循“Liskov替换”,那么仅仅因为“上下文”无关紧要而在你的函数中抛出这样的异常就违反了这个原则。

输入“界面隔离”:

通过实现接口,您(在某种程度上)向调用者保证已实现的方法,这些方法将起作用。通过排除它们,您告诉调用者这些方法不存在。

在示例中,doWork函数需要IBase的实例,并安全地调用该接口的方法。之后,它会对对象进行内省,以确定是否有其他“适用”方法可用。

接口隔离背后的目标是限制类强制实现的不需要的功能的数量,因此对于您而言,如果上下文为public,则不需要setFoo方法。

答案 3 :(得分:0)

一个干净的解决方案是拥有一个ObjectFactory类,它基于$context参数创建不同的对象,并且具有两个独立的类(具有公共基类),允许写入适当的属性

请在下面找到您的架构的可能实现:

/**
  * Base class that allows subclasses to define which properties are
  * writable via setters. Subclasses must not add public setters, 
  * otherwise the mechanism within this class will not work; subclasses
  * can add protected setters, though
  */
class PropertyRestricter {

    // only properties listed here are writable
    // to be initialised by subclasses
    protected $writableProperties;

    public function __construct() {
        // default implementation allows no writable properties
        $this->writableProperties = array();
    }

    public function __call($method, $arguments) {
        $matches = false;
        // check if a setter was called, extract the property name
        // there needs to be at least one argument to the setter
        if(count($arguments) && preg_match('/^set([A-Z][a-zA-Z0-9_]+)$/',$matches)) {                
            $propName = $matches[1];
            $propName[0] = strtolower($propName[0]);

            // update the property with the given value
            // or throw an exception if the property is not writable
            if(is_array($this->writableProperties) && in_array($propName, $this->writableProperties)) {
                $this->{$propName} = $arguments[0];
            } else {
                throw new Exception(get_class() . ": $propName is not writable");
            }
        } else {
            // not a setter, nor a public method
            // maybe display a user error
        }
    }
}

/**
  * Common properties for the actual classes
  */
class BaseObject extends PropertyRestricter {
    protected $foo, $bar;
}

class PublicObject extends BaseObject {
    public function __construct() {
        parent::__construct();
        $this->writableProperties = array('foo');
    }
}

class APIObject extends BaseObject {
    public function __construct() {
        parent::__construct();
        $this->writableProperties = array('bar');
    }
}

class ObjectFactory {
    public function createObject($context) {
        switch($context) {
            case 'public': return new PublicObject();
            case 'api': return new APIObject();
            default: return null;
        }
    }
}

对象的根是PropertyRestricter类,它允许子类定义哪些属性是可写的。它使用魔术方法__call(),以便能够拦截setter调用并验证写入属性的尝试。但请注意,仅当子类不为其属性添加公共设置器时,此方法才有效。

下一级是BaseObject类,它只定义了两个属性,以减少代码冗余。

最后一个级别包含由ObjectFactory实例化的两个类:PublicObject,' APIObject . These classes simply initialise the writableProperties array, as the rest of the work is done by the PropertyRestricter`类。

这也是一个可扩展的解决方案,因为它允许根据需要添加任意数量的属性和子类,每个子类定义其属性编写规则。

__call()方法中的属性更新也可以自定义,我通过直接设置属性以最简单的方式实现。实际的setter可以在子类中使用,__call()可以更新以调用setter,并提到需要保护setter以使机制工作。