使用 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-rb
gem 中,我们将主要类定义为关系和存储库。关系模仿 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