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()
吗?)
谢谢!
在学习的过程中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()
吗?)
谢谢!