PHP/OOP方法重写DRY方式


PHP/OOP method overriding the DRY way

我很好奇对于以下行为是否有更好的设计:

<?php
class Foo {
    public function foo() {
        // Foo-specific foo stuff.
    }
}
class Bar extends Foo {
    public function foo() {
        // Bar-specific foo stuff.
        parent::foo();
    }
}
class Baz extends Bar {
    public function foo() {
        // Baz-specific foo stuff.
        parent::foo();
    }
}
$boz = new Foo();
$boz->foo(); // should do the stuff in Foo::foo()
$biz = new Bar();
$biz->foo(); // should do the stuff in Bar::foo() and Foo::foo()
$buz = new Baz();
$buz->foo(); // should do the stuff in Baz::foo(), Bar::foo(), and Foo::foo()
// etc...

本质上,我有一个基类Foo,其方法Foo::foo()包含一些应该始终运行的通用代码。我也有从Foo继承的各种子类,每个子类都有自己的特定代码,也应该始终运行。

我在这里使用的设计使用DRY原则来确保Foo::foo()中的代码不会在Bar::foo()Baz::foo()中重复,并且Bar::foo()中的代码不会在Baz::foo()中重复,等等。

这个设计的问题(?)是,我依赖于子类总是显式地调用parent::foo()在任何情况下,和类扩展这些类做同样的,等等,无限。然而,没有办法(据我所知)实际执行这一点。

所以我的问题是——是否有更好的设计来实现相同的行为,或者在父/子类之间强制执行这种"契约"?

有些人要求用例。多年来,我在几个项目中遇到过这种范例,但由于保密协议等原因,无法给出一个真实世界的例子,所以这里有一个超级基本的例子,可能有助于更好地说明这个问题:

<?php
// Vehicle
class Vehicle {
    public function start() {
        // Vehicle engines are on when you start them.
        // Unless they belong to me, that is :-(
        $this->setEngineStatus(Vehicle::ENGINE_ON);
    }
}
// Vehicle > Automobile
class Automobile extends Vehicle {
    public function start() {
        // Automobile engines are on when you start them.
        parent::start();
        // Automobiles idle when you start them.
        $this->setEngineRpm(Automobile::RPM_IDLE);
    }
}
// Vehicle > Airplane
class Airplane extends Vehicle {
    public function start() {
        // Airplane engines are on when you start them.
        parent::start();
        // Airplanes also have radios that need to be turned on when started.
        $this->setRadioStatus(Airplane::RADIO_ON);
    }
}
// Vehicle > Automobile > Car
class Car extends Automobile {
    public function start() {
        // Cars engines are on and idle when you start them.
        parent::start();
        // Cars also have dashboard lights that turn on when started.
        $this->setDashLightsStatus(Car::DASH_LIGHTS_ON);
    }
}
// Vehicle > Airplane > Jet
class Jet extends Airplane {
    public function start() {
        // Jet engines and radios are on when you start them.
        parent::start();
        // Jets also arm their weapons when started.
        $this->setWeaponsHot(true);
    }
}
// Vehicle > Automobile > BobsSuperAwesomeCustomTruck
class BobsSuperAwesomeCustomTruck extends Automobile {
    public function start() {
        // Uh-oh... Bob didn't call parent::start() in his class, so his trucks
        // don't work, with no errors or exceptions to help him figure out why.
        // Bob's trucks also need to reset their pinball machine highscores when started.
        $this->resetPinballScores();
    }
}

我不认为这更好,但这是一种可能的方法。

class abstract Foo {
    public function foo() {
        // Foo-specific foo stuff.
        $this->_foo();
    }
    // Might be abstract, might be an empty implementation
    protected abstract function _foo();
}
class Bar extends Foo {
    protected function _foo() {
        // Bar-specific foo stuff.
    }
}

就我个人而言,我更喜欢你的方式,因为我认为它更容易阅读。这也意味着子进程不必拥有自己的foo()实现。似乎更面向对象。但是,如果您需要每个子类都有自己添加的foo()实现,这可能会满足您的要求。

只要你在子类中重写你的方法,在我所知道的任何语言中都没有办法强制执行父类方法的行为。如果你只是为你的应用编写代码,你应该能够信任你自己的代码调用parent::foo()。但是,如果你正在编写一个库、框架或API,其他人将在此基础上进行构建,那么你的想法是有价值的。Ruby on Rails通过回调很好地利用了这种行为。

所以不要定义任何 foo方法。相反,使用__call和闭包数组作为回调。我的PHP真的很生疏,所以我忘记了一些细节。
class Foo {
  // I forget how to make a class variable in PHP, but this should be one.
  // You could define as many callback chains as you like.
  $callbacks = array('foo_callback_chain' => []);
  // This should be a class function. Again, forget how.
  function add_callback($name, $callback) {
    $callbacks[$name.'_callback_chain'][] = $callback;
  }
  // Add your first callback
  add_callback('foo', function() {
    // do foo stuff
  })
  def method__call($method, $args) {
    // Actually, you might want to call them in reverse order, as that would be more similar
    foreach ( $callbacks[$method_name.'_callback_chain'] as $method ) {
      $method();
    }
  }
}

然后在你的子类中,只需添加更多的回调"add_callback"。这并不适用于所有情况,但在某些情况下效果很好。(更多关于闭包的信息,请访问http://php.net/manual/en/functions.anonymous.php.)

我找到了一种更好的通用方法来避免闭包和其他丑陋的技巧。

class A {
    /**************************************************************/
    // Chain caller helpers, defined in base class only 
    // (single point of maintenance)
    protected $_chain_params; 
    final public function chain_call($method_name, $params){
        $class = get_class($this);  // get last child classname
        $chain = array($class);
        while ($class !== 'A'){    // get all parents classname
            $class = get_parent_class($class);
            $chain[] = $class;
        }
            // Call reversed chain
        $this->_chain_params = $params;
        for ($k = count($chain) - 1; $k >= 0; $k--){
            $class = $chain[$k];
            $refl = new 'ReflectionMethod($class, $method_name);
            if ($refl->class === $class)
                $ret = call_user_func_array(array($this, 
                                                  $class.'::'.$method_name), 
                                                  $this->_chain_params);
        }
        return $ret;
    }
    final protected function chain_modify_params($params){
        $this->_chain_params = $params;
    }
    /*************************************************************/
    // Methods overrided by child classes:
    public function foo($a, $b){
        echo "A foo fired with params a=$a b=$b <br>";
    }
    protected function bar($a, &$b){
        echo "A bar fired with params a=$a b=$b <br>";
        return 1000;
    }
}
 // Child classes extending base class. NOTE: no need to smell the code!
class B extends A {
    public function foo($a, $b){
        echo "B foo fired with params a=$a b=$b <br>";
    }
    protected function bar($a, &$b){
        echo "B bar fired with params a=$a b=$b <br>";
        return 2000;
    }
}
class C extends B {
    public function foo($a, $b){
        echo "C foo fired with params a=$a b=$b <br>";
    }
    protected function bar($a, &$b){
        echo "C bar fired with params a=$a b=$b <br>";
        $a++;  // override param value
        $b++;  // override referenced param value
        echo " - C modify => a=$a b=$b <br>";
        // reflect changed parameters to the next child class in chain ;)
        $this->chain_modify_params(array($a, &$b));
        return 3000;
    }
}
class D extends C {
    public function foo($a, $b){
        echo "D foo fired with params a=$a b=$b <br>";
    }
    protected function bar($a, &$b){
        echo "D bar fired with params a=$a b=$b <br>";
        return 4000;
    }
}
$d = new D();
echo 'Call "foo" directly... <br>';
$d->foo(10, 20);
echo '<br> Call "foo" in chain mode... <br>';
$d->chain_call('foo', array(10, 20));
echo '<br> More complex example: call "bar" in chain mode,'.
     'passing $k by reference, '.
     'and getting last method result... <br><br>';
$k = 40;
$ret = $d->chain_call('bar', array(30, &$k));
echo "<br> D->bar() return: " . $ret;
echo "<br>k = $k";
结果:

Call "foo" directly... 
D foo fired with params a=10 b=20 
Call "foo" in chain mode... 
A foo fired with params a=10 b=20 
B foo fired with params a=10 b=20 
C foo fired with params a=10 b=20 
D foo fired with params a=10 b=20 
More complex example: call "bar" in chain mode, 
passing $k by reference, and getting last method result... 
A bar fired with params a=30 b=40 
B bar fired with params a=30 b=40 
C bar fired with params a=30 b=40 
 - C modify => a=31 b=41 
D bar fired with params a=31 b=41 
D->bar() return: 4000
k = 41

在PHP中没有办法强制执行;

但是,如果Foo:: Foo必须总是在任何子类::Foo之前执行,并且你不关心结果;也许这些方法的实际内容设计得很糟糕。

如果你总是必须初始化一些东西,也许你可以在构造函数中做,如果你记录每个调用,也许你需要一个装饰器。

这是另一个可能有效的选项:

class Foo {
  function doFoo() {
    // the code that 'must always run' goes here
    ...
    ...
    ...
    // and now we're calling the 'overridden' method.
    foo();
  }
  protected function foo() {
     // move along, nothing to see here
  }

}
class Bar extends Foo {
  protected function foo() {
     // Bar-specific foo stuff. 
  }
}
class Baz extends Foo {
  protected function foo() {
     // Baz-specific foo stuff. 
  }
}

这里的缺陷是没有"多重继承"或链接。

但是,是的,也许你真的需要某种发布-订阅模式…或者谁知道呢?

如果你在问如何实现设计问题的解决方案,你应该明确地问如何解决你的设计问题。