很好的思考。但 Rails 的核心思想是约定优于配置,并且高度依赖灵活的元编程,很多 OO 的传统手艺,比如 Dependency Injection 会有点水土不服,因为控制器、模型等 app 下的类都是自动加载的,各种类在其它类中都是直接用。
对于 单一功能原则,很多情况下只是一个理想,如果都实现,一定会过度工程化。一个 Rails 的 post controller 严格来说就已经违反了这个原则,CRUD, 加上 flash 消息,还有参数过滤验证,远不只单一功能了。对于 QuotationCommand 这种类,远没有到复杂到看不懂的地步,无法想像牵一发而动全身的情况,如果有,在这个类里面应该也可以搞定,如果测试覆盖得还不错,不可能有什么灾难。
解耦是没有解耦的,以前依赖 ListCommand, 现在依赖 Search::UserSearch, 不过好处还是有的,职责分离了,减少了层级依赖,可以更好测试. 不过要完全解耦得用 DI:
class ListCommand
def initialize(params, search_service: Search::UserSearch.new)
@params = params
@search_service = search_service
end
def call
@search_service.search(@params)
end
end
class ExportCommand
def initialize(params, search_service: Search::UserSearch.new)
@params = params
@search_service = search_service
end
def call
users = @search_service.search(@params)
# 导出逻辑...
end
end
然后你就可以:
search_service = Rails.env.production? ?
Search::ElasticsearchUserSearch.new :
Search::UserSearch.new
ListCommand.new(params, search_service: search_service)
不过说实话,这完全是自 high, rails 不讲究 DI, 早年很多 DI gem 都死了。
我也完全可以接受没有重构的版本,因为你的 export 就是把 user list 导出,你的 list 变了,export 就导出你变化了的 list, 完全不要重构。除非你还要导出 post list, 如果真是那样,你导出 user 的 export 类大概率也不能重用。
个人觉得完全没必要拆分,虽然 Concern 可以这样用,但最初的设计是为了 model 间共享重复代码。
除此之外,这一切考量都是基于传统编程,在 AI 加持下,我倾向减少这种拆分,因为这增加了 AI 文件操作的时间,那么多 Concerns 也有很多重复代码,这会增加 token, 花更多钱,我觉得原来 user model 中的注释就已经很好地分隔了功能。
我不知道你们现在怎样,目前我的项目 95% 的代码都是 Cursor 写的,包括测试,很少出现要亲自 debug 某个功能的情况,Cursor 甚至可以自己加 Rails.logger.debug 然后再通过输出自己找到问题,我只要告诉它出现了什么 bug.
对于 FavoritesController, 虽然看上去是简单了,只用了 create, destroy 这种标准 action, 但你在 controller 目录下增加了一个文件,我觉得还不如在 controller 加两个非标准的 action, 因为在实际项目中,很多 controller 都会有几个非标准的 action, 甚至还带很多参数,按这种代码洁癖,controller 文件夹下的非标 action 变成的 controller 文件也会群魔乱舞。自己跟 AI 聊天时还要手动添加更多文件,想想都头大。
时代变了,参考文献早于 2022 年的都要想想是不是针对 AI 有弊端。想想自己以前抓头发写过的优雅代码,在 Pull request 跟初级程度员吵过的架,现在看都是浮云,当打工人只要把老板哄开心了,自己的项目只要把用户哄开心了就可以了。有时候优化了半天,结果项目没了,只能安慰自己编程水平提高了。现在面对 AI, 觉得自己那点水平也不过如此。
高级的食材,只需要简单的烹饪,rails 已经搭好了一个架子,用 AI 把功能填进去就可以了,少玩一点花活,也不用给初级程序员讲解代码。
fat controller, fat model, spaghetti code 跟我关系都不大,写代码的是 AI, 修 Bug 的也是它,有人担心工程大了,AI 的上下文不够长怎么办,这有点过虑了,工程代码增长的速度应该是赶不上大模型 context 增长的速度。
愿闻其详,是不是 Web 够用了?
DX(Developer Experience)请写全。都是小学词汇,放个缩写真的很难猜。
为什么要一定要流式,放个菊花转一转也可以吧。 :)
顺便说一下,表情包用不了了。
粗略看了一下,是个用 map 和 vector 的轮子,CRUD 只实现了 C, 目的是为了解决 Ruby String 对象的内存占用问题,特别是重复对象和空值。
所以 Readme 中提到的 480MB 的 Ruby 内存占用我不知道你是不是用了 freeze
或 # frozen_string_literal: true
, 另外,Ruby 3.4 默认设置了 frozen_string_literal 为 true, 如果你有 1440000 个字符串,可以升级 Ruby 看一下内存占用。
你的 gem 在我看来只适合你的特定场景,一个健壮的 rails 系统要尽量减少 gem, 如果内存溢出,得排查老半天,如果不是自己写的 gem, 灾难程度还要升级。
因为内存的特性,内存中的数据只适合做缓存,如果真要在本地用几百 MB 的缓存,我可以在本地装个 Redis 就可以了,Redis 默认还有持久化,速度也能保证。当然,我最先会考虑 ActiveSupport::Cache::MemoryStore
, Redis 都不会装。
MemoryStore 的 ruby 对象设计也很优雅,内存限制内不会被清理,超过设定值后,最久未使用(LRU)项会被清理。现在内存这么便宜,我在欧洲 OVH 16 欧元/月的服务器都是 64 GB 内存,为什么还要折腾代码?
ActiveSupport::Cache::MemoryStore
已经实现了这个功能。Ruby 现在几乎等同于 Rails, 所以用不到你这个 Gem 吧。
css 挂了
Rails 这边是 unique 的,数据库 migration 那边应该同时加上约束吧。擦皮鞋哪有只擦一只的道理,只擦一只鞋不应该过 Pull request。
有些高并发并且不要求 unique 的情况下,也可能造成数据混乱,比如要求只能一个 worker 处理一个对象的数据,但 workers 从一个任务池拿任务时,有可能多个 workers 先后拿到同一个对象不同的耗时任务,因为这个对象可能前前后后有很多任务,这时候用最简单的 advisory_lock 就可以避免了,可以用对象的 id 做 锁的 key。
https://dev.mysql.com/doc/refman/9.1/en/locking-functions.html
任务开始的时候用 IS_FREE_LOCK('#{id}')
查一下,如果 free 就 get_advisory_lock
, 最后 ensure
一下 release_advisory_lock
ActiveRecord::Base.connection.get_advisory_lock(id)
ActiveRecord::Base.connection.release_advisory_lock(id)
这样就保证了只有一个 worker 能处理这个耗时任务,其它的 works 拿到这个对象的任务时,要把这个任务放回任务池。
https://apidock.com/rails/ActiveRecord/ConnectionAdapters/AbstractAdapter/get_advisory_lock
Quote: “如果不是经常更新的内容,比如首页的 Banner 和广告这类首页接口,并发量最大,而且更新少”
常更新的也没有问题,设置好过期时间就行了,一小时更新一次,一天也就 24 次,虽然每个 puma 进程都管理自己的 cache,比如说有 8 个 puma 进程,同一个缓存就要重复保存 8 次,那一天也只更新 24*8 = 192 次,但现在内存这么便宜,数据库又放在 ssd 上,不用太在乎。
Rails 官方的文档不建议在生产环境中使用 MemoryStore 是因为多进程或多服务器之间不能共享缓存,另外就是上面说的会浪费内存。理解了原因就可以打破规则了。
我经手的一个生产环境的 k8s 里有十几台 64GB 的 rails 服务器,直接配置的 MemoryStore 作为 Cache, 再用另外的全局变量 redis_client 连接 Redis cluster 用来处理经常变动 (1 小时以下,一般是分钟以下,秒级别) 的缓存,目前运行得非常稳。
对于那种又大又不定期更新的缓存,还希望能在各进程和服务器间同步更新,并且不希望把这么大的缓存存在 redis,也有办法:
:categories_cache
中,永不过期,另外再把缓存生成的时间保存在 :categories_lazy_updated_at
中,永不过期。在更新 model 的同时,用 after_save
钩子在 Redis 中更新一个对应的键 categories_redis_updated_at
,值就是当下的 timestamp,在各个服务器每个进程中的 MemoryStore 缓存这个 Redis 的键值对,键名 :categories_eager_updated_at
,过期时间一分钟或你期望的间隔。
每次读取 :categories_cache
时都比较 :categories_lazy_updated_at
和 :categories_eager_updated_at
的值,这个过程都在本地,时间忽略不计,如果前者小于后者,说明 Category 已经更新了,那重新生成缓存 :category_cache
,再把 :categories_lazy_updated_at
的值更新为 categories_eager_updated_at
的值,其实也是categories_redis_updated_at
的值。
这样在 Redis 中只用了一个 timestamp 就在极小的间隔同步了各个进程和各个服务器之间的缓存更新。如果有多个这样的缓存,还可以优化,比如把多个缓存的 timestamps 组成一个 hash 放在 Redis 的一个键中。我最后还做了一个页面,所有的缓存都一目了然,包括值,占用空间大小,过期时间,当然页面中的本地缓存只是某一台服务器中某一个 puma 进程中的值
Rails 的 MemoryStore 效率奇高,因为以前内存贵 HDD 慢,一直在业界没好好利用,我看完源码后觉得现在不在生产环境用真是太浪费了,特别是那些大块头又常年不更新的缓存,现在它们极大概率可以在 garbage collection 中 survive。
沉下心来好好学习了一下这个 docker 示例以后,坚定成为 docker 党。
Rails dev prod 和 docker-compose 用同一个.env 配置文件。开发环境生产环境完全一致,操作系统 Win Mac Linux 完全无关,升级各种软件环境只要改文本配置。
一次学习,之后完全不在这方面耗任何时间,强烈推荐。
“做”和“做到极致”是两个不同的概念。如果你耐心看完所有内容,相信你会认同那么多 star. 不然你可以开个 repo, 我给你 star.
这哥们是 docker 船长,真把 docker 玩明白了,比 Rails 官方还玩得明白,我深度使用后觉得值得推荐。
https://github.com/nickjj/docker-rails-example
我的 pg 和 redis 没放 docker. 但我有一个 python 项目把 pg 和 redis 放在 docker 里,数据文件存母机,大小过 TB, 运行得也非常稳。
赞,很好的项目,如果在开发的时候,顺便做个笔记,那将会是一个很好的初学者教程。
如果我会搭,那我会自己搭一个,而不是用你的,因为总有一些需求/细节跟你不一样。
如果我不会搭,那就更不会用你的了,有耐心想学的话,可能要从头爬你的 commits.
cool
是 MIT 吗?没有 License 信息。
点赞👍
要不要把这个翻译成 ruby? https://github.com/ccxt/ccxt
也许把你实现的方法分享出来才对大家更有帮助吧?
很好的想法。想知道相对我自己注册公司,你们有什么优势。这也应该是你们宣传的重点吧。
这个好像不容易。
最简单的方式就是选 n, 然后 copy paste.
程序方面,可能可以加个 e 选项,打开默认编辑器编辑 commit 信息,我有空试一下。
谢谢 Rei 对中文 Rails 圈的贡献
另外挑个骨头:alias 重音在最前面。 http://dict.cn/alias
RubyMine 家的授权比较特殊,个人版可以在公司用,公司版又贵很多,公司又不能私下给我点钱买个人版。 最后公司给我买的公司版,贵是贵了点,但都合规。
用了 RubyMine 就不想换了,Debug 下断点太方便了,包括 RSpec 也能下断点,再也不用 binding.pry 了。
好像 VS Code 也能下断点,没空研究,但猜测应该不如 RubyMine,希望我错了。
跳转也很方便,当然遇到个 小明 小红 小帅 的方法名,那就要在一堆列表里找了。
请问是 #10 楼说的那样使用源码编译,加上 --enable-yjit 吗?
我也不会,但可以看别人的代码: https://github.com/howl-anderson/scel2txt
现在中国 ruby 程序员的工资大概在什么水平? 我在德国,一般的程序员,税前大概 6 万欧左右,德国税重保险贵,能扣一少半。其实我所在的公司也想招远程,但这工资水平,感觉国内的人看不上。
https://edgeapi.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch
Rails 6.0 以后有 skip_nil
选项了。
向你学习!英语的受众的确广很多!