编写处理另一个函数输出的PHP函数时使用的设计模式



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

3返回一个CCD_ 4,该CCD_。

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

稍后,myFunctionA()被更新,因此它不再返回包括关键字Page_Type的对象,而是返回具有数组值的关键字Page_Types

因此,myFunctionB()现在也需要更新——它将不再处理作为字符串的Page_Type,而是处理作为数组的Page_Types

如果我理解正确(我可能没有理解),上面是Dependency-Request的一个例子,它引发的广泛重构可以通过(我认为)部署依赖注入模式(甚至可能是服务定位器模式?)而不是(??)来避免

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

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

如果myFunctionH()需要myFunctionG()需要myFunctionF(),而CCD_26需要myFunctionA()呢?我不想现在更新myFunctionA()意味着还要更新另外三个函数(FmyFunctionA()0和H)。


尝试回答:

我目前能想到的最好的答案——这可能不是最佳实践的答案,因为我还不知道是否有一个正式的问题与我在上面的问题中描述的问题相对应——是对我如何展示设置的以下重述:

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

myFunctionA__v1_0()返回包含密钥的Object具有字符串值的CCD_ 35。

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

后来,myFunctionA__v1_0()仍然存在,但也由Object0继承返回一个包含关键字Page_Types-的对象,该关键字具有数组值。

为了使CCD_ 42访问CCD_,现在还需要一个myFunctionB__v1_1(),能够处理阵列CCD_ 45。

这可以概括为:

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

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

我不知道我是否以正确的方式处理这个问题,但这是我迄今为止想出的最好的方法。

在编程中,提供者-即myFunctionA()对其消费者myFunctionB()一无所知是非常常见的。唯一处理此问题的正确方法是预先定义API,从不更改它;)

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

至于您的提供程序myFunctionA():如果您不能为数据本身预先定义接口/API,即您知道数据的结构必须更改(以非向后兼容的方式),但您不知道如何。。。那么您需要以某种方式版本

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

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

你提出的解决方案听起来应该有效。然而,至少有几个缺点:

  • 它要求您保留一个不断增加的遗留函数列表,以防有任何消费者请求他们的数据。这将变得非常复杂,从长远来看可能是不可能的
  • 根据未来需要进行的更改,可能根本不可能为myFunctionA__v1_0()生成输出——在您的示例中,您在系统中添加了多个page_types的可能性——在这种情况下,您可能只需重写v1_0即可使用第一个CCD_63,传统消费者会很高兴。但是,如果您决定从系统中完全放弃page_types的概念,则必须以某种方式计划完全删除v1_0。因此,你需要建立一种方式来向消费者传达这一点

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

由于我们已经确定:

  1. 必须进行向后不兼容的更改
  2. 你对消费者一无所知,也没有能力在需要时改变他们

我建议您不要为数据定义不可变的API,而是定义一个不可变的API,以便在应该必须升级时与消费者进行通信。

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

接受提供程序中的版本参数:

这个想法是让使用者明确地告诉提供者返回哪个版本。

提供商可能如下所示:

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

依赖注入在OOP上下文中更相关。然而,我在这里要做的主要事情是停止考虑归还你现有的东西,开始考虑这两种方法是如何协同工作的,以及它们的合同是什么。

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

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

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

通过将返回类型从字符串更改为数组,似乎破坏了myFunctionA和myFunctionB之间的接口。我认为DI帮不了什么忙。

首先,这不是依赖注入。

你的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()的响应,都意味着要编辑HandData Structure。您只需要编辑Converter即可确保它继续正确地从Hand转换为Leg。您不需要触摸myFunctionB,反之亦然。

当你有另一个Producer时,这将非常有帮助,它将提供你在问题中提到的HandmyFunctionC()myFunctionD()。。。你还有很多其他的Consumer会消耗Leg,比如myFunctionH()myFunctionG()。。。

希望这能帮助

最新更新