Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/davydovanton/hanami-architecture
Ideas and suggestions about architecture for hanami projects
https://github.com/davydovanton/hanami-architecture
Last synced: 15 days ago
JSON representation
Ideas and suggestions about architecture for hanami projects
- Host: GitHub
- URL: https://github.com/davydovanton/hanami-architecture
- Owner: davydovanton
- Created: 2017-09-25T23:04:40.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2018-07-05T20:28:09.000Z (over 6 years ago)
- Last Synced: 2024-10-19T14:41:43.904Z (2 months ago)
- Size: 121 KB
- Stars: 46
- Watchers: 7
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# hanami-architecture
Ideas and suggestions about architecture for hanami projects
## Table of Contents
* Application rules
* Actions
* View
* API
* Serializers
* HTML
* Forms
* View objects
* IoC containers
* How to load all dependencies
* How to load system dependencies
* `Import` object
* Testing
* Interactors, operations and what you need to use
* When you need to use it
* Hanami-Interactors
* Dry-transactions
* Testing
* Domain services
* Service objects, workers
* Models
* Command pattern
* Repository
* Entity
* Changesets
* Event sourcing## Application rules
All logic for displaying data should be in applications.If your application include custom middleware, it should be in apps/app_name/middlewares/ folder
### Actions
Actions it's just a transport layer of hanami projects. Here you can put:
1. request logic
2. call business logic (like services, interactors or operations)
3. Sereliaze response
4. Validate data from users
5. Call simple repository logic (but you need to understand that it'll create tech debt in your project)```ruby
module Api::Controllers::Issue
class Show
include Api::Action
include Import['tasks.interactors.issue_information']params do
required(:issue_url).filled(:str?)
end# bad, business logic here
def call(params)
if params[:action] == 'approve'
TaskRepository.new.update(params[:id], { approved: true })
ApproveTaskWorker.perform_async(params[:id])
else
TaskRepository.new.update(params[:id], { approved: false })
endredirect_to routes.moderations_path
end# good, we use intecator for updating task and sending some to background
def call(params)
TaskStatusUpdater.new(params[:id], params[:action]).call
redirect_to routes.moderations_path
end
end
end
```We will cover `Import` object in [`Import` object](https://github.com/davydovanton/hanami-architecture#import-object) section.
### API
#### Serializer
Try to use https://github.com/nesaulov/surrealist with hanami-view presenters. For example:
```ruby
# in apps/v1/presenters/entities/user.rbrequire 'hanami/view'
module V1
module Presenters
module Entities
class User
include Surrealist
include Hanami::Presenterjson_schema do
{
id: Integer,
first_name: String,
last_name: String,
email: String
}
endend
end
end
end# in apps/v1/presenters/users/show.rb
module V1
module Presenters
module Users
class Show
include Surrealistjson_schema do
{
status: String,
user: Entities::User.defined_schema
}
endattr_reader :user
# @example Base usage
#
# user = User.new(name: 'Anton')
# V1::Presenters::Users::Show.new(user).surrealize
# # => { "status": "ok", "user": { "name": "Anton" } }
def initialize(user)
@user = Entities::Price.new(user)
enddef status
'ok'
end
end
end
end
end
```### HTML
#### Forms
#### View objects
## IoC containers
[IoC containers](https://gist.github.com/blairanderson/8072d951a480a590f0bd) is preferred way to work with project dependencies.We suggest to use [dry-containers](http://dry-rb.org/gems/dry-container/) for working with containers:
```ruby
# in lib/container.rb
require 'dry-container'class Container
extend Dry::Container::Mixinregister('core.http_request') { Core::HttpRequest.new }
namespace('services') do
register('analytic_reporter') { Services::AnalyticReporter.new }
register('url_shortener') { Services::UrlShortener.new }
end
end
```Use string names as a keys, for example:
```ruby
Container['core.http_lib']
Container['repository.user']
Container['worders.approve_task']
```You can initialize dependencies with different config:
```ruby
# in lib/container.rb
require 'dry-container'class Container
extend Dry::Container::Mixinregister('events.memory_sync') { Hanami::Events.initialize(:memory_sync) }
register('events.memory_async') { Hanami::Events.initialize(:memory_async) }
end
```### How to load all dependencies
### How to load system dependencies
For loading system dependencies you can use 2 ways:
1. put all this code to `config/initializers/*`
2. use [dry-system](http://dry-rb.org/gems/dry-system/)#### Dry-system
This libraty provide a simple way to load your dependency to container. For example you can load redis client or API clients here. Check this links as a example:
* https://github.com/ossboard-org/ossboard/tree/master/system
* https://github.com/hanami/contributors/tree/master/systemAfter that you can use container for other classes.
### `Import` object
For loading dependencies to other classes use `dry-auto_inject` gem. For this you need to create `Import` object:```ruby
# in lib/container.rb
require 'dry-container'
require 'dry-auto_inject'class Container
extend Dry::Container::Mixin# ...
endImport = Dry::AutoInject(Container)
```After that you can import any dependency in to other class:
```ruby
module Admin::Controllers::User
class Update
include Admin::Action
include Import['repositories.user']def call(params)
user = user.update(params[:id], params[:user])
redirect_to routes.user_path(user.id)
end
end
end
```### Testing
For testing your code with dependencies you can use two ways.The first, DI:
```ruby
let(:action) { Admin::Controllers::User::Update.new(user: MockUserRepository.new) }it { expect(action.call(payload)).to be_success }
```The second, mock:
```ruby
require 'dry/container/stub'Container.enable_stubs!
Container.stub('repositories.user') { MockUserRepository.new }let(:action) { Admin::Controllers::User::Update.new }
it { expect(action.call(payload)).to be_success }
```We suggest using mocks only for not DI dependencies like persistent connections.
## Interactors, operations and what you need to use
Interactors, operations and other "functional objects" needs for saving your buisnes logic and they provide publick API for working with domains from other parts of hanami project. Also, from this objects you can call other "private" objects like service or lib.
### When you need to use it
### Hanami-Interactors
Interactors returns object with state and data:
```ruby
# in lib/users/interactors/signup
require 'hanami/interactor'class Users::Intecators::Signup
include Hanami::Interactor
expose :userdef initialize(params)
@params = params
enddef call
find_user!
singup!
endprivate
def find_user!
@user = UserRepository.new.create(@params)
error "User not found" unless @user
enddef singup!
Users::Services::Signup.new.call(@user)
end
endresult = User::Intecators::Signup.new(login: 'Anton').call
result.successful? # => true
result.errors # => []
```Links:
* https://github.com/hanami/utils/blob/master/lib/hanami/interactor.rb### Dry-transactions
## Domain services (simple way)
Use interactors. Interactors are top level and verbs. A feature is directly mapped 1:1 with a use case/interactor.```
Router => Action => Interactor
``````ruby
# Bad
class A::Nested::Namespace::PublishStory
end# Good
class PublishStory
end
```Put all interactors to `lib/bookshelf/interactors` folder. And also, you can call services, repositories, etc from interactors.
## Domain services (hard way)
We have applications for different logic. That's why we suggest using DDD and split you logic to separate domains. All these domains should be in `/lib` folder and looks like:```
/lib
/users
/interactors
/libs/books
/interactors
/libs/orders
/interactors
/libs
```Each domain have "public" and "private" classes. Also, you can call "public" classes from apps and core finctionality (`lib/project_name/**/*.rb` folder) from domains.
![hanami-project](https://github.com/davydovanton/hanami-architecture/blob/master/images/project.png?raw=true)
Each domain should have a specific namespace in a container:
```ruby
# in lib/container.rb
require 'dry-container'class Container
extend Dry::Container::Mixinnamespace('users') do
namespace('interactors') do
# ...
endnamespace('services') do
# ...
end# ...
end
end
```Each domain should have public interactor objects for calling from apps or other places (like workers_) and private objects as libraries:
```ruby
module Admin::Controllers::User
class Update
include Admin::Action
# wrong, private object
include Import['users.services.calculate_something']# good, public object
include Import['users.interactor.update']def call(params)
# ...
end
end
end
```## Service objects, workers
## Event sourcing