Erlang/Elixir ChangeSet 的思路 Rails 会借鉴么?

chenge · 2018年09月04日 · 最后由 piecehealth 回复于 2018年09月06日 · 3798 次阅读

Change Set 的中文含义应该是变更设置,set 这里是设置而不是集合的意思。

主要是对数据做一些验证等预处理,Ecto 去掉了 callback 的做法,变更都写在 changset 里面,方便 code review 这些吧。

Rails 开发也可以考虑采用这种方式吧?大家觉得如何?

上个例子来看看?

没啥不可能的,混合范式的 JavaScript 都有借鉴的成品了:https://github.com/poteto/ember-changeset

如果 Rails 想做一样可以做,这大概不是一个能不能或者会不会的问题,而是愿不愿意或者接不接受的问题吧。

感觉代码很清楚。

def signup_changeset(user, params \\ %{}) do
   user
     |> cast(params, [:email, :password, :password_confirmation])
     |> validate_required([email, :password, :password_confirmation])
     |> validate_format(:email, ~r/@/)
     |> validate_length(:password, min: 5)
     |> password_and_confirmation_matches()
     |> generate_password_hash()
 end

 # ...

 defp generate_password_hash(changeset) do
   password = get_change(changeset, :password)
   hash = Comeonin.Bcrypt.hashpwsalt(password)
   changeset |> put_change(:password_hash, hash)
 end
defmodule Myapp.Test.Schema.UserTest do
  use Myapp.DataCase
  alias Myapp.Schema.User

  test "rejects if passwords don't match" do
    bad_pass = %{
        email: "brad@example.com",
        password: "asfga67585ASDF",
        password_confirmation: "asfga67585AS"
      }
    changeset = User.signup_changeset(%User{}, bad_pass)
    refute changeset.valid?
  end

end
nightire 回复

看来已经得到认同了。 不用等 Rails 修改,自己可以这么干,有什么不好么?

@chenge ROM 里借鉴了 Changeset 的概念 :https://api.rom-rb.org/rom/ROM/Changeset.html

Rails 的 ActiveRecord 作为一个超级 model ,在框架层面集成好用的功能于一身。这点跟 Ecto 在设计理念上有根本差异,脱离这点谈 Changeset 不大现实。毕竟 Changeset 的所有功能在 AR 里都有,只是如何组织,以及好不好用的问题。无端添加一个概念反而容易让人迷惑 -- 我该用那种方式修改数据?这点在 Ecto 里并不存在,因为 Changeset 是唯一修改 “模型” 数据的方式。

Changeset 另一个好处是容易脱离数据模型再开一层抽象,比如做外部数据的校验。这方面其实 Ruby 也早就探索过,不过是叫做 Form Object 。虽然 ActiveModel 不太给力,但 Ruby 社区的轮子也不少,不喜欢 AR 的可以去用 dry-rb ,有相对完善的校验和类型系统,足够你干很多事情。

名词是新名词,但内部概念还真不是新概念。

看了下文档,ChangeSet 不就是 Elixir 这样的函数式语言没有实例变量而不得不把所有变量通过参数传递么。

而且用起来跟 ActiveRecord 没啥区别:

changeset = User.changeset(%User{}, %{age: 0, email: "mary@example.com"})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [age: {"is invalid", []}, name: {"can't be blank", []}]
user = User.new(age: 0, email: "mary@example.com")
user.save
user.errors #= > <#ActiveModel::Errors ... @message={:age=>["is invalid"], :name=>["cant be blank"]}

看 Ruby 还短一点。

ChangeSet 用作 validation 一个坏处是很容易被跳过,不能当作数据入库前的守门人。用作 filter 还比较合理。Ruby 2.5 的 yield_self 就可以方便写出链式过滤器调用了。 https://blog.bigbinary.com/2017/12/12/ruby-2-5-added-yield_self.html 下一版也许会把 yield_self 增加一个 alias then

学习新语言看到新特性,就感觉无所不能,忘了以前是怎么做的,这是一种通病。函数式语言推广者把 “无状态” 奉为金科玉律,无视现实世界还有很多有状态的事物,成也于此败也于此。

Rei 回复

没有状态能搞开发么?只是处理状态的方式和思路不一样。

正好今天看到这个写的 elixir 一年体会 https://medium.com/@lorenzo_sinisi/what-i-have-learned-working-1-year-with-full-time-using-elixir-51efd4c1f66

不安利了。

chenge 回复

用脚可以拿筷子吗?练练也是可以用的,但是有手的情况下何须用脚拿筷子。

无状态在处理数据流的时候也许有不少优点,但在跟 DB 交互这里,DB 作为数据流终点就是有状态的,强用无状态实现不是一个优势。

Rei 回复

Struct 是不是状态?

chenge 回复

不懂你的观点。

Struct 用来约束数据结构,数据一旦生成就不可变,要改变的时候只能新增一个数据。例如:

defmodule Order do
  defstruct number: '', status: 'open'
end

iex> order = %Order{ number: '20180101' }
%Order{number: '20180101', status: 'open'}
iex> order_paid = %{order | status: 'paid'}
%Order{number: '20180101', status: 'paid'}
iex> order
%Order{number: '20180101', status: 'open'} # 旧的数据依然存在

此时内存中存在两份 number 为 20180101 的订单,就因为 elixir 的变量不可变,新变量只是在假装不知道旧数据存在罢了。我认为这不是很好的对现实关系的映射。

可以这样吧,可以重复绑定变量。

order = %{order | status: 'paid'}

chenge 回复

Ops, 我忘了 elixir 相对 erlang 的改变之一就是增加了可重复绑定的变量,这在 erlang 认为是不纯。

直接修改实例变量,不比新增一个 struct 然后覆盖它更直观吗?

order.status = 'paid'
Rei 回复

流行的说法叫第一性原理,软件的第一性原理就是低耦合。对象的问题就是增加了耦合,不是一点点。 任何两个方法之间都是通过数据耦合的,你承认吧。

对象流行的原因是比较而言好理解一些,缺点也是明显的,程序越大越严重,写多了自己就明白了吧。

chenge 回复

不承认对象客观存在,又变着法子模仿对象的功能,只能说开心就好。

"Above all, I hope we don't become missionaries. Don't feel as if you're Bible salesmen. The world has too many of those already. What you know about computing other people will learn. Don't feel as if the key to successful computing is only in your hands."

"No Silver Bullet"

难道 AR 本身不就是 changeset 吗?只是 implicit 而已啊。。。。

Rei 回复

Shadow binding 还是纯的,名字看起来相同但其实是不同的变量。

任何设计都是取舍,Elixir 就没有 dup() 和 clone() 的必要,把 struct 作为参数传给别的方法也不用担心 struct 的状态被改变了,这可以避免一些让人很头疼的 bug。但 struct 就缺乏对象维持状态的功能了 (其实 process 才是和 Ruby 对应的对象……)

luikore 回复

我的理解,不一定对。模块相当于类,Struct 是数据,两个结合起来就可以,比如 100 个订单就是一个 List,有一百个 Struct。

Process 是并发单元,只在需要并发的部分使用,也是有代价的,关系相对复杂些。

luikore 回复

Process 比对象还对象,还有个运行时 (当前执行的方法)。由于变量不可变,还是显示的穿进去的,非常好 debug。

如果喜欢changeset,可以在调#save之前把before_validation, validation, before_save 都手动调一遍,效果差不多。🙈

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册