Crystal语言中何时选择类,何时选择扩展self的模块?
这问题问得太到位了!在Crystal里,类和模块确实都能实现Service.get这种类似静态调用的效果,但它们的设计初衷和适用场景其实差得挺多,我给你梳理清楚:
核心区别:类 vs 模块
先搞明白二者本质上的不同,才能选对:
- 实例化能力:类可以被
new创建实例(比如Service.new),模块完全不行。模块本质是方法、常量的集合,没法生成具体的实例对象。 - 代码复用方式:类只能单继承,而模块可以通过
include(给实例加方法)或extend(给类/模块本身加方法)被多个类复用,是Crystal实现多态和代码共享的核心方式之一。 - 语义指向:类代表的是具体实体,比如
User、Order、DatabaseConnection,哪怕你现在只用类方法,它底层还是个能实例化的“实体模板”;模块则偏向功能集合/命名空间,比如Math、StringTools,它不是一个“东西”,而是一组相关的能力或工具。 - 状态承载:类可以有实例变量、类变量,能轻松承载状态(比如连接池、配置信息);模块虽然
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




