[译] 一步步教你:如何在Ruby搭建微服务架构(下)

上一篇:[译] 一步步教你:如何在Ruby搭建微服务架构(上)


构建你的服务

首先,先停止我们的“Hello World”服务。去到运行服务的控制窗口并按Ctrl + C停止服务。接下来我们需要把“Hello World”服务转换成“Person”服务。


代码结构

让我们先看下项目的代码结构树。它看起来像这样:


  • bin目录存放启动服务的脚本

  • config目录存放全部配置文件

    • 可以在boot.rb文件添加全部服务的依赖。如果你打开它,你会发现已经有很多依赖罗列在那。如果你需要添加更多,这就是你应该修改的地方。

    • application.yml文件保存了全部的应用配置。我们稍候会再来看这个文件。

    • 可以在config/initializers目录里添加初始化脚本。例如,你可以在这添加ActiveRecord配置或者Redis连接。添加到此目录的脚本会在服务启动时运行。

  • 可以在db/migrate目录存放ActiveRecord或者Sequel迁移如果你有的话。若你没有,可以把这些目录一并删除。

  • lib目录则驻扎主要应用代码。

    • settings.rb文件简单地加载了application.yml文件并使它在整个服务的核心过程中都可用,以便你可以在任何地方访问你的配置。例如,Settings.broker.backend将返回在上面YML文件中定义的代理商后端地址。

    • service_register.rb文件注册了服务和服务路由。我们稍候再讲解。

    • hello_world_service.rb文件定义了“Hello Wolrd”服务的端点。

    • 如果你使用ActiveRecord,lib/daos目录则存放ActiveRecord对象,否则存放其他你可能最终创建的数据访问对象(data access objects),例如Sequel模型。

    • lib/dtos目录存放了数据传输对象(data transfer objects)。这些对象最终将会发送回给服务客户端。

    • lib/repositorie目 录存放了你的仓库。仓库是指允许服务访问数据的对象以及唯一允许处理DAO的对象。所以如果一个服务想要一组“Hello World”实例,它会向仓库索取。作为回报,仓库使用合适的DAO从数据库获取相关的数据。然后获取到的数据映射到一个合适的 “HelloWorld”DTO或者“HelloWorld”DTO集合并返回给服务。

    • lib/repositories/mappers用于存放映射器。映射器是指将DAO转换成DTO的对象,反之亦然。


config目录中的application.yml文件看起来像这样:

defaults: &defaults
  broker:
    backend: tcp://127.0.0.1:7776
    frontend: tcp://127.0.0.1:7777
  logging:
    console:
      level: info

development:
  <<: *defaults

test:
  <<: *defaults

production:
  <<: *defaults

此配置仅是简单地设置了代理端后端和前端的地址,以及日记级别。

如果到目前为止这些听起来都很迷惑,别担心因为随着我们继续向前一切将变得更加清晰明了。


Person 服务

那么,让我们继续我们的“Person”服务。先从配置数据连接开始。打开config/initializers/active_record.rb文件并把唯一的一行取消注释。然后,添加以下实体到application.yml中的开发环境配置以便它看起来像这样:

defaults: &defaults
  broker:
    backend: tcp://127.0.0.1:7776
    frontend: tcp://127.0.0.1:7777
  logging:
    console:
      level: info
  database:
    adapter: postgresql
    database: zss-tutorial-development


既然你添加了数据库配置,你得创建对应的数据库。这一次,没办法自动化创建数据库除非你使用的是默认的PostgreSQL数据库,这样的话你就可以简单地运行:

$ rake db:create


如果你偏向另外的数据库,你需要添加合适的gem到gemfile并且对项目进行bundle install。

接下来是迁移(migration)。为此,简单地在db/migrate目录中创建叫做000_creates_persons.rb的文件:

$ touch db/migrate/000_creates_persons_table.rb


打开这个文件并且创建迁移因为你可能使用的是正则Rails迁移:

class CreatesPersons < ActiveRecord::Migration

  def change
    create_table :persons do |t|
      t.name
      t.timestamps
    end
  end
  
end


接下来,运行它:

$ rake db:migrate
== 0 CreatesPersons: migrating ================================================
-- create_table(:persons)
DEPRECATION WARNING: `#timestamp` was called without specifying an option for `null`. In Rails 5, this behavior will change to `null: false`. You should manually specify `null: true` to prevent the behavior of your existing migrations from changing. (called from block in change at /Users/francisco/Code/microservices-tutorial/db/migrate/000_creates_persons.rb:6)
   -> 0.0012s
== 0 CreatesPersons: migrated (0.0013s) =======================================


既然已经创建了数据库表,让我们再为它创建对应的模型。新建文件lib/daos/person.rb

$ touch lib/daos/person.rb


像这样编辑它:

module DAO
  class Person < ActiveRecord::Base
  end
end


这就是你的模型。现在你需要为“Person”创建一个DTO模型以便你可以把它返回给客户端。创建文件lib/dtos/person.rb

$ touch lib/dtos/person.rb


像这样编辑它:

module DTO
  class Person < Base
    attr_reader :id, :name
  end
end


接下来你需要创建一个把“Person”DAO转换成“Person”DTO的映射器。创建文件lib/repositories/mappers/person.rb,并像这样编辑它:

module Mapper
  class Person < Mapper::Base

    def self.to_dao dto_instance
      DAO::Person.new id: dto_instance.id, name: dto_instance.name
    end

    def self.to_dto dao_instance
      DTO::Person.new id: dao_instance.id, name: dao_instance.name
    end

  end
end

这里,Mapper::Base要求你必须实现self.to_dao和self.to_dto。如果你不想这样做,你也可以实现self.map并重写根据接收到的属性是DAO还是STO而调用了to_dao或to_dto的Mapper::Base.map。


现在你已经有了访问数据库的DAO,发送给客户端的DTO,以及进行转换的映射器。你可以在一个仓库里使用这三个类创建允许你从数据库获取用户并返回相应的DTO集合的逻辑。


那么让我们来创建此仓库。新建文件lib/repositories/person.rb

$ touch lib/dtos/person.rb


像这样编辑它:

module Repository
  class Person < Repository::Base

    def get
      DAO::Person.all.map do |person|
        Mapper::Person.map(person)
      end
    end

  end
end


此仓库仅拥有单纯从数据库获取全部用户并且映射成person DTO集合的实例方法get -- 相当简单。让我们把这些整合起来。现在剩下来的就是创建服务以及调用这个创建的端点。为了做到这些,先创建文件lib/person_service.rb

$ touch lib/person_service.rb


像这样编辑它:

class PersonService < BaseService

  attr_reader :person_repo

  def initialize
    @person_repo = Repository::Person.new
  end

  def get payload, headers
    persons = person_repo.get()
    if persons.empty?
      raise ZSS::Error.new(404, "No people here")
    else
      persons.map &:serialize
    end
  end

end


Person服务在初始化中创建了仓库。全部Person服务的public实例方法都有有效负载以及如果不需要可忽略的头部。两者都是Hashie::Mash实例并保存发送给端点的变量,或者属性、头部,以及他们回复模拟HTTP响应因为每个响应都有一个客户端可以用来找到发送给服务请求结果的状态码,随着服务的响应有效负载。响应码和你期望从HTTP服务得到的一样。例如,一个成功的请求将会返回200状态码,并伴随着响应负载。如果某些服务出现了错误,那么状态码将会是500,且如果发送给服务的参数有某些问题,状态码将会是400。服务可以回复很多HTTP状态码和相应的负载。。所以,假如你想你的服务告诉它的客户端不允许访问某个存在的端点时,可以通过403状态码来响应。如果你回头看下我们上面的服务代码,你会看到另一个响应码的例子。在get端点,当找不到用户时我们返回了404状态码以及可选的“No people here”消息,就像HTTP服务找不到可用资源时返回404一样。如果仓库真的需要返回用户,服务将会对DTO进行序列化并返回给客户端。每一个DTO都有一个缺省的序列化器来返回一个JSON对象,其中包含了在DTO的attr_reader或者attr_accessible中定义key和这些key对应的值。当然,你可以通过在你的DTO类中定义序列化方法来重载这个序列化器。


既然有了定义的服务,我们还要注册它。这是最后一步。打开文件lib/service_register.rb并把出现的“HelloWorld”全部替换成“Person”,所以最后这个文件看起来应该像这样:

module ZSS
  class ServiceRegister

    def self.get_service
      config = Hashie::Mash.new(
        backend: Settings.broker.backend
      )

      service = ZSS::Service.new(:person, config)

      personInstance = PersonService.new

      service.add_route(personInstance, :get)

      return service
    end

  end
end

你可以注意到,在调用add_route时有一个小小的变化。我们移除了“HELLO/WORLD”。这是因为仅当服务动词与实现的方法不匹配时才需要此字符串。在我们的案例中,当使用GET动词调用person服务时,调用的方法是get,所以我们可以省略此字符串。


你需要在ServiceRegister类中定义self.get_service方法。这个方法初始化服务并与代理商后端连接起来。随后它会与一个或多个服务定义中的服务方法路由相匹配。例如,在下面场景中,它创建了服务并和代理商绑定:

config = Hashie::Mash.new(
  backend: Settings.broker.backend
)

service = ZSS::Service.new(:person, config)


随后初始化一个服务处理器:

personInstance = PersonService.new


接着,把服务处理器与服务绑定:

service.add_route(personInstance, :get)


最后,必须返回服务实例。

return service


现在,在我们加载“Person”服务前还仅仅只有一步;我们需要为它创建一个可执行的脚本。我们已经有了一个针对“HelloService”的脚本。那么,打开文件bin/zss-service,把“hello-world”替换成“person”,然后保存。回到控制台并运行:

$ bin/zss-service run
Starting person:
	PID: ./log
	LOGS: ./log
Started person daemon...
15-29-15 19:29:54 | INFO | ZSS::SERVICE - Starting SID: 'PERSON' ID: 'person#d3ca7e1f-e229-4502-ac2d-0c01d8c285f8' Env: 'development' Broker: 'tcp://127.0.0.1:7776'


欧啦!你刚刚已经第一次启动了你的“Person”服务。让我们来测试一下。打开文件bin/zss-client,把变量sid的值改成“person”并把客户端的调用从hello_world()改成get()。一旦做好后,在新的窗口运行这个客户端:

$ bin/zss-client
/Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:41:in `new': No people here (ZSS::Error)
	from /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:41:in `call'
	from /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:55:in `method_missing'
	from bin/zss-client:12:in `<main>'


正如你看到的,出现了一个ZSS::Error。这是因为当服务找不到用户时我们抛出了一个错误以及在服务的数据库里还没有用户。


让我们来处理这个错误。打开zss-client并像这样编辑它:

begin
  client = ZSS::Client.new(sid, config)
  p client.get()
rescue ZSS::Client => e
  if e.code == 404
    p e.message
  else
    raise e
  end
end


现在我当错误码是404时则打印错误信息,而如果是其他错误时则继续抛出。运行客户端再来看下效果:

$ bin/zss-client
"No people here"


非常棒!现在添加一些用户到数据库并看下服务是否会返回给客户端。为了做到这一点,简单地打开一个服务控制台:

$ rake service:console


添加一些用户:

$ rake service:console
[1] pry(main)> DAO::Person.create name: 'John'
=> #<DAO::Person:0x007fe51bbe9d00 id: 1, name: "John", created_at: 2015-12-16 13:22:37 UTC, updated_at: 2015-12-16 13:22:37 UTC>
[2] pry(main)> DAO::Person.create name: 'Mary'
=> #<DAO::Person:0x007fe51c1dafe8 id: 2, name: "Mary", created_at: 2015-12-16 13:22:42 UTC, updated_at: 2015-12-16 13:22:42 UTC>
[3] pry(main)> DAO::Person.create name: 'Francis'
=> #<DAO::Person:0x007fe51bc11698 id: 3, name: "Francis", created_at: 2015-12-16 13:22:53 UTC, updated_at: 2015-12-16 13:22:53 UTC>
[4] pry(main)> exit


现在,再次运行客户端。

$ bin/zss-client
[{"id"=>1, "name"=>"John"}, {"id"=>2, "name"=>"Mary"}, {"id"=>3, "name"=>"Francis"}]

可以看到有返回了。


最后的结论

回顾这次教程中提供的代码,你可能会想很多步骤都是不需要的,例如创建仓库或者DTO,其实你是对的。你所需要的是有一个功能性的“Person”服务来作为你的服务类以及DAO,以便可以直接从服务类中调用。不管怎样,最佳实践是遵循此文章中所描述的模式,因为可以把逻辑和数据存储操作分离。服务应该只关注于他们的逻辑,而仓库应该处理数据存储的全部交互。DTO决定了服务的负载以及序列化,而DAO只关心从存储中获取数据。在此教程中描述的约定和技术被称为仓库模式(repository pattern),你可以对照以下这张图片。



我想以邀请任何觉得此文章有用的同学对SOA服务套件进行贡献来结尾,你可以通过任何方式来扩展或者强化它。欢迎你的fork和pull request!


我希望可以帮助你开启微服务。如果你想查看服务源代码,在GitHub上有一个可用的完成的版本


本文翻译作者为:dogstar,发表于开源中国个人博客;欢迎转载,但请注明出处,谢谢!

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
6 收藏
2
分享
返回顶部
顶部