【问题标题】:What Design Pattern to use when writing a PHP function which processes the output from another function?编写处理来自另一个函数的输出的 PHP 函数时使用什么设计模式?
【发布时间】:2020-05-03 06:08:44
【问题描述】:

我有两个函数:myFunctionA()myFunctionB()

myFunctionA() 返回一个 Object,其中包括具有字符串值的键 Page_Type

myFunctionB() 处理myFunctionA() 返回的Object 中的多个条目,包括Page_Type 及其字符串值。

稍后,myFunctionA() 已更新,因此它不再返回包含键 Page_Type 的对象,而是返回键 Page_Types - 它具有数组值。

因此,myFunctionB() 现在也需要更新 - 它将不再处理 Page_Type 这是一个字符串,而是 Page_Types 这是一个数组。 p>

如果我理解正确(我可能没有理解),以上是 依赖请求 的示例,并且可以通过(我认为)部署 来避免它引发的大量重构依赖注入模式(或者甚至可能是服务定位器模式?)而不是(??)

但是,尽管阅读了这个主题,我仍然不确定依赖注入如何在 PHP 或 Javascript 函数中工作(大部分解释涉及像 C++ 这样的编程语言和像 Classes 这样的 OOP 概念,而我正在处理 PHP 和 javascript 中的第三方函数)。

是否有任何方式来构建我的代码,以便更新myFunctionA()(以任何重要方式)将不需要我也更新myFunctionB()(以及所有其他 em> 函数调用myFunctionA() - 例如myFunctionC()myFunctionD()myFunctionE() 等)?

如果myFunctionH() 需要myFunctionG() 需要myFunctionF() 而需要myFunctionA() 怎么办?我不希望现在更新myFunctionA() 意味着另外三个函数(FGH)都必须更新。


尝试回答:

目前我能想到的最佳答案 - 这可能不是最佳实践答案,因为我还不知道是否存在与我所描述的问题相对应的正式问题在上面的问题中 - 以下是我如何呈现设置的重述:

我有两个(不可变的)函数:myFunctionA__v1_0()myFunctionB__v1_0()

myFunctionA__v1_0() 返回一个包含键的对象 Page_Type 有一个字符串值。

myFunctionB__v1_0() 处理对象中的多个条目 由myFunctionA__v1_0() 返回,包括Page_Type 及其 字符串值。

后来,myFunctionA__v1_0() 仍然存在,但也由myFunctionA__v2_0() 继任 返回一个包含键 Page_Types 的对象 - 它有一个数组值。

为了让myFunctionB访问myFunctionA__v2_0()返回的对象, 现在还需要一个myFunctionB__v1_1(),能够处理 数组Page_Types

这可以概括为:

  • myFunctionB__v1_0() 需要 myFunctionA__v1_0() 返回的对象
  • myFunctionB__v1_1() 需要 myFunctionA__v2_0() 返回的对象

由于每个函数在被正式命名后都变得不可变,所以永远不会发生的是,我们最终得到了一个myFunctionB__v1_0() 需要myFunctionA__v2_0() 返回的对象的示例。

我不知道我的方法是否正确,但这是迄今为止我想出的最好方法。

【问题讨论】:

  • 您似乎将“依赖注入”(这是一种实践)与“依赖注入容器”(这是一个具体的解决方案/库,并且在“服务定位器”解决方案中有替代方案)混淆了。另外,开始推动使用 OOP 而不是这种疯狂的程序。
  • 谢谢,@tereško。为了确保给定函数不会调用一个函数然后返回第一个函数无法处理的数据,您能否推荐一种明确的替代方法来按名称对函数进行版本控制,然后使它们不可变(正如我上面建议的那样)?
  • 您拥有的是“已发布的 API”,但由于愚蠢地坚持使用函数,您无法使用接口作为边界(输入和输出)。因此,要么开始使用像 21 世纪这样的类,要么坚持使用版本化和不可变函数。这是自伤。
  • 谢谢。我应该强调(尽管应该很明显)我不是程序员。您对“输入/输出接口和边界”有任何阅读建议吗?我在 OOP 上找到了这个:cs.utexas.edu/~mitra/csSummer2012/cs312/lectures/oop.html,但它相当高级/抽象。你能推荐其他人吗?非常感谢,@tereško。
  • 我还在Entity-Control-Boundary pattern 上找到了一个维基百科条目。当您提到“边界接口”时,您所指的是那种东西吗?

标签: javascript php design-patterns architecture


【解决方案1】:

这在provider的编程中很常见 - 即。 myFunctionA() 对其消费者一无所知myFunctionB()唯一正确的处理方法是预先定义一个 API 并且永远不要更改它;)

我看不出对 consumer 进行版本控制的目的——这个原因必须是 myFunctionB() 的“下游”——即。 myFunctionB()消费者myFunctionB() 的作者无法控制...在这种情况下,myFunctionB() 本身成为提供者,并且作者将不得不处理这个问题(也许使用与你相同的模式)......但这不是你的问题要处理。

至于您的提供者myFunctionA():如果您不能预先为数据本身定义接口/API - 即。你知道数据的结构必须改变(以非向后兼容的方式),但你不知道如何......然后你需要版本一些东西 一种或另一种方式

自从您预见到这一点并从一开始就做好计划以来,您已经领先了大多数人。

避免在某些时候对使用者myFunctionB() 进行更改的唯一方法是以向后兼容的方式对提供者myFunctionA() 进行全部 更改。您描述的更改不向后兼容,因为 myFunctionB() 不可能知道如何处理 myFunctionA() 的新输出而不进行修改。

您提出的解决方案听起来应该可行。但是,至少有两个缺点:

  • 它要求您保留一个不断增长的遗留函数列表,以防有任何消费者请求他们的数据。这将变得非常难以维护,从长远来看可能是不可能的。
  • 根据将来需要进行哪些更改,可能根本无法为 myFunctionA__v1_0() 生成输出 - 在您的示例中,您可以在系统中添加多个 page_types - 在这种情况下,您可能只需重写v1_0 以使用第一个,旧的消费者会很高兴。但是,如果您决定从系统中完全删除 page_types 的概念,您将不得不计划以一种或另一种方式完全删除 v1_0。因此,您需要建立一种与消费者沟通的方式。

处理这个问题的唯一正确方法仍然是预先定义一个 API 并且永远不要更改它。

既然我们已经确定:

  1. 必须进行向后不兼容的更改
  2. 您对消费者一无所知,也无权在需要时更改它们

我建议不要为您的数据定义一个不可变的 API,而是定义一个不可变的 API,让您在消费者应该必须升级时与他们交流。

这听起来可能很复杂,但不一定是这样:

接受provider中的版本参数:

这个想法是让消费者明确告诉提供者返回哪个版本。

提供者可能如下所示:

function myFunctionA(string $version) {
    $page_types = ['TypeA', 'TypeB'];
    $page = new stdClass();
    $page->title = 'Page title';
    switch ($version) {
        case '1.0':
            $page->error = 'Version 1.0 no longer available. Please upgrade!';
            break;
        case '1.1':
            $page->page_type = $page_types[0];
            $page->warning = 'Deprecated version. Please upgrade!';
            break;
        case '2.0':
            $page->page_types = $page_types;
            break;
        default:
            $page->error = 'Unknown version: ' . $version;
            break;
    }
    return $page;
}

因此,提供者接受一个参数,该参数将包含消费者可以理解的版本 - 通常是消费者上次更新时的最新版本。

提供者尽最大努力提供所请求的版本

  • 如果不可能有一个“合同”通知消费者($page->error 将存在于返回值上)
  • 如果它可能的,但是有一个更新的版本可用,另一个“合同”已经到位以通知消费者这一点($page->warning 将存在于返回值上)。

并处理消费者中的一些案例:

消费者需要发送它期望的版本作为参数。

function myFunctionB() {
    //The consumer tells the provider which version it wants:
    $page = myFunctionA('2.0');
    if ($page->error) {
        //Notify developers and throw an error
        pseudo_notify_devs($page->error);
        throw new Exception($page->error);
    } else if ($page->warning) {
        //Notify developers
        pseudo_notify_devs($page->warning);
    }
    do_stuff_with($page);
}

myFunctionB() 旧版本的第二行 - 或完全不同的消费者 myFunctionC() 可能会要求旧版本:

$page = myFunctionA('1.1');

这使您可以随时进行向后兼容的更改,而无需消费者进行任何操作。您可以尽最大努力在可能的情况下仍支持旧版本,从而为旧版消费者提供“优雅”的降级。

当您必须进行重大更改时,您可以继续支持旧版本一段时间,然后最终将其完全删除。

元信息

我不确定这是否有用...但您可以为使用过时版本的消费者添加一些元信息:

function myFunctionA(string $version) {
    # [...]
    if ($page->error || $page->warning) {
        $page->meta = [
            'current_version' => '3.0',
            'API_docs' => 'http://some-url.fake'
        ]
    }
    return $page;
}

这可以在消费者中使用:

pseudo_notify_devs(
    $page->error .
    ' - Newest version: ' . $page->meta['current_version'] .
    ' - Docs: ' . $page->meta['API_docs']
);

...如果我是你,我会小心不要让事情过于复杂...总是KISS

【讨论】:

  • 很棒的答案。谢谢你。我非常喜欢每个函数返回版本数据然后启用任何需要另一个函数的函数具有检查它调用并相应响应的函数的版本数据。这使得myFunctionA__v1_0()myFunctionA__v2_0()myFunctionA__v2_1() 能够全部简单地称为myFunctionA()。构思巧妙。
  • 进一步考虑......数组可能包含所有必要的版本信息:'Recommended' => ['3.1.1', '3.1', '3.0'], 'Deprecated' => ['2.0'], 'Obsolete' => ['1.0']
  • 嗯......虽然......我不确定这个数组(或你的string $version)可以去哪里(?)它不能进入​​myFunctionA(),因为,自我显然,myFunctionA() 的每个版本都会告诉您它是最新的推荐版本。 (只有以后的版本会告诉你那个版本的myFunctionA() 已经被弃用了……)
  • 也许我不完全理解您的用例。在几乎任何现实生活中的应用程序中,您都希望您的消费者尽可能使用最新版本……而且您今天永远不会使用除最新版本之外的任何版本来编写新消费者……那么告知消费者的确切目的是什么关于过去的版本?要么您正在使用最新版本,要么您正在使用应该尽快更新的某个版本,或者您正在尝试使用已经消失的版本。在第一种情况下你很好,在后两种情况下你都需要更新到最新的。
  • @Rounin 重新阅读您的评论我不确定您是否理解基本概念。消费者在调用提供者时必须明确声明 可以理解的版本。提供者然后尽最大努力返回该版本 - 但是有一个“合同”来处理不可能的情况(或者它是,但是有一个更新的版本应该由消费者实施可能的)。我再次编辑,试图使这个想法更明确。
【解决方案2】:

首先,这不是依赖注入。

您的myFunctionA() 可以称为生产者,因为它提供数据,因此应该证明它是Data Structure。 您的myFunctionB() 可以称为消费者,因为它使用myFunctionA 提供的数据。

因此,为了使您的生产者和消费者独立工作,您需要在它们之间添加另一层,调用ConverterConverter 层会将Producer 提供的Data Structure 转换为Consumer 可以理解的众所周知的Data Structure

我真的推荐你阅读本书Clean Code第6章:对象和数据结构。这样你就能完全理解上面的概念了,关于Data Structure

例子

假设我们有Data Structure 呼叫Hand,拥有右手和左手属性。

class Hand {
    private $rightHand;
    private $leftHand
    // Add Constructor, getter and setter
}

myFunctionA() 将提供对象HandHandData Structure

function myFunctionA() {
    $hand = Hand::createHand(); //Assume a function to create new Hand object
    return $hand;
}

假设我们还有另一个Data Structure,调用Leg,Leg 将能够通过myFunctionB() 消费;

class Leg {
    private $rightLeg;
    private $leftLeg
    // Add Constructor, getter and setter
}

然后,我们需要有一个转换器,在中间,从手转换为腿,并使用myFunctionB()

class Converter {
    public static function convertFromHandToLeg($hand) {
        $leg = makeFromHand($hand); //Assume a method to convert from Hand to Leg
        return $leg; 
    }
}
myFunctionB(Converter::convertFromHandToLeg($hand))

因此,每当您编辑myFunctionA() 的响应时,意味着您将编辑Data StructureHand。您只需编辑Converter 以确保它继续正确地从Hand 转换为Leg。您无需触摸myFunctionB,反之亦然。

当您有另一个 Producer 将提供 Hand 时,这将非常有用,就像您在问题中提到的那样,myFunctionC()myFunctionD()... 而且您还有许多其他 Consumer 将消耗 @ 987654358@ 喜欢myFunctionH(), myFunctionG()...

希望有帮助

【讨论】:

  • 谢谢,这很有帮助。我将阅读 Robert C. “Uncle Bob” Martin 的 “Clean Code” 的第 6 章。在我的设置中,any Producer 函数将输出 PHP 关联数组,any Consumer 将使用 PHP 关联数组。 (所以我认为我已经完成了一半(?))但我仍然试图了解如何处理上面问题中概述的场景,其中生产者关联数组的值类型已更新,不再是消费者期望消费的价值(即现在是 array 而不是 string)。
  • 我看到,在第 97 页,Martin 准确地描述了我面临的问题:“程序代码使得添加新数据结构变得困难,因为所有函数都必须更改。”
  • 我读了这一章,但我没有被顿悟的闪电击中。我怀疑可能使这种情况特别不寻常的是不对称知识:也就是说,生产者的作者不会知道消费者或消费者是否存在。只有 Consumer 的作者知道 Producer 的存在和结构。 (但据我所知,在编程世界中,这可能不像我想象的那么不寻常)。
  • @Rounin 关联数组是一种数据结构,需要一个转换器才能走。我在答案中添加了示例,希望这会对您有所帮助。您可以假设类 Hand 等于关联数组 ['leftHand' => something, 'rightHand' => something]。
  • @Rounin Martin 准确地描述了我面临的问题:“程序代码使得添加新数据结构变得困难,因为所有函数都必须更改。
【解决方案3】:

您似乎通过将返回类型从字符串更改为数组来破坏了 myFunctionA 和 myFunctionB 之间的接口。 我不认为 DI 可以提供帮助。

【讨论】:

  • 谢谢。这些是第三方函数(即用户编写的函数)。我们可以假设myFunctionA() 的作者不会知道myFunctionB() 的作者正在请求前者的输出作为依赖。
【解决方案4】:

依赖注入在 OOP 上下文中更相关。但是,我在这里要做的主要事情是停止考虑返回您可用的内容,并开始考虑这两种方法如何协同工作以及它们的合同是什么。

弄清楚 myFunctionA() 的逻辑输出是什么,将该合约编码为一个对象,然后将您拥有的数据转换为该格式。这样,即使您在 myFunctionA() 中获取数据的方式发生了变化,您也只需更新该一次转换。

只要您遵守该约定(可以通过自定义对象表示)、myFunctionB() 和其他希望根据约定接收数据的方法,您就不必再更改这些方法。

因此,我在这里的主要收获是开始考虑您需要的数据,并以对您的应用程序最有意义的方式而不是在您收到的结构中传递它。

【讨论】:

  • 谢谢。我明白这是一个困难的情况,因为我在这里处理的不是第一方编写的函数,而是第三方编写的函数(即用户编写的函数)。我们可能在旧金山有一个团队编写了myFunctionA(),在班加罗尔有一个团队编写了myFunctionB(),它请求myFunctionA() 的返回输出作为依赖项。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-04
  • 1970-01-01
  • 2021-10-16
  • 2019-04-02
  • 2019-06-11
  • 2021-11-29
  • 2016-04-18
相关资源
最近更新 更多