使用动态数组和自定义集合类的规则



使用动态数组和自定义集合类的规则
这是我在使用动态数组时要遵守的规则。 实际上,这是设计数组的指南,但是我不想将其放在设计对象的指南中,因为并非每种面向对象的语言都具有动态数组。 这些示例是用PHP编写的,因为它类似于Java(您可能已经熟悉),但是使用动态数组而不是内置的集合类和接口。

使用数组作为列表


所有项目都必须是同一类型。


如果将数组用作列表(按一定顺序排列的值的集合),则所有值都必须具有相同的类型:

$goodList = [ 'a', 'b' ]; $badList = [ 'a', 1 ]; 

常见的列表类型注释样式为: @var array<TypeOfElment> 。 确保不添加索引类型,它应该始终为int

需要忽略每个项目的索引


PHP将为每个列表项(0、1、2等)自动创建一个新索引。 但是,您既不应依赖这些索引,也不应直接使用它们。 客户只能依靠iterablecountable

因此,您可以自由使用foreachcount() ,但不要使用for在列表项之间循环:

 // Good loop: foreach ($list as $element) { } // Bad loop (exposes the index of each element): foreach ($list as $index => $element) { } // Also bad loop (the index of each element should not be used): for ($i = 0; $i < count($list); $i++) { } 

在PHP中,如果列表中没有索引,或者索引数大于元素数,则for循环可能根本无法工作。

使用过滤器而不是删除项目


您可能希望按索引( unset() )删除项目,但是与其删除,不如使用array_filter()创建一个没有多余元素的新列表更好。

同样,不应依赖元素索引。 因此,在使用array_filter()请勿使用flag参数来按索引甚至按元素和索引过滤掉元素。

 // Good filter: array_filter( $list, function (string $element): bool { return strlen($element) > 2; } ); // Bad filter (uses the index to filter elements as well) array_filter( $list, function (int $index): bool { return $index > 3; }, ARRAY_FILTER_USE_KEY ); // Bad filter (uses both the index and the element to filter elements) array_filter( $list, function (string $element, int $index): bool { return $index > 3 || $element === 'Include'; }, ARRAY_FILTER_USE_BOTH ); 

将数组用作关联数组


如果键是相关的而不是索引(0、1、2等),则可以自由使用关联数组(可以通过键的唯一键从中提取值的集合)。

所有密钥必须具有相同的类型。


使用关联数组的第一条规则:所有键必须具有相同的类型(最常见的是string )。

 $goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // Bad (uses different types of keys) $badMap = [ 'foo' => 'bar', 1 => 'baz' ]; 

所有值都必须是同一类型。


值同样适用:它们必须是同一类型。

 $goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // Bad (uses different types of values) $badMap = [ 'foo' => 'bar', 'bar' => 1 ]; 

注释类型的常见样式是: @var array<TypeOfKy, TypeOfValue>

关联数组必须保持私有


列表由于其特性的简单性,可以安全地从一个对象转移到另一个对象。 即使列表为空,任何客户端都可以循环浏览元素或对其进行计数。 由于客户可以依赖与任何值都不匹配的键,因此使用Map更加困难。 这意味着关联数组通常应相对于管理它们的对象保持私有。 而不是让客户端直接访问内部映射,而是让getter(可能还有setter)检索值。 如果请求的键没有值,则引发异常。 但是,如果您可以将地图及其值完全设为私有,请执行此操作。

 // Exposing a list is fine /** * @return array<User> */ public function allUsers(): array { // ... } // Exposing a map may be troublesome /** * @return array<string, User> */ public function usersById(): array { // ... } // Instead, offer a method to retrieve a value by its key /** * @throws UserNotFound */ public function userById(string $id): User { // ... } 

将对象用作具有多种类型值的关联数组


如果要使用关联数组,但要在其中存储不同类型的值,请使用对象。 定义一个类,添加公共类型属性,或添加一个构造函数和获取方法。 这些对象包括配置或命令对象:

 final class SillyRegisterUserCommand { public string $username; public string $plainTextPassword; public bool $wantsToReceiveSpam; public int $answerToIAmNotARobotQuestion; } 

规则例外


库和框架有时需要更动态地使用数组。 这样就不可能(也不希望)遵循以前的规则。 示例包括存储在数据库表中的数据数组以及 Symfony中的表单配置

自定义集合类


自定义集合类可能是与IteratorArrayAccess和其他实体一起使用的好工具,但是我发现代码常常令人困惑。 初次看代码的任何人都必须查阅PHP手册,即使他是一位经验丰富的开发人员。 此外,您将不得不编写更多代码进行维护(测试,调试等)。 因此,在大多数情况下,具有正确类型注释的简单数组就足够了。

什么表明您需要将数组包装在自定义集合对象中?

  • 与数组有关的逻辑重复。
  • 客户必须处理有关数组内容的太多细节。

使用自定义集合类来防止重复逻辑。


如果使用同一阵列的多个客户端执行相同的任务(例如,过滤,比较,减少,计数),则可以使用自定义集合类删除重复项。 将重复的逻辑转移到集合类方法可以使任何客户端仅通过调用集合方法即可执行相同的任务:

 $names = [/* ... */]; // Found in several places: $shortNames = array_filter( $names, function (string $element): bool { return strlen($element) < 5; } ); // Turned into a custom collection class: use Assert\Assert; final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( array_filter( $this->names, function (string $element): bool { return strlen($element) < 5; } ) ); } } $names = new Names([/* ... */]); $shortNames = $names->shortNames(); 

使用方法转换集合的优点是,命名了此转换。 您可以为调用array_filter()添加一个简短的信息性名称,否则很难找到。

取消客户与自定义集合类的绑定


如果客户端遍历数组,从所选元素中获取部分数据并对它们执行某些操作,则该客户端将与所有相应类型紧密相关:数组,元素,检索到的值,选择器方法等。由于存在如此深的绑定,您在不破坏客户端的情况下更改与这些类型相关的任何内容将变得更加困难。 在这种情况下,您还可以将数组包装在自定义集合类中,并给出正确的答案,在内部执行必要的计算,然后放松客户对集合的绑定。

 $lines = []; $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } // Turned into a custom collection class: final class Lines { public function totalQuantity(): int { $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } return $sum; } } 

自定义集合类的一些规则


使它们不变


执行此类转换时,对集合实例的现有引用不应受到影响。 因此,任何执行此转换的方法都应返回该类的新实例,如上例所示:

 final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( /* ... */ ); } } 

当然,如果转换内部数组,则可以转换为其他类型的集合或简单数组。 与往常一样,请确保返回正确的类型。

只提供客户真正需要的行为


仅实现您真正需要的,而不是从具有通用集合的库中扩展类,或者实现通用过滤器或映射,以及为每个自定义集合类进行缩减,而不必实现。 如果在某个时候停止使用该方法,则将其删除。

使用IteratorAggregate和ArrayIterator进行迭代


如果您使用的是PHP,则不要实现Iterator接口的所有方法(保存内部指针等),而是仅实现IteratorAggregate接口,并使其基于内部数组返回ArrayIterator实例:

 final class Names implements IteratorAggregate { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function getIterator(): Iterator { return new ArrayIterator($this->names); } } $names = new Names([/* ... */]); foreach ($names as $name) { // ... } 

妥协


由于您正在为自定义集合类编写更多代码,因此客户端应该更轻松地使用此集合(而不仅仅是数组)。 如果客户端代码变得更加清晰,如果集合提供了有用的行为,那么这证明了维护自定义集合类的额外努力是合理的。 但是由于使用动态数组非常容易(主要是因为您不需要指定使用的类型),所以我很少使用集合类。 但是,一些开发人员正在积极使用它们,因此,我一定会继续寻找可能的用例。

Source: https://habr.com/ru/post/zh-CN484336/


All Articles