Skip to content

Commit

Permalink
Merge pull request #2 from solid-process/chore/examples
Browse files Browse the repository at this point in the history
Add examples folder
  • Loading branch information
serradura authored Jun 23, 2024
2 parents f3b9927 + 7a2fd35 commit 26e7609
Show file tree
Hide file tree
Showing 22 changed files with 742 additions and 0 deletions.
7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## `Solid::Adapters` Examples

> **Attention:** Each example has its own **README** with more details.
1. [Ports and Adapters](./ports_and_adapters) - Implements the Ports and Adapters pattern. It uses **`Solid::Adapters::Interface`** to provide an interface from the application's core to other layers.

2. [Anti-Corruption Layer](./anti_corruption_layer) - Implements the Anti-Corruption Layer pattern. It uses the **`Solid::Adapters::Proxy`** to define an interface for a set of adapters, which will translate an external interface (`vendors`) to the application's core interface.
212 changes: 212 additions & 0 deletions examples/anti_corruption_layer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
- [🛡️ Anti-Corruption Layer Example](#️-anti-corruption-layer-example)
- [The ACL](#the-acl)
- [🤔 How does it work?](#-how-does-it-work)
- [📜 The Contract](#-the-contract)
- [🔄 The Adapters](#-the-adapters)
- [⚖️ What is the benefit of doing this?](#️-what-is-the-benefit-of-doing-this)
- [How much to do this (create ACL)?](#how-much-to-do-this-create-acl)
- [Is it worth the overhead of contract checking at runtime?](#is-it-worth-the-overhead-of-contract-checking-at-runtime)
- [🏃‍♂️ How to run the application?](#️-how-to-run-the-application)

## 🛡️ Anti-Corruption Layer Example

The **Anti-Corruption Layer**, or ACL, is a pattern that isolates and protects a system from legacy or dependencies out of its control. It acts as a mediator, translating and adapting data between different components, ensuring they communicate without corrupting each other's data or logic.

To illustrate this pattern, let's see an example of an application that uses third-party API to charge a credit card.

Let's start seeing the code structure of this example:

```
├── Rakefile
├── config.rb
├── app
│ └── models
│ └── payment
│ └── charge_credit_card.rb
├── lib
│ ├── payment_gateways
│ │ ├── adapters
│ │ │ ├── circle_up.rb
│ │ │ └── pay_friend.rb
│ │ ├── contract.rb
│ │ └── response.rb
│ └── payment_gateways.rb
└── vendor
├── circle_up
│ └── client.rb
└── pay_friend
└── client.rb
```

The files and directories are organized as follows:

- `Rakefile` runs the application.
- `config.rb` file contains the configurations.
- `app` directory contains the domain model where the business process to charge a credit card is implemented.
- `lib` directory contains the payment gateways contract and adapters.
- `vendor` directory contains the third-party API clients.

## The ACL

The ACL is implemented in the `PaymentGateways` module (see `lib/payment_gateways.rb`). It translates the third-party APIs (see `vendor`) into something known by the application's domain model. Through this module, the application can charge a credit card without knowing the details/internals of the vendors.

### 🤔 How does it work?

The `PaymentGateways::ChargeCreditCard` class (see `app/models/payment/charge_credit_card.rb`) uses`PaymentGateways::Contract` to ensure the `payment_gateway` object implements the required and known interface (input and output) to charge a credit card.

```ruby
module Payment
class ChargeCreditCard
include ::Solid::Output.mixin(config: { addon: { continue: true } })

attr_reader :payment_gateway

def initialize(payment_gateway)
@payment_gateway = ::PaymentGateways::Contract.new(payment_gateway)
end

def call(amount:, details: {})
Given(amount:)
.and_then(:validate_amount)
.and_then(:charge_credit_card, details:)
.and_expose(:payment_charged, %i[payment_id])
end

private

def validate_amount(amount:)
return Continue() if amount.is_a?(::Numeric) && amount.positive?

Failure(:invalid_amount, erros: ['amount must be positive'])
end

def charge_credit_card(amount:, details:)
response = payment_gateway.charge_credit_card(amount:, details:)

Continue(payment_id: ::SecureRandom.uuid) if response.success?
end
end
end
```

#### 📜 The Contract

The `PaymentGateways::Contract` defines the interface of the payment gateways. It is implemented by the `PaymentGateways::Adapters::CircleUp` and `PaymentGateways::Adapters::PayFriend` adapters.

```ruby
module PaymentGateways
class Contract < ::Solid::Adapters::Proxy
def charge_credit_card(params)
params => { amount: Numeric, details: Hash }

outcome = object.charge_credit_card(params)

outcome => Response[true | false]

outcome
end
end
end
```

In this case, the contract will ensure the input by using the `=>` pattern-matching operator, which will raise an exception if it does not match the expected types. After that, it calls the adapter's `charge_credit_card` method and ensures the output is a `PaymentGateways::Response` by using the `=>` operator again.

The response (see `lib/payment_gateways/response.rb`) will ensure the ACL, as it is the object known/exposed to the application.

```ruby
module PaymentGateways
Response = ::Struct.new(:success?)
end
```

#### 🔄 The Adapters

Let's see the payment gateways adapters:

`lib/payment_gateways/adapters/circle_up.rb`

```ruby
module PaymentGateways
class Adapters::CircleUp
attr_reader :client

def initialize
@client = ::CircleUp::Client.new
end

def charge_credit_card(params)
params => { amount:, details: }

response = client.charge_cc(amount, details)

Response.new(response.ok?)
end
end
end
```

`lib/payment_gateways/adapters/pay_friend.rb`

```ruby
module PaymentGateways
class Adapters::PayFriend
attr_reader :client

def initialize
@client = ::PayFriend::Client.new
end

def charge_credit_card(params)
params => { amount:, details: }

response = client.charge(amount:, payment_data: details, payment_method: 'credit_card')

Response.new(response.status == 'success')
end
end
end
```

You can see that each third-party API has its way of charging a credit card, so the adapters are responsible for translating the input/output from the third-party APIs to the output known by the application (the `PaymentGateways::Response`).

## ⚖️ What is the benefit of doing this?

The benefit of doing this is that the core business logic is decoupled from the legacy/external dependencies, which makes it easier to test and promote changes in the code.

Using this example, if the third-party APIs change, we just need to implement a new adapter and make the business processes (`Payment::ChargeCreditCard`) use it. The business processes will not be affected as it is protected by the ACL.

### How much to do this (create ACL)?

Use this pattern when there is a real need to decouple the core business logic from external dependencies.

You can start with a simple implementation (without ACL) and refactor it to use this pattern when the need arises.

### Is it worth the overhead of contract checking at runtime?

You can eliminate the overhead by disabling the `Solid::Adapters::Proxy` class, which is a proxy that forwards all the method calls to the object it wraps.

When it is disabled, the `Solid::Adapters::Proxy.new` returns the given object so that the method calls are made directly to it.

To disable it, set the configuration to false:

```ruby
Solid::Adapters.configuration do |config|
config.proxy_enabled = false
end
```

## 🏃‍♂️ How to run the application?

In the same directory as this `README`, run:

```bash
rake

# -- CircleUp --
#
# #<Solid::Output::Success type=:payment_charged value={:payment_id=>"2df767d0-af83-4657-b28d-6605044ffe2c"}>
#
# -- PayFriend --
#
# #<Solid::Output::Success type=:payment_charged value={:payment_id=>"dd2af4cc-8484-4f6a-bc35-f7a5e6917ecc"}>
```
30 changes: 30 additions & 0 deletions examples/anti_corruption_layer/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

if RUBY_VERSION <= '3.1'
puts 'This example requires Ruby 3.1 or higher.'
exit! 1
end

require_relative 'config'

task :default do
puts '====================='
puts 'Anti Corruption Layer'
puts '====================='

puts
puts '-- CircleUp --'
puts

circle_up_gateway = PaymentGateways::Adapters::CircleUp.new

p Payment::ChargeCreditCard.new(circle_up_gateway).call(amount: 100)

puts
puts '-- PayFriend --'
puts

pay_friend_gateway = PaymentGateways::Adapters::PayFriend.new

p Payment::ChargeCreditCard.new(pay_friend_gateway).call(amount: 200)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'securerandom'

module Payment
class ChargeCreditCard
include ::Solid::Output.mixin(config: { addon: { continue: true } })

attr_reader :payment_gateway

def initialize(payment_gateway)
@payment_gateway = ::PaymentGateways::Contract.new(payment_gateway)
end

def call(amount:, details: {})
Given(amount:)
.and_then(:validate_amount)
.and_then(:charge_credit_card, details:)
.and_expose(:payment_charged, %i[payment_id])
end

private

def validate_amount(amount:)
return Continue() if amount.is_a?(::Numeric) && amount.positive?

Failure(:invalid_amount, erros: ['amount must be positive'])
end

def charge_credit_card(amount:, details:)
response = payment_gateway.charge_credit_card(amount:, details:)

Continue(payment_id: ::SecureRandom.uuid) if response.success?
end
end
end
19 changes: 19 additions & 0 deletions examples/anti_corruption_layer/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require "bundler/inline"

$LOAD_PATH.unshift(__dir__)

gemfile do
source "https://rubygems.org"

gem "solid-result", "~> 2.0"
gem "solid-adapters", path: "../../"
end

require "vendor/pay_friend/client"
require "vendor/circle_up/client"

require "lib/payment_gateways"

require "app/models/payment/charge_credit_card"
11 changes: 11 additions & 0 deletions examples/anti_corruption_layer/lib/payment_gateways.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module PaymentGateways
require_relative 'payment_gateways/contract'
require_relative 'payment_gateways/response'

module Adapters
require_relative 'payment_gateways/adapters/circle_up'
require_relative 'payment_gateways/adapters/pay_friend'
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module PaymentGateways
class Adapters::CircleUp
attr_reader :client

def initialize
@client = ::CircleUp::Client.new
end

def charge_credit_card(params)
params => { amount:, details: }

response = client.charge_cc(amount, details)

Response.new(response.ok?)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module PaymentGateways
class Adapters::PayFriend
attr_reader :client

def initialize
@client = ::PayFriend::Client.new
end

def charge_credit_card(params)
params => { amount:, details: }

response = client.charge(amount:, payment_data: details, payment_method: 'credit_card')

Response.new(response.status == 'success')
end
end
end
15 changes: 15 additions & 0 deletions examples/anti_corruption_layer/lib/payment_gateways/contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module PaymentGateways
class Contract < ::Solid::Adapters::Proxy
def charge_credit_card(params)
params => { amount: Numeric, details: Hash }

outcome = object.charge_credit_card(params)

outcome => Response[true | false]

outcome
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

module PaymentGateways
Response = ::Struct.new(:success?)
end
11 changes: 11 additions & 0 deletions examples/anti_corruption_layer/vendor/circle_up/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module CircleUp
class Client
Resp = ::Struct.new(:ok?)

def charge_cc(_amount, _credit_card_data)
Resp.new(true)
end
end
end
Loading

0 comments on commit 26e7609

Please sign in to comment.