分类
php

如何优雅的传递参数

在我们的代码中,无外乎是数据和逻辑。但是很多代码很不好的地方就在于数据和逻辑结合地太过于紧密,这样就会导致代码的可维护性和可扩展性大大降低。所以,我们需要将数据封装起来,从逻辑中抽取出来。要做到这一点,有非常多的手段,其中一种就是使用 Bean 的类来抽离数据。

如何使用 Bean 来传参

我们经常会看到如下的代码,一个方法会接受很多的参数:

<?php

class Service {
 		public function doSomeTime(string $boo, int $foo, string $bzz, bool $baz, float $faz) {} 
}

甚至有时候参数多得要换行写。为了解决这个方法,很多人会使用聚合类的数据结构来包装这些变量,如改成如下形式:

<?php

class Service {
  	/**
     * @example doSomeTime(['boo' => '', 'foo' => '', 'bzz' => '', baz => '', 'faz' => ''])
     */
 		public static function doSomeThings(array $params) {} 
}

我们使用 $param 来接受传参一来是少了类型的校验、二来是代码缺少了 self-document ,需要额外的注释来说明。所以,如果我们可以使用使用一种名为 Bean 的类来封装代码如下:

<?php
class ParamsBean {
 	protected string $boo;
  protected int $foo;
  protected string $bzz;
  protected bool $baz;
  protected float $faz;
}

可以看出,我们的 Bean 类就是普通的类,只是这中类没有啥逻辑,是纯粹对数据的封装。一般我们还会给这些类中的属性写一些 getter 、 setter  的方法。如下示例:

<?php
class ParamsBean {
	protected string $boo;
  // getter
  public function getBoo(): string {
   	return $this->$boo; 
  }
  // setter
  public function setBoo(string $boo): void {
   	$this->$boo = $boo;
  }
}

其实就是这么简单,但是我们代码可以更加优雅。比如上面的例子:

<?php
$bean = new ParamsBean();
$bean->setBoo('boo');
Service::doSomeThings($bean);

这样我们就解决了上面所说的两个问题,首先是类型不能校验的问题,现在我们交给了 Bean 中的属性类型校验,并且通过 getter 和 setter 方法,我们对类型可以加强校验或对参数进一步加工。另外我们的代码本身就是 self-document 的,这也是面向对象编程带给我们的,代码的可读性会更好,封装性更好。如下图所示:

对于这个 Bean 中拥有那些属性,在 IDE 的帮助下一目了然。

对 Bean 进行封装

但是上面这样的 Bean 还不是很好用,比如我希望对 Bean 做更多的通用性地封装。下面的代码参考了 EasySwoole 框架中 Bean 的实现。比如我们可以传入一个数组,然后根据传入的数组为 Bean 中的属性进行赋值:

<?php
class Bean {
 		public function __construct (?array $data) {
        foreach ($data as $key => $value) {
            $this->$key = $value;
        }
    } 
}

再来,我们可以希望能够将 Bean 中的数据输出为数组, 为其添加 toArray 方法:

<?php
class Bean {
 		public function toArray (): array {
        $data = [];
        foreach ($this as $key => $value) {
            $data[$key] = $value;
        }
        return $data;
    } 
}

接着,我们希望当用户传入 Bean 中不存在的属性的时候能够自动的过滤掉。不然当执行到构造方法第 5 行的时候,会因为属性不存在而报错 $this->$key , 可是 $key 在 Bean 中可能会不存在。

要实现这点,首先我们要利用反射获取所有的 Bean 中的属性。然后在判断是否 $key 是否在属性中存在:

<?php
final public function allProperty (): array {
        $data = [];
        $clazz = new ReflectionClass($this);
  			// 利用反射获取类中所有 Public 和 Protected 修饰的属性
        $protectedAndPublicProps = $clazz->getProperties(
            ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED
        );
        foreach ($protectedAndPublicProps as $prop) {
            if ($prop->isStatic()) {		
                continue;		// 跳过静态属性,一般也不建议在 Bean 中写静态的属性,除非是个别的常量
            }
            array_push($data, $prop->getName());
        }
        return $data;
    }

最后,我们来实现一个过滤方法,在构造函数以及 toArray 方法都去过滤不存在的属性:

<?php
private function filterNotExistsProps (array $data): array {
		$props = $this->allProperty();
  	// 获取所有的属性,并过滤数据
		return array_filter($data, fn($key) => in_array($key, $props), ARRAY_FILTER_USE_KEY);
}

到这里,我们的 Bean 就比较好用了。但是相对 EasySwoole 中的实现还是简单了一些,也算是抛砖引玉吧。大家可以去看看这个框架中的实现,或者其他的框架也会有类似的实现。

对 Bean 的单元测试

最后,我们要给之前写的这个 Bean 类写一段单元测试,代码抄录如下:

<?php
class BeanTest extends TestCase {

    protected Bean $bean;

    protected function setUp () {
        parent::setUp();
      	// 利用匿名函数构造一个被测试的类,继承我们上面写的 Bean 类
        $this->bean = new class(['name' => 'bob', 'age' => 15, 'notExistsKey' => false]) extends Bean {
            protected string $name;
            protected int $age;
            protected static string $staticProp = 'static prop';

            public function getName () { return $this->name; }
        };
    }

    /**
     * 测试构造函数
     */
    public function testConstruct () {
        $this->assertEquals('bob', $this->bean->getName());
    }

    /**
     * 测试输出数组
     */
    public function testToArray () {
        $arr = $this->bean->toArray();
        $this->assertIsArray($arr);
        $this->assertArrayHasKey('name', $arr);
        $this->assertArrayHasKey('age', $arr);
        $this->assertArrayNotHasKey('notExistsKey', $arr);
        $this->assertEquals('bob', $arr['name']);
        $this->assertEquals(15, $arr['age']);
    }
}

总结

这篇文章一来是和大家分享,如何更加优雅地封装我们在代码中的数据。我很喜欢一个比喻,逻辑就是水管,而数据就是水管里的水。我们要引导水流,控制它的流向以及水量。

另外,还是想和大家分享这个原则,数据和逻辑分离

发表评论

邮箱地址不会被公开。 必填项已用*标注