对象设计风格指南概要
如何创建和使用对象
如何创建和使用对象
我最近在读一本很有意思的书,叫《对象设计风格指南》,作者是马蒂亚斯·诺巴克 (Matthias Noback),讲的是如何才能把对象设计得尽善尽美。所以我决定在这里跟大家分享一些我觉得比较有用的技巧和指导。当然,如果你想深入了解这个主题,我建议你通读全书。
1º 面向对象编程概念简介
本书中,继承的作用很小,尽管它被认为是面向对象编程的基石之一。实际上,使用继承往往会导致设计混乱。
本书主要在以下两种情况下使用继承:
- 在定义依赖项接口时(依赖注入和倒置)。
- 在定义对象层次结构时,例如定义继承自内置异常类的自定义异常时,我们通常会主动阻止开发者继承我们的类。您可以通过在类名前添加 `final` 关键字来实现这一点。稍后会对此进行更详细的解释。强烈建议使用组合而非继承。
关于单元测试的基本结构,这里留出一点空间:
每个测试方法的基本结构是 Arrange(准备)- Act(执行)- Assert(断言):
1. Arrange:将我们正在测试的对象(也称为 SUT,即被测对象)置于某个已知的状态。2
. Act:调用其某个方法。3
. Assert:对最终状态进行一些断言。
事情变得棘手了。物体分为两种类型:
- 服务对象要么执行任务,要么返回信息。这类对象只需创建一次,即可被多次使用,但其自身属性不可更改。它们的生命周期非常简单。一旦创建,它们就可以像执行特定任务的小型机器一样无限期地运行。这类服务是不可变的。服务对象是执行者,它们的名称通常表明其功能:渲染器、计算器、存储库、调度器等等。
- 这类对象用于存储数据,并可选择性地提供一些用于操作或检索这些数据的行为。第一类对象使用这类对象来完成其任务。这些对象是服务所使用的素材。它们有两种子类型:值对象和模型/实体,但我们暂且不深入探讨。
2º 关注服务
关于如何提供优质服务,有很多建议,我将对它们进行简要概括:
- 要创建服务,请使用依赖注入,以便在实例化后立即使用服务并进行测试替身。因此,依赖项需要显式声明。以下是一个示例,您可以看到参数 `$appConfig` 仅用于获取缓存目录……因此,请确保只注入服务实际需要的值,而不是注入整个配置对象。
interface Logger
{
public function log(string $message): void;
}
final class FileLogger implements Logger
{
private Formatter $formatter;
// Formatter is a dependency of FileLogger
public function __ construct(Formatter $formatter)
{
$this->formatter = $formatter;
}
public function log(string $message): void
{
formattedMessage = $this->formatter->format($message) ;
// ….
}
}
- 尽可能将所有相关的配置值放在一起。服务不应该注入整个全局配置对象,而应该只注入它需要的值。
错误的方法
final class MySQLTableGateway
{
public function __construct(
string $host,
int $port,
string $username,
string $password,
string $database,
string $table
) {
// ...
}
}
好方法
final class MySQLTableGateway
{
public function __construct(
ConnectionConfiguration $connectionConfiguration,
string $table
) {
// $table is the name of the table, It isn’t necessary to make the connection
}
}
- 避免使用服务定位器(可以从中检索其他服务的服务),并显式注入所需的依赖项。
- 所有构造函数参数都应该是必需的,否则代码会变得不必要地复杂。如果你确实想将其作为可选依赖项,可以使用空对象。
- 服务应该是不可变的,也就是说,一旦服务完全实例化,就不可能更改,因为其行为可能非常难以预测。
所以……请避免类似这样的情况:
final class EventDispatcher
{
private array $listeners = [];
public function addListener(
string $event,
callable $listener
): void {
$this->listeners[event][] = $listener;
}
}
- 仅当构造函数中出现验证错误时才分配属性或抛出异常。
final class FileLogger implements Logger
{
private string $logFilePath;
public function __construct(string $logFilePath)
{
// $logFilePath should be properly set up, so we just need a safety check
if (! is_writable($logFilePath)) {
throw new InvalidArgumentException(
'Log file path ’ . $logFilePath . ‘ should be writable’
);
}
$this->logFilePath = $logFilePath;
}
public function log(string message): void
{
// ...
}
}
- 理想情况下,应创建对象以避免隐藏依赖项,例如,使用 json_encode() 函数或 PHP 中的 DateTime 类。
错误的方法
final class ResponseFactory
{
public function createApiResponse(array $data): Response
{
// json_encode is a hidden dependency
return new Response(
json_encode(
$data,
JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), ['Content-Type' => 'application/json']
);
}
}
好方法
final class JsonEncoder
{
/ **
* throws RuntimeException
*/
public function encode(array $data): string
{
try {
return json_encode(
$data,
JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT
);
// we can throw our own exception, with more specific info
} catch (RuntimeException previous) {
throw new RuntimeException(
'Failed to encode data: ' . var_export($data, true),
0,
previous
);
}
}
}
final class ResponseFactory
{
private JsonEncoder $jsonEncoder;
// JsonEncoder can be injected as a dependency
public function __construct(JsonEncoder $jsonEncoder)
{
$this->jsonEncoder = $jsonEncoder;
}
public function createApiResponse(data): Response
{
return new Response($this->jsonEncoder->encode($data));
}
}
你可以对 date() 和语言中的大型核心实用程序执行相同的操作,这样你的应用程序层就会完全解耦。
3º 其他物体
3.1 值对象和模型/实体
主要建议如下:
- 在构造函数中验证对象,可以确保应用程序中只有有效对象,每个对象都符合预期。如果数据无效,应该在构造函数中抛出异常。本书建议避免使用自定义异常来处理无效参数异常,因为这类运行时异常表明……更多内容请参见下文。
final class Coordinates
{
public function _construct(float $latitude, float $longitude)
{
if ($latitude > 90 || $latitude < -90) {
throw new InvalidArgumentException(
'Latitude should be between -90 and 90'
);
}
$this->latitude = $latitude;
}
}
- 不要使用属性填充词,稍后我们会通过示例来了解它们在哪些情况下可以使用。
- 实体/模型应该具有唯一 ID,而值对象则不具备这种特性,因为它们只是封装了一个或多个原始类型的值。
- 为了给构造函数增加更多语义,出现了命名构造函数,这些是具有特定领域名称的静态方法,允许你的代码拥有比典型的 new class() 更好的名称。
标准方式
$salesOrder = new SalesOrder();
更好的方法
$salesOrder = SalesOrder::place();
你可以将 __construct 方法设为私有,以避免使用它,并在 place() 方法内部调用构造函数。
final class DecimalValue
{
private int value;
private int precision;
private function __construct(int $value, int $precision)
{
this.value = value;
Assertion.greaterOrEqualThan($precision, 0);
$this->precision = $precision;
}
public static function fromInt(
int $value,
int $precision
): DecimalValue {
return new DecimalValue($value, $precision);
}
public static function fromFloat(
float $value,
int $precision
): DecimalValue (
return new DecimalValue(
(int) round($value * pow(10, precision)),
$precision
):
}
}
- 值对象的优点之一是,如果在其构造函数中进行验证,则在看到值对象时,您就知道它包含已验证的信息,而无需在代码的其他位置验证此信息。
- 测试对象和构造函数的行为时,要模拟它们会失败的情况,不要仅仅为了检查值是否正确而创建测试。
public function it_can_be_constructed(): void
{
$coordinates = new Coordinates(60.0, 100.0);
assertEquals(60.0, $coordinates->latitude());
assertEquals(100.0, $coordinates->longitude());
}
- 总之,值对象不仅表示领域概念,它们还可以出现在应用程序的任何位置。值对象是一个不可变对象,它封装了基本类型的值。
3.2 DTO(数据传输对象)
3.1 节的规则并不适用于这种类型的对象,即数据目标对象 (DTO)。在值对象和模型中,我们追求的是数据的一致性和有效性,而在 DTO 中,我们只需要(顾名思义)将数据从一个点传递到另一个点。
- 可以使用常规构造函数创建 DTO。
- 它的属性可以逐一设置。
- 它的所有属性都已公开,因此请将其设为公开,并直接访问它们,无需使用 getter 方法。
- 它的属性只包含原始类型的值。
- 属性可以选择性地包含其他 DTO 或简单的 DTO 数组。
- 必要时可以使用属性填充符。
final class ScheduleMeetup
{
public string $title;
public string $date;
public static function fromFormData(
array $formData
): ScheduleMeetup {
$scheduleMeetup = new ScheduleMeetup();
$scheduleMeetup->title = $formData['title'];
$scheduleMeetup->date = $formData['date'];
return $scheduleMeetup;
}
}
对象包含方法,方法中包含行为。方法分为两种:查询方法用于检索信息,命令方法用于执行任务。但两者都可以使用相同的“模板”进行设计,即:
1. 检查参数,如有错误则抛出错误。2
. 执行方法需要执行的操作,必要时抛出错误。3
. 检查后置条件。如果测试完善,则无需进行此步骤,但例如,在处理遗留代码时,进行后置条件检查对于安全检查非常有用。4
. 如果是查询方法,则返回结果。
您已经看到异常处理是代码的重要组成部分,在某些情况下使用自定义异常非常有用:
1. 如果您想捕获更高层级的特定异常类型。
try {
// possibly throws ‘SomeSpecific’ exception
} catch (SomeSpecific $exception) {
// …
}
2º 如果存在多种方法来实例化同一种类型的异常
final class CouldNotDeliverOrder extends RuntimeException
{
public static function itWasAlreadyDelivered(): CouldNotDeliverOrder
{
// ...
}
public static function insufficientQuantitiesInStock(): CouldNotDeliverOrder
{
//...
}
}
3. 如果你想使用命名构造函数来实例化异常
final class CouldNotFindProduct extends RuntimeException
{
public static function withId(
ProductId $productld
): CouldNotFindProduct (
return new CouldNotFindProduct('Could not find a product with ID’ . $productld);
}
}
throw CouldNotFindProduct .withId(/* ... */);
而且,您不必在异常类的名称中放入“Exception”,而是使用像 InvalidEmailAddress 或 CouldNotFindProduct 这样的明确名称。
好了,就这些了,书中还有很多例子,所以我强烈建议你们去看看。如果你们想要本书的第二部分,请在评论区告诉我。
来源及更多信息
文章来源:https://dev.to/migueldevelopez/object-design-style-guide-summary-42bl