DRY 和打字专业化

DRY and Typing Specialization

在学习的过程中PHP,我想到了一个(简单的?)问题,我无法“妥善”解决。 这是:

例如:

<?php

interface BagInterface
{

    public function has(string $key) : bool;
    public function get(string $key, mixed $fallback) : mixed;
    public function set(string $key, mixed $value) : self;
    public function del(string $key) : void;
    public function all() : array;
    public function filter(callable $callback) : array;

}

abstract class AbstractBag implements BagInterface
{

    private array $bag;

    public function has(string $key) : bool
    {
        return array_key_exists($key, $this->bag);
    }

    public function get(string $key, mixed $fallback = null) : mixed
    {
        return $this->has($key) ? $this->bag[$key] : $fallback;
    }

    public function set(string $key, mixed $value) : self
    {
        $this->bag[$key] = $value;

        return $this;
    }

    public function del(string $key) : void
    {
        unset($this->bag[$key]);
    }

    public function all() : array
    {
        return $this->bag;
    }

    public function filter(callable $callback) : array
    {
        return array_filter($this->bag, $callback, ARRAY_FILTER_USE_BOTH);
    }

}

所以,我可以创建“专用”包:

<?php

class CookieBag extends AbstractBag
{

    public function get(string $key, ?Cookie $fallback = null) : ?Cookie
    {
        return parent::get($key, $fallback);
    }

    public function set(string $key, Cookie $cookie) : self
    {
        return parent::set($key, $cookie);
    }

}

class CandyBag extends AbstractBag
{

    public function get(string $key, ?Candy $fallback = null) : ?Candy
    {
        return parent::get($key, $fallback);
    }

    public function set(string $key, Candy $candy) : self
    {
        return parent::set($key, $candy);
    }

}

我知道这在 PHP 中是不可能的,因为它违反了 Liskov 替换原则。

例如:

<?php

class GrandMa
{
    public function giveCookie(BagInterface $bag)
    {
        // Will be fine, BagInterface said "mixed"
        // But break LSP, error if $bag is a not a CookieBag
        bag->set('abc', new Cookie());
    }
}

所以,我阅读了多个关于同一个“问题”的 post,其中 none 提供了一个明确的解决方案,很少有人提到观察者模式,但我真的不知道如何应用它。 也许我对 C++ 模板方法太累/蒙蔽了双眼...

有没有人有任何建议、示例或更好的方法?

谢谢!

是的,像这样专门化集合通常是 "generic" or "templated" types 有用性的例子。与其 扩展 基础 class,不如 用类型参数专门化 它,给出如下内容:

class GenericBag<T>
{
    // ...

    public function get(string $key, ?T $fallback = null) : T
    {
        return $this->has($key) ? $this->bag[$key] : $fallback;
    }

    public function set(string $key, T $value) : self
    {
        $this->bag[$key] = $value;

        return $this;
    }
    
    // ...
}

class GrandMa
{
    public function giveCookie(GenericBag<Cookie> $bag)
    {
        bag->set('abc', new Cookie());
    }
}

不幸的是,这些在 PHP 中不存在,并且不太可能很快出现,因为它们如何适应现有语言存在一些基本问题。

在此期间你能做的最好的事情是使用一些机器可读的文档,这些文档可以被各种静态分析工具和 IDE 读取。例如这里是 Psalm's documentation for it;许多其他工具都支持相同的语法。

所以上面的例子是:

/** @template T */
class GenericBag
{
    // ...

    /**
     * @param string $key
     * @param T|null $fallback
     * @return T
     */
    public function get(string $key, $fallback = null)
    {
        return $this->has($key) ? $this->bag[$key] : $fallback;
    }

   /**
     * @param string $key
     * @param T $value
     * @return GenericBag<T>
     */
    public function set(string $key, $value) : self
    {
        $this->bag[$key] = $value;

        return $this;
    }
    
    // ...
}

class GrandMa
{
    /**
      * @param GenericBag<Cookie> $bag
      */
    public function giveCookie(GenericBag $bag)
    {
        bag->set('abc', new Cookie());
    }
}

@IMSoP 谢谢你的回答!所以现在,我知道在 PHP 中没有“template-like”方法来实现这一点。您的解决方案看起来很好,可以避免 code-duplication,但我不喜欢依赖“add-ons”的想法。

所以,我最终得到了以下“解决方案”,这似乎更“安全”(至少对我而言)。

<?php

interface BagInterface
{

    public function has(string $key) : bool;
    public function get(string $key, mixed $fallback) : mixed;
    public function set(string $key, mixed $value) : self;
    public function delete(string $key) : self;
    
    // ... Rest of Generic Functions Declarations 

}

class Bag implements BagInterface
{

    private array $items = [];

    public function has(string $key) : bool
    {
        return \array_key_exists($key, $this->items);
    }

    public function get(string $key, mixed $fallback = null) : mixed
    {
        return $this->has($key) ? $this->items[$key] : $fallback;
    }

    public function set(string $key, mixed $value) : self
    {
        $this->items[$key] = $value;

        return $this;
    }

    public function delete(string $key) : self
    {
        unset($this->items[$key]);

        return $this;
    }

    // ... Rest of Generic Functions Definitions
    
}

然后,为了“专业化”,我使用了“适配器模式?” (我试着加了点逻辑让它更“真实”)。

<?php

interface CookieBagInterface
{

    public function has(string $name) : bool;
    public function get(string $name) : ?CookieInterface;
    // Changed set(...) to add(...), because I do not need $key
    public function add(CookieInterface $cookie) : self;
    public function delete(string $name) : self;

    // ... Rest of Specialized Functions Declarations

}

class CookieBag implements CookieBagInterface
{

    public function __construct(
        private BagInterface $bag,
    )
    {
        // Do nothing
    }

    public function has(string $name) : bool
    {
        return $this->bag->has($name);
    }

    public function get(string $name) : ?CookieInterface
    {
        return $this->bag->get($name, null);
    }

    public function add(CookieInterface $cookie) : self
    {
        $added = \setcookie($cookie->getName(), $cookie->getValue(), [
            'expires'  => $cookie->getExpires(),
            'path'     => $cookie->getPath(),
            'domain'   => $cookie->getDomain(),
            'secure'   => $cookie->isSecure(),
            'httponly' => $cookie->isHttpOnly(),
            'samesite' => $cookie->getSameSitePolicy(),
        ]);

        if ($added)
        {
            $this->bag->set($cookie->getName(), $cookie);
        }

        return $this;
    }

    public function delete(string $name) : self
    {
        $cookie = $this->get($name);

        if ($cookie === null)
        {
            return $this;
        }

        $removed = \setcookie($cookie->getName(), '', 1);

        if ($removed)
        {
            $this->bag->delete($name);
        }

        return $this;
    }

    // ... Rest of Specialized Functions Definitions

}

好处:

  • 有效
  • 我可以修改方法(例如:Bag::set() / CookieBag::add(),不需要$key)
  • 我可以添加“逻辑”吗?方法(例如:CookieBag::add() 和 CookieBag::delete() 使用 \setcookie())

缺点:

  • 我必须re-declare(接口)和re-define(class)每个都需要
  • 我必须将我的“通用”包传递给构造函数(或者我应该打破依赖倒置原则并向构造函数添加 $bag = new Bag() 吗?)

谢谢!