您要做的第一件事是列出分析领域时想到的所有概念。我看到了:
Picture
Dimension
Dimensions
甚至,也许在 Dimensions 对象后面,您可能有一个通用(抽象)值对象 Collection 概念,它可以提供 Dimensions 以及许多其他强类型集合。
即使你可以有一个 Interfaces\Collection 来允许你域中的任何元素接受“事物的通用集合”,如果你做的很快并且不想在优化你的模型之前输入字符串。
假设 PHP 没有类模板,就像在 C++ 中一样,你不能做像 class Dimensions extends Collection<Dimension> 这样的事情,我所做的是通过严格检查输入元素来强制类型。
因此,我的集合知道它们所拥有的类是什么,并且接口通过 getItemClassName() 方法反映了这一点。
Collection 接口
这是我的基本Interfaces\Collection 的样子:
<?php
declare( strict_types = 1 );
namespace XaviMontero\ThrasherPortage\Base\Collection\Interfaces;
interface Collection extends \Countable, \IteratorAggregate, \ArrayAccess
{
public function getItemClassName() : string;
public function getItem( int $index );
}
注意 getItem() 确实不 声明显式返回类型。我将覆盖实现接口的类中的返回类型。
基本抽象不可变Collection
这是我的基本抽象 Collection(将由其他特定于类型的类实现)的样子:
<?php
declare( strict_types = 1 );
namespace XaviMontero\ThrasherPortage\Base\Collection;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\ImmutabilityException;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\InvalidTypeException;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\OutOfRangeException;
abstract class Collection implements Interfaces\Collection
{
// Based on http://aheimlich.dreamhosters.com/generic-collections/Collection.phps
protected $itemClassName;
protected $items = [];
/**
* Creates a new typed collection.
* @param string $itemClassName string representing the class name of the valid type for the items.
* @param array $items array with all the objects to be added. They must be of the class $itemClassName.
*/
public function __construct( string $itemClassName, array $items = [] )
{
$this->itemClassName = $itemClassName;
foreach( $items as $item )
{
if( ! ( $item instanceof $itemClassName ) )
{
throw new InvalidTypeException();
}
$this->items[] = $item;
}
}
public function getItemClassName() : string
{
return $this->itemClassName;
}
public function getItem( int $index )
{
if( $index >= $this->count() )
{
throw new OutOfRangeException( 'Index: ' . $index );
}
return $this->items[ $index ];
}
public function indexExists( int $index ) : bool
{
if( $index >= $this->count() )
{
return false;
}
return true;
}
//---------------------------------------------------------------------//
// Implementations //
//---------------------------------------------------------------------//
/**
* Returns the count of items in the collection.
* Implements countable.
* @return integer
*/
public function count() : int
{
return count( $this->items );
}
/**
* Returns an iterator
* Implements IteratorAggregate
* @return \ArrayIterator
*/
public function getIterator()
{
return new \ArrayIterator( $this->items );
}
public function offsetSet( $offset, $value )
{
throw new ImmutabilityException();
}
public function offsetUnset( $offset )
{
throw new ImmutabilityException();
}
/**
* get an offset's value
* Implements ArrayAccess
* @see get
* @param integer $offset
* @return mixed
*/
public function offsetGet( $offset )
{
return $this->getItem( $offset );
}
/**
* Determine if offset exists
* Implements ArrayAccess
* @see exists
* @param integer $offset
* @return boolean
*/
public function offsetExists( $offset ) : bool
{
return $this->indexExists( $offset );
}
}
这强烈基于 http://aheimlich.dreamhosters.com/generic-collections/Collection.phps 并进行了以下更改:
- 基本类型
- 我删除了所有与基本类型相关的内容。
- 在我的例子中,所有在模型中、在特定类型的集合中非常重要的东西都非常重要,以至于它们本身应该有一个 ValueObject。
- 假设我有一个 ID 列表,并且在我的应用程序中使用 sha1 ID。它们对我来说不是字符串。它们是 ID 类型的对象。
-
Collection 本身是一个值对象,因此它是不可变的。
- 我阻止了对集合内容的任何编辑权限。
- 如果这些是基于值对象的集合,那么集合本身就是一个值对象。
- 所以值是由构造函数传递的,像
offsetSet()和offsetUnset()这样的方法是被禁止的。
我有一组 4 个基于集合的异常,其中 3 个可能由 Base\Collection 抛出(另一个只是抽象的包罗万象)。
abstract class CollectionException extends \RuntimeException
class ImmutabilityException extends CollectionException
class InvalidTypeException extends CollectionException
class OutOfRangeException extends CollectionException
这些名称非常具有描述性(我希望 ;))。
注意 __construct 有 2 个参数:要存储的对象的类名,以及要放入集合中的通用对象数组。函数的输入类型不能在子类中被覆盖,因此 PHP 中没有任何“好方法”可以做到这一点,除了吃一个泛型数组并通过循环来测试类型。
注意 public function getItem( int $index ) 仍然没有声明返回类型。这是故意的。您可以覆盖输出类型。 “通用”集合在执行时可能会返回“任何东西”。
具体的强类型Dimensions 集合。
现在您有了一个通用类型的 Collection 基础对象(它是抽象的),一个集合需要是一些东西的集合。在您的情况下,是维度对象的集合。
假设有一个 Dimension 命名空间,并且有 Dimension 和Dimensions。
尺寸应该是这样的:
<?php
declare( strict_types = 1 );
namespace XaviMontero\ThrasherPortage\Dimension;
use XaviMontero\ThrasherPortage\Base\Collection\Collection;
class Dimensions extends Collection
{
public function __construct( array $items = [] )
{
parent::__construct( Dimension::class, $items );
}
public function getItem( int $index ) : Dimension
{
return parent::getItem( $index );
}
}
在这里你可以看到:
- Dimensions 是一个实际上是一个集合的类。
- 它只实现了 2 个具体方法:构造函数和 getItem。
- 这些实现只是对
parent 的包装。
-
__constructor 包装器接受一个东西数组(希望是Dimension 对象),并告诉父级通过硬编码将自己转换为Dimension::class 的集合,Dimensions 不能容纳其他任何东西比Dimension。这样可以确保此类的任何消费者都无法阻止任何事情。
- getItem 只是从父项获取项目并强制返回类型为
Dimension,因此Dimensions 的任何消费者都严格确定集合返回的任何对象是 类型为@ 987654352@.
Picture 类
你的代码没有变化,除了我喜欢用复数命名集合(例如Dimensions)而不是用一个足够的集合来调用它们(例如DimensionCollection)。
此外,我没有基本的ValueObject 类型,因为它不会给我增加任何东西。即使equals() 在基类中也没有用,因为您不会在抽象基类中强制使用具体类型,并且如果您确实将通用值对象作为equals 的类型,那么您将允许比较苹果和橙子。所以对我来说,值对象只是没有设置器的普通类。
最后,对于强类型的值对象不可变集合,通过getDimensions() 方法给它aawy 是非常安全的,因为没有人能够改变它的内容(请记住基本@ 中的throw new ImmutabilityException(); 987654361@班级)。
Class Picture
{
/** @var Dimensions */
private $dimensions;
public function __construct( Dimensions $dimensions )
{
$this->dimensions = $dimensions;
}
public function getDimensions() : Dimensions
{
return $this->dimensions;
}
}
消费它
现在您可以通过构建图片并查询它们来使用它了。
我将使用TDD 表示法:expected 来创建值,sut = 被测系统,actual 来查看我得到了什么,所以我可以对此进行单元测试。
设置图片
只需在构造函数中构建所有内容:
$expectedDimension1 = new Dimension( $whateverParamsTakesDimension1 );
$expectedDimension2 = new Dimension( $whateverParamsTakesDimension2 );
$expectedDimension3 = new Dimension( $whateverParamsTakesDimension3 );
$expectedDimensions = new Dimensions( [ $expectedDimension1, $expectedDimension2, $expectedDimension3 ] );
$sut = new Picture( $expectedDimensions );
查询图片
所有类型都完美设置:
$actualDimensions = $sut->getDimensions(); // Forces a Dimensions type.
$actualDimension1 = $actualDimensions->getItem( 0 ); // Forces a Dimension type.
$actualDimension2 = $actualDimensions->getItem( 1 );
$actualDimension3 = $actualDimensions->getItem( 2 );
您现在可以在PictureTest 中断言:
$this->assertSame( $expectedDimensions, $actualDimensions );
$this->assertSame( $expectedDimension1, $actualDimension1 );
$this->assertSame( $expectedDimension2, $actualDimension2 );
$this->assertSame( $expectedDimension3, $actualDimension3 );
纯粹的 TDD
如果你是纯粹主义者,你会
- 测试
Dimensions在DimensionsTest中的内容。
- 不要在
Picture中测试Dimensions的内容。
- 但仍测试
PictureTest 中的$actualDimensions。
- 此外,您不会测试
assertSame( $a, $b );,但您会assertTrue( $a->equals( $b ) ); 并拥有equals(),不仅在Dimension 中,而且在基础集合中,它在每个项目上循环并在每个元素上执行equals()。
我只是跳过了这部分,因为它超出了问题的范围,这只是关于如何制作集合。
结论
事实上,在公开的方法中,您正在结合您在问题中发布的两种方法。
一方面你使用强类型。
另一方面,您也在循环元素以检查它们的类型。
但是使用接收类型的抽象基集合,并具有强制输入类型(在__construct() 中)和输出类型在getItem() 中的实现,您可以转移单一责任 放入适当的位置(SOLID rulez)。
检查Dimension对象的类型不是图片的责任。那又是谁? Dimensions 应该这样做(通过硬编码传递给基本集合的类型)。然后使用基本集合来制作您作为第一个示例发布的循环,由于语言限制,必须在 PHP 中在运行时完成。
收藏变得简单
现在你有了这个,如果你想扩展你的模型
Picture
Dimension
Dimensions
进入
Picture
Pictures
Dimension
Dimensions
就像创建一个扩展Base\Collection 并将类型强制为Picture 的Pictures 一样简单。就这么简单。
等等。完成。
完整、强类型、完全可测试。还想要什么?