You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Crystal语言中何时选择类,何时选择扩展self的模块?

这问题问得太到位了!在Crystal里,类和模块确实都能实现Service.get这种类似静态调用的效果,但它们的设计初衷和适用场景其实差得挺多,我给你梳理清楚:

核心区别:类 vs 模块

先搞明白二者本质上的不同,才能选对:

  • 实例化能力:类可以被new创建实例(比如Service.new),模块完全不行。模块本质是方法、常量的集合,没法生成具体的实例对象。
  • 代码复用方式:类只能单继承,而模块可以通过include(给实例加方法)或extend(给类/模块本身加方法)被多个类复用,是Crystal实现多态和代码共享的核心方式之一。
  • 语义指向:类代表的是具体实体,比如UserOrderDatabaseConnection,哪怕你现在只用类方法,它底层还是个能实例化的“实体模板”;模块则偏向功能集合/命名空间,比如MathStringTools,它不是一个“东西”,而是一组相关的能力或工具。
  • 状态承载:类可以有实例变量、类变量,能轻松承载状态(比如连接池、配置信息);模块虽然extend self后也能加类级变量,但语义上模块更适合无状态的工具集合,硬塞状态会显得很违和。
什么时候用类?
  • 当你这个Service未来可能需要实例化时:比如现在用Service.get做便捷调用,但以后可能需要创建不同配置的Service实例(比如不同API密钥的实例),用类就留足了扩展空间。
  • 当它代表一个有状态的实体时:比如RedisService需要维护连接池状态,或者PaymentService需要保存商户配置,类的结构天然适合承载这些状态。
  • 当你需要继承扩展时:如果以后要写AliyunPaymentService < PaymentService这种子类,类的单继承机制就能完美支持。

举个类的实用例子:

class RedisService
  @@pool = Redis::Pool.new(size: 10)

  # 静态便捷方法
  def self.get(key : String)
    @@pool.with { |conn| conn.get(key) }
  end

  # 预留实例化能力,方便后续扩展
  def initialize(url : String)
    @client = Redis::Client.new(url: url)
  end

  def custom_get(key : String)
    @client.get(key)
  end
end
什么时候用模块?
  • 当它是纯工具方法集合时:比如一组字符串处理、日期格式化的无状态方法,用模块能明确传达“这是工具”的语义,比如StringUtils
  • 当你需要同时支持静态调用和混入时:比如写了一个Loggable模块,extend self后可以用Loggable.log直接调用,同时其他类include Loggable就能获得实例方法log,一举两得。
  • 当你需要纯粹的命名空间时:用来组织相关方法避免全局污染,比如API::V1::UserHandlers,用模块做命名空间比类更合适,因为它不会让人误以为这是个可实例化的实体。

举个模块的例子:

module StringUtils
  extend self

  def capitalize_words(str : String)
    str.split(" ").map(&.capitalize).join(" ")
  end

  def truncate(str : String, max_length : Int32)
    str.size > max_length ? "#{str[0...max_length]}..." : str
  end
end

# 静态调用
StringUtils.capitalize_words("hello world") # => "Hello World"

# 混入到其他类
class Article
  include StringUtils

  def formatted_title
    capitalize_words(title)
  end
end

总结一下:如果是实体/有状态/可能实例化,选类;如果是工具集合/无状态/命名空间/需要混入,选模块。

内容的提问来源于stack exchange,提问作者justapilgrim

火山引擎 最新活动