使用 dry-system 和 rom-rb 创建具有系统范围依赖注入的 Sinatra API
Sinatra 通常被视为简单 API 的工具,但它也可以用于管理大型应用程序。dry-rb 库可以帮助您为应用程序创建具有系统范围依赖注入的模块化架构。
目录
- 1. 简介
- 2.当我们的应用程序开始增长时我们该怎么办?
- 3. 如何解决这些问题?dry-system 来帮忙
- 4. 改进我们的 Sinatra 应用程序
- 5. 添加 dry system 和 dry auto_inject gem 作为我们的依赖注入层
- 6. 使用 ROM 和我们的模块化架构添加数据库连接
- 7. 结论
介绍
如今,Ruby 初学者在开发应用程序时,通常会考虑两种方案:如果想要一个简单的单文件 API,那就用Sinatra ;如果只想实现其他功能,那就用Ruby on Rails。那么,在本文中,我将提供一种方法来管理一个大型应用程序,使用 Sinatra 作为 HTTP 库,并使用 dry-rb 库作为模块化架构的粘合剂。
注意:本文生成的所有代码都可以在以下位置找到: https: //github.com/cherryramatisdev/api-with-dry-ruby
当我们的应用程序开始增长时我们该怎么办?
当一个只有一个 Ruby 文件的应用程序开始变得越来越依赖时,我们该怎么办?就我个人而言,答案是依赖注入。基本上,我会开始思考如何管理所有这些新库的配置,并在路由中快速使用它们,这样以后将路由拆分成服务和控制器类就变得轻而易举了。
好的,但是我们该怎么做呢?
对于依赖注入的基本理解可以从以下角度来看:
考虑这个“服务”类:
class SomeService
  def something_important
    # doing something important here
    'information'
  end
end
如果我们想要注入这个服务,我们可以简单地在构造函数中实例化它,例如在控制器上:
require_relative 'services/some_services'
class SomeController
  def initialize
    @service = SomeService.new
  end
  def index
    response = @service.something_important
    {result: response}.to_json
  end
end
但是这种方法有什么问题呢?嗯,对于小规模的应用程序来说,这并不会带来太多的复杂性,而且保持所有组件的隔离和可用性也相当简单,但是对于中型到大型的应用程序来说,我们会引入一些麻烦,例如:
- 
  并非所有提供商都具有简单的设置::某些提供商(例如 ORM)需要更多配置,这可能难以维护并通过应用程序提供。 
- 
  一些提供商依赖于另一个提供商::当您想要一个提供商用于数据库连接,另一个提供商用于存储库时,手动管理会非常困难,而且这种情况经常发生。 
- 
  Require hell :: 在 Ruby 上,我们没有在每个文件上导入所有库和内部代码的习惯;Ruby on Rails 等框架为具有业务逻辑的文件提供自动需要功能,当您手动滚动应用程序时,如果没有此功能就很难开发。 
如何解决这些问题?干式系统来拯救我们
我们将假设一个简单的 Sinatra 应用程序,并通过添加 dry-system 来管理我们的依赖关系;稍后,我们甚至会使用名为 rom-rb 的 gem 添加一个持久层,以增加更现实的 API 示例的功能。
一个简单的 Sinatra 应用程序
Sinatra 是一个轻量级库,设置起来非常简单,但让我们从一个更结构化的项目开始,好吗?
免责声明:本部分假设您具备关于 ruby 语言和 sinatra 库的基本知识。
启动捆绑项目
mkdir myproject && cd myproject && bundle init
添加我们的宝石
bundle add sinatra puma
创建一个路由器类来封装我们的执行
位于config/router.rb
require 'sinatra/base'
class Router < Sinatra::Base
  get '/' do
    {message: 'Hello world'}.to_json
  end
end
- 添加一个config.ru作为我们应用程序的入口点
位于config.ru项目根目录
require_relative 'config/router'
Router.run!
通过此初始设置,我们应该能够运行应用程序bundle exec puma并看到 JSON 作为响应。
改进我们的 Sinatra 应用程序
为了让我们更容易地看到依赖注入的好处,让我们通过创建两个简单的抽象来为这个简单的路由添加一些结构:控制器和服务。
首先,我们将创建一个位于的服务,lib/service/user.rb其内容如下:
module Services
  class User
    def index
      'teste'
    end
  end
end
然后让我们创建一个位于的示例控制器,lib/controllers/user.rb其内容如下:
require_relative 'lib/services/user'
module Controllers
  class User
    def initialize
      @service = Services::User.new
    end
    def index
      {message: @service.index}.to_json
    end
  end
end
如您所见,我们已经按照旧方法实例化了服务,因此我们可以通过添加来进行比较dry-system!
最后,只需更新config/router.rb文件的内容:
require 'sinatra/base'
require_relative 'lib/controllers/user'
class Router < Sinatra::Base
  get '/' do
    Controllers::User.new.index
  end
end
很简单吧?现在一切都应该正常了,但我们不会就此止步,所以让我们开始整合dry-system它,看看它的好处。
添加 dry system 和 dry auto inject gem 作为我们的依赖注入层
添加我们的宝石
bundle add dry-system dry-auto_inject zeitwerk
使我们的应用程序 REPL 工作
REPL(读取-求值-打印循环)对于 Ruby 开发者来说是一个非常重要的工具。Rails 和 Hanami 框架都提供了 REPL,所以我们将为应用程序设置一个简单的 REPL。这将使我们能够进一步集成依赖注入层,从而使我们的代码更加模块化,更易于测试。
为此,我们将创建一个名为 config/boot.rb 的文件并添加以下代码:
ENV['APP_ENV'] ||= 'development'
require 'bundler'
Bundler.setup(:default, ENV.fetch('APP_ENV', nil))
之后创建一个脚本文件,bin/console内容如下:
#!/usr/bin/env ruby
require 'irb'
IRB.start
为了使其可执行,您可以运行chmod +x ./bin/console
现在我们应该有一个适用于该应用程序的 REPL!
创建我们的主要容器
该容器将用于注册我们应用程序的所有其他组件
在下面创建一个文件,config/application.rb内容如下:
require 'dry/system'
class Application < Dry::System::Container
  configure do |config|
    config.root = Pathname('.')
    config.component_dirs.loader = Dry::System::Loader::Autoloading
    config.component_dirs.add 'lib'
    config.component_dirs.add 'config'
  end
end
loader = Zeitwerk::Loader.new
loader.push_dir(Application.config.root.join('lib').realpath)
loader.push_dir(Application.config.root.join('config').realpath)
loader.setup
通过这段代码,您可以看到我们已经解决了其中一个问题;该component_dirs.add方法和 Zeitwerk 实例将自动需要lib和config文件夹中的所有代码。
注意:zeitwerk gem 正在为我们进行延迟加载。
让我们将其包含在我们的入口点中,以使其立即发挥作用。
在config.ru和上bin/console我们将添加以下内容:
require_relative 'config/application'
Application.finalize!
该finalize!方法使Application实例变量可用于整个应用程序,并延迟加载lib和config文件夹下的文件。
require_relative提示:您可以(并且建议)从控制器和路由器文件中删除
现在您可以通过在 REPL 上bin/console键入来运行和检查应用程序实例。Application
添加示例服务作为提供者
现在我们有了主容器,接下来只需要向其注册提供者,如下所示:
创建一个位于的文件,config/providers/services.rb其内容如下:
Application.register_provider(:services) do
  start do
    register('services.user', Services::User.new)
  end
end
创建此提供程序后,我们会将其加载到我们的入口点文件中;这些是我们唯一需要文件的地方。
在config.ru:
require_relative 'config/providers/services'
还有bin/console:
require_relative '../config/providers/services'
享受工作带来的益处
回到我们的控制器类,我们可以像这样重写它:
module Controllers
  class User
    def initialize
      @service = Application['services.user']
    end
    def index
      {message: @service.index}.to_json
    end
  end
看看控制器类是如何对它从哪个类获取数据一无所知的Application['services.user']?这太酷了,因为如果你想彻底改变你的服务,你只需在提供程序文件上修改类的实例即可。
这个最初的目标对我们来说已经实现了,对吧?但我们会继续前行。
使用 ROM 和我们的模块化架构添加数据库连接
现在我们已经对如何dry-system模块化我们的应用程序有了基本的了解,让我们利用这些知识来添加一个数据库层rom-rb。
添加我们的宝石
bundle add rom rom-repository rom-sql pg
将数据库连接注册为我们系统的提供者
由于我们已经使用了dry-system这一点,因此让我们通过添加数据库连接作为提供程序来使用它:
创建位于的文件,config/providers/db.rb内容如下
免责声明:假设您正在运行 PostgreSQL 数据库。
Application.register_provider(:db) do
  prepare do
    require 'rom'
    require 'rom-sql'
  end
  start do
    connection = Sequel.connect('postgres://postgres:postgres@localhost:5432/example_database', extensions: %i[pg_timestamptz])
    register('db.connection', connection)
    register('db.config', ROM::Configuration.new(:sql, connection))
  end
end
如您所见,该register_provider方法提供了一个简单的 DSL,我们可以使用它来隔离整个设置,方法是要求正确的库prepare,然后在上实例化或注册对象start。
添加对迁移命令的支持
现在我们已经完成了基本连接,让我们Rakefile在项目的根目录下创建一个包含以下内容的连接:
require 'rom-sql'
require 'rom/sql/rake_task'
require_relative 'config/boot'
require_relative 'config/application'
require_relative 'config/providers/db'
namespace :db do
  task :setup do
    Application.start(:db)
    config = Application['db.config']
    config.gateways[:default].use_logger(Logger.new($stdout))
  end
end
如您所见,我们可以用start方法来替代finalize!注入所有提供程序的方法。这样,我们只需通过:db符号启用数据库层即可。这使我们能够在任务上进行注入:setup。
现在我们应该能够运行以下命令:
rake "db:create_migration[create_users]"
这应该创建一个位于的文件db/migrate/3128932189_create_users.rb,在这个文件上,我们可以补充以下 DSL 来为我们的应用程序创建一个示例表:
ROM::SQL.migration do
  change do
    create_table :users do
      primary_key :id
      column :name, String
      column :email, String
    end
  end
end
最后,通过运行以下命令,我们可以将此迁移保留在 Postgres 数据库上:
rake db:migrate
定义我们的关系和存储库
在rom-rbgem 中,我们将主要类定义为关系和存储库。关系模仿 Postgres 表的结构,而存储库定义我们在该关系上的操作。
首先,我们将定义一个关系来表示我们创建的新表。为此,我们将创建一个名为的文件lib/relations/users.rb并添加以下代码:
module Relations
  class Users < ROM::Relation[:sql]
    schema(:users) do
      attribute :id, Types::Integer
      attribute :name, Types::String
      attribute :email, Types::String
      primary_key :id
    end
  end
end
在这里,我们使用ROM::Relation类提供的简单 DSL 来模拟我们的迁移,并为每个属性提供正确的类型。
现在对于存储库,我们可以创建一个lib/repos/user.rb包含以下内容的文件:
require 'rom-repository'
module Repos
  class User < ROM::Repository[:users]
    commands :create
    # @param limit Integer
    def all(limit = 10)
      users.limit(limit).to_a
    end
  end
end
ROM 上的代码库提供了一些常见操作的示例命令,例如
 创建、更新和删除记录。但是,对于更复杂的查询,我们需要编写自己的方法。在本例中,我提供了一个简单的all方法,用于返回所有用户(数量有限)。
通过代码库提供我们的代码
由于我们为应用程序定义了两个新组件,因此我们将在系统上创建两个新的提供程序。
config/providers/persistence.rb首先,让我们创建一个具有以下内容的提供程序:
Application.register_provider(:persistence) do
  start do
    target.start :db
    config = target['db.config']
    config.register_relation(Relations::Users)
    register('container', ROM.container(config))
  end
end
与我们类似,我们在实例化关系类时Rakefile使用该start方法使提供程序可用。db
然后让我们创建另一个提供程序,config/providers/repos.rb其内容如下:
Application.register_provider(:repos) do
  start do
    target.start :persistence
    register('repos.user', Repos::User.new(target['container']))
  end
end
看看我们start之前定义的持久化提供程序是怎么回事?我们不需要启动该db提供程序,因为 dry-system 会自动跳转到持久化提供程序并从那里启动,所以我们可以拥有任意数量的相互依赖的提供程序。
由于我们添加了新的提供商,我们将照常更新我们的入口点文件:
在config.ru:
require_relative 'config/providers/persistence'
require_relative 'config/providers/repos'
并且位于bin/console:
require_relative '../config/providers/persistence'
require_relative '../config/providers/repos'
重构时间到了,好吗?
现在我们已经定义了所需的提供程序,接下来只需在我们想要的层上使用它们;这一层将成为我们的服务类。
在 的服务类上lib/services/user.rb,我们将重写以使用存储库:
module Services
  class User
    def initialize
      @repo = Application['repos.user']
    end
    def list_all
      users = @repo.all
      users.map do |user|
        { id: user.id, name: user.name, email: user.email }
      end.to_json
    end
  end
end
重构完成!很简单,对吧?我们的路由现在应该使用数据库来提供用户列表。
结论
我希望这篇文章对任何读到它的人都能有所帮助。我试图演示如何轻松地解耦应用程序的各个部分并进行管理,即使每个部分都需要复杂的设置。
此外,我随时乐意解答您的任何疑问,或者只是聊聊一些 Ruby 的酷炫知识。愿原力与你同在!
文章来源:https://dev.to/cherryramatis/creating-a-sinatra-api-with-system-wide-dependency-injection-using-dry-system-10mp 后端开发教程 - Java、Spring Boot 实战 - msg200.com
            后端开发教程 - Java、Spring Boot 实战 - msg200.com