【问题标题】:Use instance of child class instead of parent class given a condition给定条件使用子类的实例而不是父类
【发布时间】:2020-02-20 17:07:17
【问题描述】:

我正在努力将一个庞大的整体类分解为几个子类,但是一次完成所有工作太多了,所以我希望在时间允许的情况下将它们一个一个地拆分为多个版本。它是一个授权某些通道的身份验证类,所以目前它看起来像这样:

$auth = new Auth($user, $data);
$output = $auth->authChannel($channelName);

Auth里面,基本上是这样的:

public function __construct($user, $data)
{
    $this->user = $user;
    $this->data = $data;
}

public function authChannel($channel)
{
    $this->setUserData();

    if (isset(self::CHANNEL_AUTH_FUNCTIONS[$channel])) {
        $authFunction = self::CHANNEL_AUTH_FUNCTIONS[$channel];
        return $this->$authFunction();
    } else {
        // invalid channel
    }
}

所以self::CHANNEL_AUTH_FUNCTIONS基本上就是['channelA' => 'authChannelA', 'channelB' => 'authChannelB']等,所有这些功能都在这一类中。

现在我想做的,一次一个,是if $legacyChannel => callLegacyFunction() / else $newChannel => instantiate its own class and call auth()

所以我将Auth.php 放入它自己的命名空间中,并在同一个命名空间中拥有新的Channel.php 类。和Channel extends Auth

目前我有这个:

public function authChannel($channel)
{
    $this->setUserData();

    if (isset(self::CHANNEL_AUTH_LEGACY_FUNCTIONS[$channel])) {
        $authFunction = self::CHANNEL_AUTH_LEGACY_FUNCTIONS[$channel];

        if ($authFunction) {
            return $this->$authFunction();
        } else {
            $authClassName = __NAMESPACE__ . '\\' . ucwords($channel);
            $authClass = new $authClassName($user, $data);
            return $authClass->auth();
        }
    } else {
        // invalid channel
    }
}

有没有更好的方法来做到这一点?目前这似乎有点浪费,因为创建了两个不同的对象,并且我相信例如需要再次调用 setUserData() 函数。我也想知道除了__NAMESPACE__ . / . $className之外是否有更好的方法来获取动态类名。

【问题讨论】:

    标签: php oop design-patterns


    【解决方案1】:

    在该代码开始看起来更好之前,您必须进行大量工作。我将尝试建议尽可能少的更改,以使“迁移”尽可能轻松,尽管您离干净的设计还有几步之遥。

    首先,您可以为新的身份验证类创建一个AuthStrategyInterface

    interface AuthStrategyInterface
    {
        public function supports(string $channel): bool;
        public function auth($user, $data);
    }
    

    您的每个新身份验证类都应实现此接口。 supports($channel) 方法很容易理解:如果一个认证类可以处理某个通道,它应该返回true。

    您的Auth 课程需要一种方法来注入这些策略。通常你会在构造函数中这样做......但为了保持你的 API 不变,我们只需为此创建一个 setter 方法。

    在执行authChannel()时,它会首先检查注入的策略,看是否有支持使用的$channel,并尽可能使用。如果没有,请返回检查您的旧实现。

    这样,您在添加新的身份验证策略时无需接触任何旧代码。随着您添加新的实现,您逐渐strangling 遗留系统。有一次没有使用旧的实现,您可以进入新的代码重构阶段。

    class Auth {
        private iterable $strategies = [];
    
        public function __construct($user, $data)
        {
            $this->user = $user;
            $this->data = $data;
        }
    
        public function setAuthStrategies(iterable $strategies)
        {
            $this->strategies = $strategies;
        }
    
    
        public function authChannel($channel)
        {
            $this->setUserData();
    
            // check if any of the new strategies supports  
            foreach ($this->strategies as $strategy) {
                if ($strategy->supports($channel) {
                    return $strategy->auth($this->user, $this->data);
                }
            }
    
            // check "legacy" authentication methods.
            if (isset(self::CHANNEL_AUTH_FUNCTIONS[$channel])) {
                $authFunction = self::CHANNEL_AUTH_FUNCTIONS[$channel];
                return $this->$authFunction($this->user, $this->data);
            }
    
            // no valid authentication method
            return false;     
        }
    }
    

    要使用它,你可以这样做:

    $fooAuthStrategy = new FooAuthStrategy();
    $barAuthStrategy = new BarAuthStrategy();
    $bazAuthStrategy = new BazAuthStrategy();
    
    $auth = new Auth($user, $data);
    $auth->setAuthStrategies(
        [
            $fooAuthStrategy,
            $barAuthStrategy,
            bazAuthStrategy
        ]
    );
    
    $auth->authChannel($channel);
    

    具体情况会根据您的应用程序的设置方式而有所不同,但与您当前的方法相比,这样的方法会让您朝着更好的方向走得更远。

    【讨论】:

    • 嘿@yivi,感谢您的回复——这很有帮助!我有几个问题: 1. 基本上,每个通道都会有一个 Auth 类,这意味着一个类可以支持一个通道。话虽如此,当我已经拥有$channels => Classes 的一对一映射时,是否需要遍历$strategies 并检查supports($channel)?出于好奇,2. 我们为什么在此处的类型提示中使用iterable 而不仅仅是array
    • 另外,由于我在很大程度上可以控制链中的所有类,因此绝对愿意听到可能更适合这个问题的不同设计模式(即使我只有时间,例如,实现“第 1 步“可能是一个干净设计的多步方法)
    • 1.) 迭代和检查supports() 让您无需维护 auth->channels 的映射。您实现了一个新的 auth 类,实现了适当的 supports() 方法,并且您不必触及其他任何东西。 2.) arrayiterable 的一种类型。
    【解决方案2】:

    我不知道我是否正确理解了这个问题,但你不能那样做吗?

     public function authChannel($channel)
    {
        $this->setUserData();
    
        if (!isset(self::CHANNEL_AUTH_LEGACY_FUNCTIONS[$channel])) {
            // Invalid channel
            return;
        }
        return  self::CHANNEL_AUTH_LEGACY_FUNCTIONS[$channel]
                ? $this->$authFunction()
                : parent::auth();
    }
    
    

    【讨论】:

    • 这假设它是从子函数调用的,对吧?我的情况是他们会调用父函数Auth,但(可能)传入子类的名称,所以我需要更像 return $legacy 的东西? $this->$authFunction() : child::auth();
    • 对不起ShaneOH,我没有正确理解你。您可以在父亲中动态地实例化儿子(就像您在示例中所做的那样),但这是不受欢迎且非常丑陋的,请考虑创建一个充当身份验证提供者的类并为您提供要使用的身份验证类实例。
    猜你喜欢
    • 2013-05-07
    • 2013-06-10
    • 1970-01-01
    • 2019-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-28
    • 1970-01-01
    相关资源
    最近更新 更多