← 返回蜂巢洞察

构建者设计模式:构建复杂对象的一种更有效的方法

有些对象非常简单,比如字符串、数字或布尔值。你可以用一行代码创建它们,然后继续进行其他操作。 而另一些对象则完全不简单。例如,一个轮播组件就需要包含项目数量、项目生成函数、控制器、高度、视图窗口比例、自动播放设置、页面切换回调函数以及无限滚动配置等信息。再比如,一个HTTP请求需要URL地址、请求头信息、认证令牌、请求体内容、超时时间以及重试逻辑。又或者,一条通知消息需要标题、正文、图标、发送渠道、优先级、声音效果、振动功能以及操作按钮等等。 当你需要构建这类对象时,一种常见的方法是使用带有大量参数的构造函数。这种方法虽然可行,但随着对象结构变得越来越复杂,就会带来一系列问题:这些参数很难区分

有些对象非常简单,比如字符串、数字或布尔值。你可以用一行代码创建它们,然后继续进行其他操作。

而另一些对象则完全不简单。例如,一个轮播组件就需要包含项目数量、项目生成函数、控制器、高度、视图窗口比例、自动播放设置、页面切换回调函数以及无限滚动配置等信息。再比如,一个HTTP请求需要URL地址、请求头信息、认证令牌、请求体内容、超时时间以及重试逻辑。又或者,一条通知消息需要标题、正文、图标、发送渠道、优先级、声音效果、振动功能以及操作按钮等等。

当你需要构建这类对象时,一种常见的方法是使用带有大量参数的构造函数。这种方法虽然可行,但随着对象结构变得越来越复杂,就会带来一系列问题:这些参数很难区分;对于可选参数,需要在代码中到处进行空值检查;参数的顺序也很重要,很容易出错;而且使用构造函数创建对象时,会生成一大串参数值,让人根本不想去阅读或维护这些代码。

构建者设计模式解决了这些问题。它将复杂对象的构建过程与其表示方式分离开来,使得同样的构建流程能够通过一个清晰、循序渐进的接口来生成不同的配置结果。

目录

先决条件

在阅读本文之前,你应当已经掌握以下内容:

  • 面向对象编程:类、构造函数和方法

  • 设计模式的概念及其作用

  • Dart或C#的基本语法

你不需要事先具备设计模式的相关经验。本文会从基础原理开始介绍构建者模式。

什么是构建者模式?

构建者模式是一种创建型设计模式。创建型模式主要关注对象的创建方式,而构建者模式专门用于处理那些需要经过多个配置步骤才能完成的复杂对象的构建过程。

这种模式将那些在简单代码中常常被混为一谈的两个问题区分开来:对象本身是什么,以及它是如何被构建的。对象负责存储自己的数据并执行相应的操作;而建造者则负责处理构建逻辑,并逐步完成各项配置步骤,最终生成完整的对象。

这样一来,构建过程就更像是对所要创建的对象的描述,而不是只是一系列需要传递给构造函数的值而已。

它解决的问题

如果没有使用建造者模式,构建一个复杂的组件会是什么样子呢?以下是一个示例:

// 直接编写代码来构建轮播组件——这种写法难以阅读,也容易出错
CarouselSlider.builder(
  options: CarouselOptions(
    height: 200,
    viewportFraction: 0.97,
    enableInfiniteScroll: false,
    autoPlayCurve: Curves.easeIn,
    enlargeCenterPage: true,
    pauseAutoPlayOnManualNavigate: true,
    onPageChanged: onPageChanged,
    autoPlay: false,
  ),
  itemBuilder: (context, index, realIndex) => AdCard(ad: ads[index]),
  itemCount: ads.length,
)

这种写法虽然可以正常工作,但当你需要在同一屏幕上创建两个不同的轮播组件时,问题就出现了:一个用于展示广告,另一个用于显示账户余额。这两个组件的高度、视图窗口的比例、项目构建方式以及项目数量都需要有所不同。如果你直接复制整个构建代码块并修改相关数值,那么最终得到的两个配置文件在外观上看可能相似,但实际上却存在细微的差别。

当出现新的需求时,比如只需要为广告轮播组件添加自动播放功能,而不要对账户余额轮播组件进行同样的修改,你就必须仔细查找相关的代码片段,并确保自己修改的是正确的部分。

更根本的问题在于,构建逻辑分散在代码的各个地方。每当有地方需要创建轮播组件时,人们都需要了解所有的构建细节;而这些知识并没有被集中保存在一个固定的位置。

建造者模式将所有与构建相关的信息集中到一起,并通过一个简洁明了的接口提供给使用者。

核心组成部分

建造者模式由三个主要组成部分构成。

产品

这就是需要被构建的复杂对象。这个对象本身并不知道建造者的存在;它只负责保存自己的配置信息,并根据这些配置来执行相应的操作。通常,产品的创建是通过一个私有的构造函数来完成的,因此只有它的建造者才能真正创建它。

建造者

建造者这个类负责逐步完成各项配置工作,并最终生成产品对象。建造者提供的每个方法都会对产品的某个方面进行配置,但方法的返回值仍然是建造者本身。正是这种返回机制使得方法链式调用成为可能;最后,建造者中的最后一个方法会生成完整的产品对象。

指导者(可选组件)

指导者这个类知道如何使用建造者来创建特定类型、已经预先配置好的产品对象。它将那些关于如何构建常见配置的信息封装起来,这样调用者就不需要了解具体的实现细节了。在实际开发中,工厂模式常常扮演这样的角色。

方法链:流畅接口技术

方法链是一种能够让构建器代码读起来更加自然的技术。每个构建器方法都会返回 `this`(即构建器本身),因此可以在同一行或下一行立即调用下一个方法。

// 不使用方法链
final builder = RequestBuilder();
builder.setUrl('https://api.example.com/users');
builder.setMethod('POST');
builder.addHeader('Authorization', 'Bearer $token');
builder.setBody({'name': 'John'));
final request = builder.build();

// 使用方法链
final request = RequestBuilder()
    .setUrl('https://api.example.com/users')
    .setMethod('POST')
    .addHeader('Authorization', 'Bearer $token')
    .setBody({'name': 'John'})
    .build();

这两种写法最终产生的结果是完全相同的。使用方法链的代码读起来就像是在描述一个请求过程,而不使用方法链的代码则只是一系列命令式的语句。

方法链有时也被称为“流畅接口”。这个名称来源于代码的阅读体验:它能够像自然语言一样,从左到右、从上到下流畅地被阅读。

实际应用示例一:Flutter轮播图构建器

这是来自一个Flutter金融科技应用的真实开发案例。该应用程序需要在仪表板上显示两个不同的轮播图:一个用于展示促销广告,另一个用于显示账户余额信息。这两个轮播图的配置各不相同,但它们都采用了相同的底层构建机制。

配置对象

import 'packageflutter/widgets.dart';
import 'package:equatable/equatable.dart';
import 'package:carouselslider/carousel_slider.dart';

class CarouselArgs extends Equatable {
  final CarouselSliderController? carouselController;
  final int itemCount;
  final Widget Function(BuildContext, int, int) itemBuilder;
  final CarouselOptions options;

  const CarouselArgs({
    this.carouselController,
    required this.itemCount,
    required this.itemBuilder,
    required this.options,
  });

  @override
  List get props =>> [
        carouselController,
        itemCount,
        itemBuilder,
        options,
      ];
}

class CarouselOptions extends Equatable {
  final double? height;
  final double? viewPortFraction;
  final bool? enableInfiniteScroll;
  final bool? enlargeCenterPage;
  final bool? pauseAutoPlayOnManualNavigate;
  final bool? autoplay;
  final Curve? autoplayCurve;
  final void Function(int, CarouselPageChangedReason)? onPageChanged;

  const CarouselOptions({
    this.height,
    this.viewPortFraction,
    this.enableInfiniteScroll,
    this.enlargeCenterPage,
    this.pauseAutoPlayOnManualNavigate,
    this.autoplay,
    this_autoplayCurve,
    this.onPageChanged,
  });

  @override
  List get props =>> [
        height,
        viewPortFraction,
        enableInfiniteScroll,
        enlargeCenterPage,
        pauseAutoPlayOnManual Navigate,
        autoplay,
        autoplayCurve,
        onPageChanged,
      ];
}

CarouselArgsCarouselOptions是配置对象。它们包含了构建轮播图所需的所有数据。这些只是简单的数据容器,并不具备任何自身的构建逻辑。

产品介绍

import 'package:flutter/material.dart';
import 'package:carouselslider/carousel_slider.dart' as n;

class CustomCarousel extends StatelessWidget {
  final CarouselArgs dto;

  // 私有构造函数,只有Builder才能创建这个组件
  CustomCarousel._builder(CustomCarouselBuilder builder)
      : dto = builder._dto!;

  @override
  Widget build(BuildContext context) {
    return n.CarouselSlider.builder(
      options: n.CarouselOptions(
        height: dto.options.height,
        viewportFraction: dto.options.viewPortFraction!,
        enableInfiniteScroll: dto.options.enableInfiniteScroll!,
        enlargeCenterPage: dto.options.enlargeCenterPage,
        onPageChanged: dto.options.onPageChanged,
        autoPlayCurve: dto.options.autoplayCurve!,
        autoPlay: dto.options_autoplay!,
      ),
      itemBuilder: dto.itemBuilder,
      itemCount: dto.itemCount,
    );
  }
}

CustomCarousel就是这个产品。它的构造函数是私有的,即CustomCarousel._builder。这种使用下划线前缀的设计以及命名的构造函数确保了外部代码无法直接创建CustomCarousel》实例。唯一创建它的方法是通过Builder

这种设计是故意为之的。这样就能保证所有的轮播图构建过程都必须通过Builder来完成,在这个过程中,配置信息会经过验证,并且能够以统一的方式被组装起来。

Builder的作用

class CustomCarouselBuilder {
  BuildContext? _context;
  CarouselArgs? _dto;

  CustomCarouselBuilder setArgs(BuildContext context, CarouselArgs value) {
    _context = context;
    _dto = value;
    return this;
  }

  Widget get buildCarousel =>
      CustomCarousel._builder(this).build(_context!);
}

CustomCarouselBuilder就是这个“Builder”。它通过setArgs方法来接收BuildContextCarouselArgs参数。由于setArgs方法返回的是this,因此可以连续调用该方法来进行配置的逐步设置。

buildCarousel是最终步骤。它会调用CustomCarousel的私有构造函数,并将自己作为参数传递给该构造函数;之后再调用build方法来生成最终的组件。只有当上下文信息和配置参数都准备齐全后,才能开始构建轮播图。

Builder的应用示例:一个使用Builder的工厂模式

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:carouselslider/carousel_slider.dart' as n;

class CarouselWidgetFactory {
  static CustomCarouselBuilder showCarouselAds(
    BuildContext context, {
    void Function(int, n.CarouselPageChangedReason)? onPageChanged,
  }) =>
      CustomCarouselBuilder().setArgs(
        context,
        CarouselArgs(
          itemCount: context.read().dashboardAds.length,
          itemBuilder: (context, index, realIndex) => DashboardAdsList(
            dto: context.read().dashboardAds[index],
          ),
          options: CarouselOptions(
            height: 200,
            viewPortFraction: 0.97,
            enableInfiniteScroll: false,
            autoplayCurve: Curves.easeIn,
            enlargeCenterPage: true,
            pauseAutoPlayOnManualNavigate: true,
            onPageChanged: onPageChanged,
            autoplay: false,
          ),
        ),
      );

  static CustomCarouselBuilder showCarouselAccountBalance(
    BuildContext context, {
    void Function(int, n.CarouselPageChangedReason)? onPageChanged,
  }) =>
      CustomCarouselBuilder().setArgs(
        context,
        CarouselArgs(
          itemCount: context.read().allBalances.length,
          itemBuilder: (context, index, realIndex) => BalanceList(
            dto: context.read().allBalances[index],
          ),
          options: CarouselOptions(
            height: 230,
            viewPortFraction: 1,
            enableInfiniteScroll: false,
            autoplayCurve: Curves.decelerate,
            enlargeCenterPage: true,
            pauseAutoPlayOnManualNavigate: true,
            onPageChanged: onPageChanged,
            autoplay: false,
          ),
        ),
      );
}

CarouselWidgetFactory就相当于“导演”角色。它清楚地知道该如何为每种特定的轮播组件配置相应的构建器。关于信息面板广告轮播组件的具体样式(高度为200像素,视口比例为0.97,过渡曲线为easeIn类型),这些信息都集中存储在一个地方;而关于账户余额轮播组件的具体样式(高度为230像素,视口比例为1,过渡曲线为decelerate类型),相关数据也同样保存在同一个地方。

如果开发者需要添加新的轮播组件类型,他们只需向CarouselWidgetFactory添加一个静态方法即可。他们无需了解CustomCarouselBuilderCustomCarousel的内部实现机制,只需利用这个工厂类来描述自己所需的功能即可。

使用方法

class DashboardPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 工厂类会生成正确的构建器配置,
        // 然后通过构建器生成最终的组件。
        CarouselWidgetFactory.showCarouselAds(context).buildCarousel,
        const SizedBox(height: 16),
        CarouselWidgetFactory.showCarouselAccountBalance(context).buildCarousel,
      ],
    );
  }
}

这里只需要两行代码就能创建两个轮播组件,而调用这些代码的开发者完全不需要了解任何关于轮播组件配置的细节——他们既不知道视口比例的具体数值,也不了解自动播放曲线的类型,更不了解各个组件的构建机制。他们只需调用工厂类,工厂类会使用相应的构建器来生成最终的结果。每一层代码都只负责完成自己需要完成的任务而已。

现实世界中的第二个例子:HTTP请求构建器

第一个例子展示了如何使用构建器来创建UI组件,而这个第二个例子则说明了如何利用构建器来生成非UI对象,比如HTTP请求。这种设计模式在数据层和API客户端中非常常见。

最终产物

class ApiRequest {
  final String url;
  final String method;
  final Map headers;
  final Map? body;
  final Duration timeout;
  final int maxRetries;

  // 私有构造函数——只有构建器才能创建ApiRequest对象
  ApiRequest._({
    required this.url,
    required this.method,
    required this.headers,
    this.body,
    required this.timeout,
    required this.maxRetries,
  });
}

ApiRequest类包含了生成HTTP请求所需的所有信息。它的私有构造函数确保了这个对象总是通过构建器来创建的,在创建过程中会应用默认值并进行验证。

构建器

class ApiRequestBuilder {
  String? _url;
  String _method = 'GET';
  final Map _headers = {};
  Map? _body;
  Duration _timeout = const Duration(seconds: 30);
  int _maxRetries = 0;

  ApiRequestBuilder url(String url) {
    _url = url;
    return this;
  }

  ApiRequestBuilder method(String method) {
    _method = method;
    return this;
  }

  ApiRequestBuilder header(String key, String value) {
    _headers[key] = value;
    return this;
  }

  ApiRequestBuilder bearerToken(String token) {
    _headers['Authorization'] = 'Bearer $token';
    return this;
  }

  ApiRequestBuilder contentType(String type) {
    _headers['Content-Type'] = type;
    return this;
  }

  ApiRequestBuilder body(Map body) {
    _body = body;
    return this;
  }

  ApiRequestBuilder timeout(Duration timeout) {
    _timeout = timeout;
    return this;
  }

  ApiRequestBuilder withRetries(int maxRetries) {
    _maxRetries = maxRetries;
    return this;
  }

  ApiRequest build() {
    if (_url == null || _url!.isEmpty) {
      throw ArgumentError('生成ApiRequest对象时必须提供URL地址');
    }

    return ApiRequest._(
      url: _url!,
      method: _method,
      headers: Map.unmodifiable(_headers),
      body: _body,
      timeout: _timeout,
      maxRetries: _maxRetries,
    );
  }
}
ApiRequestBuilder上的每个方法都会设置一个配置参数,然后返回`this`。而`build()`方法是整个流程的最终步骤:它会检查是否缺少必要的字段,并构建出一个不可变的`ApiRequest`对象。

需要注意的是,`_method`、`_timeout`和`_maxRetries`这些属性都配备了合理的默认值。除非用户有意覆盖这些默认值,否则无需手动指定它们。这正是Builder模式相比构造函数的一大优势:可选配置确实是可选项,不存在需要进行空值检查或使用默认参数之类的麻烦。

使用方法

// 一个标准的、经过身份验证的POST请求
final createUserRequest = ApiRequestBuilder()
    .url('https://api.example.com/users')
    .method('POST')
    .bearerToken(authToken)
    .contentType('application/json')
    .body({'name': 'Oluwaseyi', 'email': 'seyi@example.com'})
    .timeout(const Duration(seconds: 15))
    .build();

// 一个带有重试机制的GET请求
final getUserRequest = ApiRequestBuilder()
    .url('https://api.example.com/users/$userId')
    .bearerToken(authToken)
    .withRetries(3)
    .build();

// 一个为第三方服务设置自定义头的请求
final webhookRequest = ApiRequestBuilder()
    .url('https://webhook.example.com/events')
    .method('POST')
    .header('X-API-Key', apiKey)
    .header('X-Webhook-Secret', webhookSecret)
    .contentType('application/json')
    .body(eventPayload)
    .timeout(const Duration(seconds: 5))
    .build();

每条请求的描述都非常清晰:包括URL、请求方法、认证方式、请求体以及超时设置。只要阅读这些信息,就能立刻了解这条请求的具体类型及其包含的内容。

相比之下,直接使用构造函数来创建请求对象就会显得很不方便:

// 不使用Builder模式——代码可读性差,参数顺序也很重要
final request = ApiRequest._(
  url: 'https://api.example.com/users',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer $authToken',
    'Content-Type': 'application/json',
  },
  body: {'name': 'Oluwaseyi', 'email': 'seyi@example.com'},
  timeout: const Duration(seconds: 15),
  maxRetries: 0,
);

使用构造函数时,你必须清楚所有参数及其排列顺序;而Builder模式则允许你只指定所需的配置项,其代码结构更像文档说明。

C#中的构建器模式

C#中也同样采用了这种构建器模式,这进一步证明了这是一种通用的设计原则,并非Dart语言所特有的技术。由于C#支持方法链机制,因此在实现构建器模式时,它的表达能力尤为强大。

C#中的HTTP请求构建器

public class ApiRequest { public string Url { get; } public string Method { get; } public Dictionary Headers { get; } public object? Body { get; } public TimeSpan Timeout { get; } public int MaxRetries { get; } // 私有构造函数 private ApiRequest( string url, string method, Dictionary headers, object? body, TimeSpan timeout, int maxRetries) { Url = url; Method = method; Headers = headers; Body = body; Timeout = timeout; MaxRetries = maxRetries; } public static ApiRequestBuilder Create() => new ApiRequestBuilder(); } public class ApiRequestBuilder { private string? _url; private string _method = "GET"; private readonly Dictionary _headers = new(); private object? _body; private TimeSpan _timeout = TimeSpan(seconds(30)); private int _maxRetries = 0; public ApiRequestBuilder Url(string url) { _url = url; return this; } public ApiRequestBuilder Method(string method) { _method = method; return this; } public ApiRequestBuilder Header(string key, string value) { _headers[key] = value; return this; } public ApiRequestBuilder BearerToken(string token) { _headers["Authorization"] = $"Bearer {token}"; return this; } public ApiRequestBuilder ContentType(string contentType) { _headers["Content-Type"] = contentType; return this; } public ApiRequestBuilder Body(object body) { _body = body; return this; } public ApiRequestBuilder Timeout(TimeSpan timeout) { _timeout = timeout; return this; } public ApiRequestBuilder WithRetries(int maxRetries) { _maxRetries = maxRetries; return this; } public ApiRequest Build() { if (string.IsNullOrEmpty(_url)) throw new ArgumentException("需要提供URL地址"); return new ApiRequest( _url, _method, new Dictionary(_headers), _body, _timeout, _maxRetries ); } }

在C#中使用它

// 经过身份验证的POST请求
var createUserRequest = ApiRequest.Create()
    .Url("https://api.example.com/users")
    .Method("POST")
    .BearerToken(authToken)
    .ContentType("application/json")
    .Body(new { name = "Oluwaseyi", email = "seyi@example.com" })
    .Timeout(TimeSpan(seconds(15))
    .Build();

// 带有重试逻辑的GET请求
var getUserRequest = ApiRequest.Create()
    .Url($"https://api.example.com/users/{userId}")
    .BearerToken(authToken)
    .WithRetries(3)
    .Build();

这种编写模式是完全相同的。方法名称遵循C#的约定,都是大写的。`Build()`方法是整个请求处理流程的最后一步。同时,也强制使用了私有构造函数。其代码结构与Dart语言中的对应实现完全一致。

C#(ASP.NET)中的UI构建器

在.NET中,构建模式也被广泛用于后端系统中创建复杂的对象。下面是一个用于生成通知信息的示例:

public class Notification
{
    public string Title { get; }
    public string Body { get; }
    public string? ImageUrl { get; }
    public NotificationPriority Priority { get; }
    public Dictionary Data { get; }
    public bool Silent { get; }

    private Notification(
        string title,
        string body,
        string? imageUrl,
        NotificationPriority priority,
        Dictionary data,
        bool silent)
    {
        Title = title;
        Body = body;
        ImageUrl = imageUrl;
        Priority = priority;
        Data = data;
        Silent = silent;
    }

    public static NotificationBuilder Builder(string title, string body)
        => new NotificationBuilder(title, body);
}

public class NotificationBuilder
{
    private readonly string _title;
    private readonly string _body;
    private string? _ImageUrl;
    private NotificationPriority _priority = NotificationPriority.Default;
    private readonly Dictionary _data = new();
    private bool _silent = false;

    internal NotificationBuilder(string title, string body)
    {
        _title = title;
        _body = body;
    }

    public NotificationBuilder WithImage(string imageUrl)
    {
        _ImageUrl = imageUrl;
        return this;
    }

    public NotificationBuilder WithPriority(NotificationPriority priority)
    {
        _priority = priority;
        return this;
    }

    public NotificationBuilder WithData(string key, string value)
    {
        _data[key] = value;
        return this;
    }

    public NotificationBuilder AsSilent()
    {
        _silent = true;
        return this;
    }

    public Notification Build() => new Notification(
        _title,
        _body,
        _ImageUrl,
        _priority,
        new Dictionary(_data),
        _silent
    );
}

// 使用示例
var notification = Notification
    .Builder("新交易", "您收到了50,000奈拉")
    .WithPriority(NotificationPriority.High)
    .WithData("transaction_id", "txn_001")
    .WithData("type", "credit")
    .Build();

var silentNotification = Notification
    .Builder("后台同步", "")
    .AsSilent()
    .WithData("sync_type", "full")
    .Build();

构建器、构造函数与工厂模式

要了解何时该使用构建器,而非构造函数或工厂方法,就必须明白它们各自能解决什么问题。

当一个对象的结构足够简单,其所有参数都能一目了然,并且可选配置项也很少时,构造函数才是合适的选择。例如,一个仅包含id、name和email字段的用户对象,就不需要使用构建器。

当你需要控制创建的对象类型,或者对象的创建过程需要根据某些逻辑来决定具体使用哪种类型时,工厂方法就是最佳选择。比如Repository.create()方法会根据当前环境返回SqlRepository或HiveRepository中的某一种。

当一个对象具有许多可选配置参数,其创建过程需要多个步骤完成,或者你希望确保在所有必要参数都准备好之后才进行对象的创建,从而避免生成无效对象时,构建器才是合适的选择。此外,当你希望构建代码结构清晰、易于理解时,构建器也是个不错的选择。

在carousel示例中,我们有意同时使用了构建器和工厂模式。工厂方法提供了一些预先配置好的接口方法(如showCarouselAds、showCarouselAccountBalance),而构建器则负责完成这些复杂配置的逐步构建过程。这两种模式各司其职,共同完成了任务。

何时使用构建器模式

构建器模式有着许多适用的场景。

当需要创建的对象包含许多参数,尤其是许多可选参数时,就应该使用构建器模式。那些带有十个可选参数的构造函数往往难以阅读,也容易被错误配置。

此外,当对象的创建过程需要多个步骤,并且这些步骤在对象生成之前必须经过验证时,构建器也是一个很好的选择。构建器可以确保在调用build()方法之前,所有必要的参数都已经存在。

如果你希望构建代码本身就能清晰地说明其功能,那么构建器模式也是个不错的选择。使用描述性强的方法名进行链式调用,可以让代码像文档一样易于理解。读者无需了解内部实现细节,就能明白这段代码的作用。

当你需要同一个对象有多种不同的表现形式时,构建器也非常有用。通过改变调用的方法,同一个构建器可以用来生成测试请求、预发布请求或生产环境请求,而无需修改Request类本身。 最后,当你想防止生成无效对象时,构建器模式也能发挥重要作用。将产品的构造函数设为私有,并在构建器的build()方法中加入验证逻辑,就能确保绝对不会生成无效对象。

何时不应使用它

当一个对象的结构很简单,其构造函数也已经非常清晰易懂时,就应避免使用构建器模式。对于那些只需要两个必要参数的类来说,添加构建器只会增加复杂性,而并不会带来任何实际价值。

当不变性不是需要考虑的因素,且对象可以在创建后通过属性设置方法进行配置时,这种设计方式也不是一个理想的选择。对于某些对象来说,创建后的简单配置反而比使用构建者模式更为合适。

此外,当构建步骤之间存在严格的顺序要求,而线性构建者模式无法满足这些要求时,最好避免使用这种模式。如果某一步骤在执行之前必须知道前一步的结果,那么采用其他设计模式可能会更加合适。

结论

构建者设计模式解决了随着对象结构变得越来越复杂,每个开发人员都会遇到的问题。参数众多的构造函数会导致代码难以阅读、维护困难,且容易出现配置错误;可选参数还需要进行空值检查以及设置默认值,这些操作往往会使代码的意图变得模糊不清。

构建者模式将复杂对象的构建过程与对象本身分离开来。配置信息是通过一系列描述性的方法调用逐步累积起来的,而最终的构建步骤则会验证这些配置并生成最终的对象。产品的私有构造函数确保了无法绕过构建者模式来创建对象。

在一个真实的Flutter金融科技应用中,轮播组件的实现就很好地体现了这一设计模式:构建者负责收集各种组件的配置信息,工厂方法为特定类型的轮播组件提供预先配置好的构建步骤,而产品本身只能通过构建者模式来创建。每当添加新的轮播组件类型时,就需要为工厂方法添加相应的代码;当需要修改轮播组件的配置时,也只需调整对应的工厂方法即可。调用代码根本不需要直接接触轮播组件的内部实现细节。

HTTP请求构建器在数据层也体现了同样的设计模式:通过方法链逐步进行配置设置,为可选参数提供了合理的默认值,在构建之前会进行验证检查,最终生成的状态一定是不可变的。

方法链的存在使得构建者模式的代码具有很高的可读性。每次方法调用都会返回构建者对象,从而使得后续的调用能够依次进行。这种链式调用方式从左到右或从上到下地执行,就像是在描述整个构建过程一样。这不仅仅是一种美观的设计手法,更是让构建者模式代码具备自文档说明能力和长期维护性的关键所在。

复杂对象的构建是每个代码库都会遇到的问题,而构建者模式正是经验丰富的工程师们解决这一问题的有效手段。

相关文章

技术实践

在修改现有的代码库之前,如何利用人工智能来理解这些代码的含义

许多工程师在继承现有的代码库时,首先想要做的就是修改这些代码。我完全理解这种冲动。 当你打开一个长达1500行的类文件时,你会看到其中混杂着数据库调用、业务规则、分散在各处的配置值、根本没人愿意去修改的方法,以及那些提到早已被淘汰的系统的注释。 这时,一款人工智能编码助手主动提出可以帮助你理解整个代码库的结构。 于是你就会问: 请重构这个类。 但通常来说,现在还不是进行重构的时候。 从处理遗留系统的经验中,我认识到:代码虽然可能写得很糟糕,但却可能蕴含着重要的信息。 某个奇怪的条件实际上可能代表着某种业务规则;重复出现的计算逻辑可能是由于两个看似相同的流程其实并不完全相同所致;一个命名很糟糕的

阅读全文
技术实践

如何管理代码库中的上下文文件,从而让人工智能编码助手产生更优质的成果

你向编码助手请求创建一个新的端点,90秒后,这个新的端点就已经可以正常使用了。 然后你查看代码变更内容,发现它引入了一个并不存在于你的`package.json`文件中的验证库;尽管你们的团队在去年春天就已经改用了Node.js的测试框架,但它仍然使用Jest来编写测试用例;此外,由于它不知道代码库中其他处理函数都是通过服务来调用的,所以它直接从路由处理函数内部访问了数据库。 代码可以运行,它编写的测试用例也能通过,但你们还是不得不重写大部分代码。 这些情况都不是模型本身的问题——它确实给出了一个看似合理的解决方案,但它之所以会犯这些错误,是因为没有人告诉它这个特定的代码库是如何运作的。 你们

阅读全文
技术实践

如何使用Pydantic AI构建具备生产级功能的智能代理

使用原始的LLM SDK来构建AI代理,在开发原型阶段确实可行,但一旦你需要结构化输出、可测试的代码以及具备生产环境可靠性的系统,这些问题就会显现出来。 这些问题的出现具有很强的规律性。你的笔记本代码可以正常运行,于是你将其应用到生产环境中,并开始添加各种补丁:比如为`json.loads`添加异常处理逻辑,编写辅助函数来去除Markdown格式的标记,使用`if`语句检查字段类型,设置重试机制,以及创建一个将工具名称与对应的可调用函数关联起来的映射函数。这些代码单独来看并不复杂,但当它们汇集在一起时,就会占据你代码库的大部分内容,而真正的代理逻辑反而被这些辅助代码所掩盖。 本文将按照这些问题

阅读全文
技术实践

如何利用人工智能对传统应用程序进行现代化改造,同时又避免对其进行彻底的重写?

我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。 但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。 虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。 人工智能让这个问题变得更加复杂了。 它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。 但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果: 还是那个系统,只不过被更快地重新编写了一遍而已。 这并不一定算

阅读全文