注意: 为了防止因为“好的实践”基于个人观点而被踩,您也可以将问题改述为:检查返回值类型是补偿PHP缺乏泛型的好实践吗?(我没有使用这个,因为它暗示了存在缺陷)。
问题
从Java/C#世界来看,PHP的松散类型处理总是有点让人烦恼。当输入参数的类型提示引入后,情况有所改善,但我仍然缺少泛型和对返回值的类型提示。
我发现自己偶尔需要在代码中显式检查类型-这感觉有点不对,因为语言本身可以为我处理-我想向社区提出以下问题:
- 检查返回值类型是补偿PHP缺乏泛型的好实践吗?
- 是否有更好/更标准的方法来解决这个问题?
- 目前正在讨论在PHP中实现泛型吗?
例子
为了更好地理解这个问题,考虑以下例子:
假设我们正在构建一个框架,将输入数据转换为其他输出数据。例如:
将表示XML文档的字符串转换为DomDocument,然后通过xpath表达式选择所述DomDocument的标题,并将其转换为另一个字符串。
(string) $xml =[TransformToDomDocument]=> (DomDocument) $doc =[TransformToString]=> (string) $title
现在假设输入的不是包含XML的字符串,而是Json(但是保持其他数据相同)。我们现在想要将Json输入转换为Json对象,并使用JsonPath表达式选择标题。
(string) $jsonString =[TransformToJson]=> (Json) $jsonObject =[TransformToString]=> (string) $title
(注:第二个例子应该说明整个框架应该是非常灵活的。)
使用一系列适配器对象执行转换,这些对象处理从输入到输出的转换:
interface AdapterInterface{
/**
* Transform some input data into something else.
* @param mixed $data
* @return mixed
*/
public function transform($data);
/**
* Set the Adapter that is used to preprocess the $data before calling $this->transform($data)
* @param AdapterInterface $adapter
*/
public function setPredecessorAdapter(AdapterInterface $adapter);
}
class XmlToDomDocumentAdapter implements AdapterInterface{
private $predecessor;
/**
* Transform an xml string into a DOMDocument.
* @param mixed $data
* @return DomDocument
*/
public function transform($data){
if($this->predecessor !== null){
$data = $this->predecessor->transform($data);
// At this point, we just have to "trust" that the predecessor returns a (string)
}
$doc = new DomDocument();
$doc->loadXml($data);
return $doc;
}
}
class DomDocumentToStringAdapter implements AdapterInterface{
private $xpathExpression;
private $predecessor;
/**
* Transform a DomDocument into a string.
* @param mixed $data
* @return string
*/
public function transform($data){
if($this->predecessor !== null){
$data = $this->predecessor->transform($data);
// At this point, we just have to "trust" that the predecessor returns a (DOMDocument)
}
$xpath = new DOMXpath($data);
$nodes = $xapth->query($this->xpathExpression);
if($nodes->length > 0){
throw new UnexpectedValueException("Xpath didn't match");
}
$result = $nodes->item(0)->nodeValue;
return $result;
}
}
使用方法:
$input = "..."
$xmlToDom = new XmlToDomDocumentAdapater();
$domToString = DomDocumentToStringAdapter();
$domToString->setPredecessorAdapter($xmlToDom);
$output = $domToString->transform($input);
问题出现在适配器依赖于它的前置适配器返回正确的输入时。
if($this->predecessor !== null){
$data = $this->predecessor->transform($data);
// At this point, we just have to "trust" that the predecessor returns a (DOMDocument)
}
在C#中,我会使用泛型来解决这个问题:Generics。
interface AdapterInterface{
/**
* Tranform some input data into something else.
* @param mixed $data
* @return T
*/
public function T transform<T>(object data);
}
/* using it */
//...
if(this.predecessor !== null){
data = this.predecessor.transform<string>(data);
// we now know for sure that the data is of type 'string'
}
//...
由于PHP不支持泛型,我在思考是否在每次调用transform($data)
后添加类型检查是一种好的实践:
if($this->predecessor !== null){
$data = $this->predecessor->transform($data);
if(!is_string($data){
throw new UnexpectedValueException("data is not a string!");
}
// we now know for sure that the data is of type 'string'
}
我的当前解决方案
我目前正在使用多个接口来定义transform
方法的输出,如下所示:
interface ToStringAdapterInterface extends AdapterInterface{
/**
* Transform some input data into something else.
* @param mixed $data
* @return string <<< define expected output
*/
public function transform($data);
}
interface ToDomDocumentAdapterInterface extends AdapterInterface{
/**
* Transform some input data into something else.
* @param mixed $data
* @return DOMDocument<<< define expected output
*/
public function transform($data);
}
在每个转换器中,我会确保仅接受适合的接口作为前继接口:
class DomDocumentToStringAdapter implements ToStringAdapterInterface {
private $xpathExpression;
private $predecessor;
public function __construct(ToDomDocumentAdapterInterface $predecessor){
$this->predecessor = $predecessor;
}
// ...
}
switch
语句或类似的方式。这些特性可以使代码更简洁,但它们的不存在并不会从根本上影响你编写同样功能的能力。 - deceze