【问题标题】:PHP/OOP method overriding the DRY way覆盖 DRY 方式的 PHP/OOP 方法
【发布时间】:2011-09-12 22:56:25
【问题描述】:

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

<?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() 中的代码在@987654330 中不重复@,等等。

这种设计的问题(?)是我依赖子类在每种情况下始终显式调用parent::foo(),以及扩展这些类以执行相同操作的类,等等无限。但是,没有办法(据我所知)实际执行此操作。

所以我的问题是 - 是否有更好的设计来完成相同的行为,或者以某种方式在父/子类之间强制执行这种“合同”?

更新

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

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

【问题讨论】:

  • 您可以将其设计为从基类抽象继承:php.net/manual/en/language.oop5.abstract.php
  • 我认为不仅在 php 中,而且在任何类似 C 的编程语言中都没有。实际上,我认为 OOP 概念中没有更好的方法。
  • @Lawrence,这并不强制在扩展类中使用 parent::foo()。
  • 这不是 PHP 特有的。它是 OOP 范式的一部分。就像在现实生活中一样,孩子继承父母的特质。父母不能对孩子、孙子、曾孙等强制特征。
  • 这是一个很好的比喻,赫伯特。

标签: php oop


【解决方案1】:

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

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() 实现。似乎更OOP。但是,如果您要求每个子类都有自己添加的 foo() 实现,这可能会为您解决问题。

【讨论】:

  • 感谢您的回复,这是我最初考虑的一种方法,但很快发现这只能在一个继承级别上正常工作(我需要它与任意数量的继承一起工作)级别 - 例如 Foo > Bar > Baz > Boz 而不仅仅是 Foo > Bar)。
  • 我已经有几年没有做任何 PHP 了,但是 parent::_foo() 不起作用?我不确定您为什么如此担心对 parent::_foo(); 的呼叫丢失。这就是我们进行单元测试的原因;)
  • 是的,只要每个中间类也执行parent::foo()(或您的情况下为parent::_foo())。我不会说我担心它。我只是好奇。
  • 这是一个有趣的问题,但我想我从来没有遇到过让每个后续类都调用父级的问题。
  • 没有给父母打电话,但连续的电话确实让我想起了责任链模式。
【解决方案2】:

只要您在子类中覆盖您的方法,我所知道的任何语言都无法强制执行父方法的行为。如果您只是为您的应用程序编写代码,您应该能够信任自己的代码来调用 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。)

【讨论】:

  • 你是对的,你的 PHP 很生锈,哈哈。但是你的设计简单而优雅,而且很有效。因为回调链既是定义的又是通过链增量添加的,所以无论继承有多深,都可以从基类中强制执行“契约”。天才:-)
  • +1 昨晚刚好在查找闭包和回调。我正在寻找一种方法来触发一个方法,显式调用它,或者让一个方法进行某种调度来调用父级和子级。我相信这些功能仅限于 PHP 5.3。无论如何,很好的答案 bioneuralnet 很高兴看到你找到了解决方案 drrcknlsn :)。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    PHP 没有办法专门执行此操作;

    但是,如果 Foo::foo 必须总是在任何 subclass::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. 
      }
    
    }
    

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

    但是,是的,也许您实际上需要某种 pub-sub 模式.. 或者谁知道呢?

    您是在询问如何实施设计问题的解决方案,您应该特别询问如何解决您的设计问题。

    【讨论】:

    • 最后的评论很好。 “你在问如何实现设计问题的解决方案,你应该特别问如何解决你的设计问题。”
    • 本质上,继承链的每一层都有共同的行为。我正在寻找一种 DRY 方法来在单个方法调用中执行所有这些代码。我希望这有助于解释得更好一点?
    • @drrcknlsn:确实如此。如果您可以举一个可能需要此问题的示例,请在上面发表评论;)
    猜你喜欢
    • 2021-12-07
    • 2013-06-07
    • 2010-09-09
    • 1970-01-01
    • 2012-12-03
    • 1970-01-01
    • 2015-04-22
    • 2012-12-24
    相关资源
    最近更新 更多