:::tips 本质上, 建造者模式是为了能灵活的配置类初始化参数

:::

前言

建造者模式也是用于类的创建的, 但是直接使用构造函数或者用set方法就能创建对象, 为什么还需要一个 建造者模式 呢? 工厂模式 也可以创建对象, 他们的区别是什么?

为什么需要建造者模式

在平时的开发中, 创建一个对象最常用的方式就是 new 或者 构造函数 完成, 那么什么时候这种方式不能用呢, 我们通过一个实例来看一下

假设我们需要一个资源池配置类 ResourcePoolConfig ,编码实现这个类

23种设计模式 - 建造者模式 - 图1

  1. package main
  2. import "fmt"
  3. type CResourcePoolConfig struct {
  4. name string
  5. maxTotal uint
  6. maxIdle uint
  7. minIdle uint
  8. }
  9. func CreatResourcePool(name string) *CResourcePoolConfig {
  10. return &CResourcePoolConfig{
  11. name: name,
  12. maxTotal: 8,
  13. maxIdle: 8,
  14. minIdle: 0,
  15. }
  16. }
  17. func (c *CResourcePoolConfig) setMaxTotal (maxTotal uint){
  18. if maxTotal > 100{
  19. return
  20. }
  21. c.maxTotal = maxTotal
  22. }
  23. func (c *CResourcePoolConfig) setMaxIdle (maxIdle uint){
  24. if maxIdle > 100{
  25. return
  26. }
  27. c.maxIdle = maxIdle
  28. }
  29. func (c *CResourcePoolConfig) setMinIdle (minIdle uint){
  30. if minIdle < 1{
  31. return
  32. }
  33. c.minIdle = minIdle
  34. }
  35. func main() {
  36. test := CreatResourcePool("a")
  37. test.setMaxTotal(70)
  38. test.setMaxIdle(50)
  39. test.setMinIdle(5)
  40. fmt.Println(test)
  41. }
  • 没有遵守的原则
    • 开闭原则 如果增加了参数, 构造函数必然需要修改, 并且有可能修改入参, 这样导出都要修改其构造函数
  • 随着难度加大, 上面的模式面临着以下困难
    • 如果像 name 一样的字段很多, 那么构造函数中的设置必然就有很多, 参数列表就会很长, 但如果我们用set来设置, 又无法放下校验的逻辑
    • 假如 配置间有依赖关系 ,那么配置间的依赖关系或约束条件也没办法放下
    • 如果我们希望配置好就不能动这个类了, 那么暴露set方法就不合适

golang

假设我们需要配置一下下面的业务实体

  1. type Server struct {
  2. Addr string
  3. Port int
  4. Protocol string
  5. Timeout time.Duration
  6. MaxConns int
  7. TLS *tls.Config
  8. }
  • Addr 地址 必填
  • Port 端口号 必填
  • Protocol 协议 有默认值
  • Timeout 超时时间 有默认值
  • MaxConns 最大连接数 有默认值
  • TLS https相关证书 可以没有

由于go中没有重载, 所以可能写出下面这样的代码, 相当的不优雅, 不能忍

  1. func NewDefaultServer(addr string, port int) (*Server, error) {
  2. return &Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil
  3. }
  4. func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
  5. return &Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil
  6. }
  7. func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
  8. return &Server{addr, port, "tcp", timeout, 100, nil}, nil
  9. }
  10. func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
  11. return &Server{addr, port, "tcp", 30 * time.Second, maxconns, tls}, nil
  12. }

配置对象的方式

  1. type Server struct {
  2. Addr string
  3. Port int
  4. Conf *Config
  5. }
  6. type Config struct {
  7. Protocol string
  8. Timeout time.Duration
  9. Maxconns int
  10. TLS *tls.Config
  11. }
  12. func NewServer(addr string, port int, conf *Config) (*Server, error) {
  13. //...
  14. }
  15. //Using the default configuratrion
  16. srv1, _ := NewServer("localhost", 9000, nil)
  17. conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration}
  18. srv2, _ := NewServer("locahost", 9000, &conf)

将 必填 和 非必填的参数分开, 但是 Config中依然要判断 nil 感觉代码不太干净

建造者模式

  1. //使用一个builder类来做包装
  2. type ServerBuilder struct {
  3. Server
  4. }
  5. func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
  6. sb.Server.Addr = addr
  7. sb.Server.Port = port
  8. //其它代码设置其它成员的默认值
  9. return sb
  10. }
  11. func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
  12. sb.Server.Protocol = protocol
  13. return sb
  14. }
  15. func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {
  16. sb.Server.MaxConns = maxconn
  17. return sb
  18. }
  19. func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {
  20. sb.Server.Timeout = timeout
  21. return sb
  22. }
  23. func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {
  24. sb.Server.TLS = tls
  25. return sb
  26. }
  27. func (sb *ServerBuilder) Build() (Server) {
  28. return sb.Server
  29. }
  30. func main(){
  31. sb := ServerBuilder{}
  32. server, err := sb.Create("127.0.0.1", 8080).
  33. WithProtocol("udp").
  34. WithMaxConn(1024).
  35. WithTimeOut(30*time.Second).
  36. Build()
  37. }
  • 这就是标准的一种建造者模式了, 不过这种方式在错误处理的时候会比较麻烦
  • 另外随意暴露了 set 方法, 有可能导致某处被更改

函数式实现(Functional Options)

  1. package main
  2. import (
  3. "crypto/tls"
  4. "fmt"
  5. "time"
  6. )
  7. type Server struct {
  8. Addr string
  9. Port int
  10. Protocol string
  11. Timeout time.Duration
  12. MaxConns int
  13. TLS *tls.Config
  14. }
  15. type Option func(*Server)
  16. func WithProtocol(p string) Option{
  17. return func(s *Server) {
  18. s.Protocol = p
  19. }
  20. }
  21. func WithTimeOut(t time.Duration) Option{
  22. return func(s *Server) {
  23. s.Timeout = t
  24. }
  25. }
  26. func WithMaxConns(maxconns int) Option{
  27. return func(s *Server) {
  28. s.MaxConns = maxconns
  29. }
  30. }
  31. func WithTLS(tls *tls.Config) Option{
  32. return func(s *Server) {
  33. s.TLS = tls
  34. }
  35. }
  36. func NewServer(addr string, port int , options... Option) (*Server, error){
  37. srv := Server{
  38. Addr : addr,
  39. Port: port,
  40. Protocol: "tcp",
  41. Timeout: 30 * time.Second,
  42. MaxConns: 1000,
  43. TLS : nil,
  44. }
  45. for _, option := range options{
  46. option(&srv)
  47. }
  48. return &srv, nil
  49. }
  50. func main() {
  51. s1 , _ := NewServer("127.0.0.1", 1024)
  52. s2 , _ := NewServer("127.0.0.1", 1024, WithProtocol("kcp"))
  53. s3 , _ := NewServer("0.0.0.1", 8080, WithTimeOut(1 * time.Minute), WithMaxConns(200))
  54. fmt.Println(s1,s2,s3)
  55. }

这也是目前各个开源项目中最常用的配置方式, 墙裂推荐