Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Dry-Monads를 사용하는 레일에서의 철도 지향 프로그래밍

오류 처리는 모든 프로그램의 중요한 부분입니다. 코드를 구현하는 과정에서 발생할 수 있는 오류에 대해 사전에 예방하는 것이 중요합니다. 이러한 오류는 각 오류와 해당 오류가 발생한 응용 프로그램의 단계를 적절하게 설명하는 출력이 생성되도록 하는 방식으로 처리되어야 합니다. 그럼에도 불구하고 코드가 계속 작동하고 읽을 수 있도록 보장하는 방식으로 이를 달성하는 것도 중요합니다. 이미 가지고 있을 수 있는 질문에 답하는 것으로 시작하겠습니다. 철도 지향 프로그래밍이란 무엇입니까?

철도 지향 프로그래밍

특정 목적을 달성하는 기능은 더 작은 기능으로 구성될 수 있습니다. 이러한 기능은 궁극적으로 최종 목표를 달성하는 여러 단계를 수행합니다. 예를 들어, 데이터베이스에서 사용자의 주소를 업데이트하고 이 변경 사항을 사용자에게 알리는 기능은 다음 단계로 구성될 수 있습니다.

validate user -> update address -> send mail upon successful update

이러한 각 단계는 실패하거나 성공할 수 있으며, 어떤 단계라도 실패하면 기능의 목적이 달성되지 않아 전체 프로세스가 실패하게 됩니다.

철도 지향 프로그래밍(ROP)은 Scott Wlaschin이 발명한 용어입니다. 이는 철도 스위치 비유를 이와 같은 기능의 오류 처리에 적용합니다. 철도 스위치(영국에서는 "포인트"라고 함)는 한 트랙에서 다른 트랙으로 기차를 안내합니다. Scott은 각 단계의 성공/실패 출력이 마치 철도 스위치처럼 작동한다는 점에서 이 비유를 사용하여 성공 트랙이나 실패 트랙으로 이동할 수 있습니다.

Dry-Monads를 사용하는 레일에서의 철도 지향 프로그래밍 철도 스위치 역할을 하는 성공/실패 출력

단계 중 하나라도 오류가 발생하면 실패 출력에 의해 실패 트랙으로 이동하여 나머지 단계를 건너뜁니다. 그러나 성공 출력이 있을 경우 아래 이미지와 같이 최종 목적지로 안내하는 데 도움이 되도록 다음 단계의 입력에 연결됩니다.

Dry-Monads를 사용하는 레일에서의 철도 지향 프로그래밍 함께 연결된 여러 단계의 성공/실패 출력

이 2트랙 비유는 철도 지향 프로그래밍의 배경입니다. 한 단계의 실패가 전체 프로세스의 실패임을 보장하기 위해 모든 단계(즉, 프로세스의 일부인 모든 방법)에서 이러한 성공/실패 출력을 반환하려고 노력합니다. 각 단계를 성공적으로 완료해야만 전반적인 성공으로 이어집니다.

일상 생활의 예

The Milk Shakers라는 매장에서 직접 우유 한 상자를 구매하려는 목표가 있다고 가정해 보겠습니다. . 관련 단계는 다음과 같습니다.

Leave your house -> Arrive at The Milk Shakers -> Pick up a carton of milk -> Pay for the carton of milk

집밖으로 나갈 수 없다면 첫걸음이 실패이기 때문에 모든 과정이 실패다. 집에서 나와 월마트에 간다면? 지정된 상점에 가지 않았기 때문에 프로세스는 여전히 실패입니다. Walmart에서 우유를 구입할 수 있다고 해서 그 과정이 계속되는 것은 아닙니다. ROP는 Walmart에서 프로세스를 중지하고 상점이 The Milk Shakers가 아니었기 때문에 프로세스가 실패했음을 알려주는 실패 출력을 반환합니다. 그러나 올바른 상점에 갔다면 출력을 확인하고 프로세스를 종료하거나 다음 단계로 진행하는 프로세스가 계속되었을 것입니다. 이것은 보다 읽기 쉽고 우아한 오류 처리를 보장하고 if/else 없이도 이를 효율적으로 달성합니다. 및 return 개별 단계를 연결하는 진술.

Rails에서는 Dry Monads라는 보석을 사용하여 이 2트랙 철도 출력을 얻을 수 있습니다. .

드라이 모나드 및 작동 원리 소개

모나드는 원래 수학적 개념이었습니다. 기본적으로 코드에서 사용할 때 상태 저장 값의 명시적 처리를 제거할 수 있는 여러 특수 기능의 합성 또는 추상화입니다. 또한 프로그램 논리에 필요한 계산 상용구 코드의 추상화일 수도 있습니다. 상태 저장 값은 특정 함수에 국한되지 않으며 몇 가지 예에는 입력, 전역 변수 및 출력이 있습니다. 모나드는 이러한 값을 한 모나드에서 다른 모나드로 전달할 수 있도록 하는 바인드 함수를 포함합니다. 따라서 명시적으로 처리되지 않습니다. 예외, 롤백 커밋, 재시도 논리 등을 처리하도록 빌드할 수 있습니다. 여기에서 모나드에 대한 자세한 정보를 찾을 수 있습니다.

내가 검토할 것을 권하는 문서에 명시된 대로, 건식 모나드는 Ruby의 공통 모나드 집합입니다. 모나드는 오류, 예외 및 연결 함수를 처리하는 우아한 방법을 제공하여 코드를 훨씬 더 이해하기 쉽고 모든 ifs and elses 없이 원하는 모든 오류 처리를 갖습니다. . 우리는 결과 모나드에 집중할 것입니다. 앞서 이야기한 성공/실패 출력을 달성하기 위해 정확히 필요한 것이기 때문입니다.

다음 명령을 사용하여 Railway-app이라는 새 Rails 앱을 시작하겠습니다.

rails new railway-app -T

명령에서 -T는 테스트에 RSpec을 사용할 예정이므로 테스트 폴더를 건너뛸 것임을 의미합니다.

다음으로 Gemfile에 필요한 gem을 추가합니다. gem dry-monads 성공/실패 결과 및 gem rspec-rails 테스트 및 개발 그룹에서 테스트 프레임워크로 사용합니다. 이제 bundle install를 실행할 수 있습니다. 추가된 보석을 설치하려면 앱에서 하지만 테스트 파일과 도우미를 생성하려면 다음 명령을 실행해야 합니다.

rails generate rspec:install

함수를 여러 단계로 나누기

최종 목표를 달성하기 위해 함께 작동하는 더 작은 방법으로 기능을 나누는 것이 항상 권장됩니다. 이러한 방법의 오류가 있는 경우 프로세스가 실패한 위치를 정확히 식별하고 코드를 깨끗하고 읽기 쉽게 유지하는 데 도움이 됩니다. 이를 가능한 한 간단하게 만들기 위해 사용자에게 자동차를 배달하는 Toyota 자동차 대리점 클래스를 구성합니다. 원하는 모델과 색상이 있는 경우, 제조 연도가 2000년 이전이 아닌 경우, 배송할 도시가 인근 도시 목록에 있는 경우. 이것은 흥미로울 것입니다. :)

배송 프로세스를 여러 단계로 나누어 시작하겠습니다.

  • 제조 연도가 2000년 이전이 아닌지 확인하십시오.
  • 모델을 사용할 수 있는지 확인합니다.
  • 색상을 사용할 수 있는지 확인합니다.
  • 배송할 도시가 인근 도시인지 확인합니다.
  • 자동차가 배달될 것이라는 메시지를 보냅니다.

이제 다른 단계가 해결되었으므로 코드를 살펴보겠습니다.

성공/실패 결과 입력

앱/모델 폴더에 car_dealership.rb라는 파일을 만들어 보겠습니다. 중요한 세부 사항으로 이 클래스를 초기화합니다. 파일 상단에 dry/monads가 필요합니다. , 그리고 클래스 이름 바로 뒤에 DryMonads[:result, :do]를 포함해야 합니다. . 이렇게 하면 결과 모나드와 do 표기법(yield 단어를 사용하여 여러 모나드 연산의 조합을 가능하게 함)을 사용할 수 있습니다.

require 'dry/monads'

class CarDealership

include Dry::Monads[:result, :do]

  def initialize
    @available_models = %w[Avalon Camry Corolla Venza]
    @available_colors = %w[red black blue white]
    @nearby_cities = %w[Austin Chicago Seattle]
  end
end

다음으로 deliver_car를 추가합니다. 관련된 다른 모든 단계로 구성되며 모든 단계가 성공하면 성공 메시지를 반환합니다. 이러한 단계를 서로 결합하거나 바인딩하기 위해 yield 단어를 추가합니다. 이는 이러한 단계에서 실패 메시지가 deliver_car의 실패 메시지가 됨을 의미합니다. 메소드 및 이들 중 하나의 성공 출력은 목록의 다음 단계 호출로 이어집니다.

def deliver_car(year,model,color,city)
  yield check_year(year)
  yield check_model(model)
  yield check_city(city)
  yield check_color(color)

  Success("A #{color} #{year} Toyota #{model} will be delivered to #{city}")
end

이제 다른 모든 메소드를 추가하고 확인 결과에 따라 성공/실패 결과를 첨부해 보겠습니다.

def check_year(year)
  year < 2000 ? Failure("We have no cars manufactured in year #{year}") : Success('Cars of this year are available')
end

def check_model(model)
  @available_models.include?(model) ? Success('Model available') : Failure('The model requested is unavailable')
end
def check_color(color)
  @available_colors.include?(color) ? Success('This color is available') : Failure("Color #{color} is unavailable")
end

def check_city(city)
  @nearby_cities.include?(city) ? Success("Car deliverable to #{city}") : Failure('Apologies, we cannot deliver to this city')
end

현재 클래스와 필요한 모든 메서드가 있습니다. 어떻게 될까요? 이 클래스의 새 인스턴스를 만들고 deliver_car를 호출하여 알아보겠습니다. 다른 인수를 사용하는 메서드입니다.

good_dealer = CarDealership.new

good_dealer.deliver_car(1990, 'Venza', 'red', 'Austin')
#Failure("We have no cars manufactured in year 1990")

good_dealer.deliver_car(2005, 'Rav4', 'red', 'Austin')
#Failure("The model requested is unavailable")

good_dealer.deliver_car(2005, 'Venza', 'yellow', 'Austin')
#Failure("Color yellow is unavailable")

good_dealer.deliver_car(2000, 'Venza', 'red', 'Surrey')
#Failure("Apologies, we cannot deliver to this city")

good_dealer.deliver_car(2000, 'Avalon', 'blue', 'Austin')
#Success("A blue 2000 Toyota Avalon will be delivered to Austin")

위와 같이 delivery_car 메소드의 실패 결과는 실패하는 메소드에 따라 달라집니다. 해당 메소드의 실패는 실패가 되며, 모든 메소드가 성공하면 자신의 성공 결과를 반환합니다. 또한 이러한 단계는 deliver_car와 독립적으로 호출할 수도 있는 개별 메서드라는 것을 잊지 마십시오. 방법. 아래에 예가 나와 있습니다.

good_dealer.check_color('wine')
#Failure("Color wine is unavailable")

good_dealer.check_model('Camry')
#Success('Model available')

RSpec으로 테스트

위의 코드를 테스트하기 위해 spec 폴더로 이동하여 car_dealership_spec.rb 파일을 만듭니다. spec/models 경로에서 . 첫 번째 줄에는 'rails_helper'가 필요합니다. 먼저 실패의 컨텍스트에 대한 테스트를 작성한 다음 성공의 컨텍스트를 작성합니다.

require 'rails_helper'

describe CarDealership do
  describe "#deliver_car" don
    let(:toyota_dealer) { CarDealership.new }
    context "failure" do
      it "does not deliver a car with the year less than 2000" do
        delivery = toyota_dealer.deliver_car(1990, 'Venza', 'red', 'Austin')
        expect(delivery.success).to eq nil
        expect(delivery.failure).to eq 'We have no cars manufactured in  year 1990'
      end

       it "does not deliver a car with the year less than 2000" do
        delivery = toyota_dealer.deliver_car(2005, 'Venza', 'yellow', 'Austin')
        expect(delivery.success).to eq nil
        expect(delivery.failure).to eq 'Color yellow is unavailable'
      end
   end
 end
end

위와 같이 result.failure를 사용하여 실패 또는 성공 결과에 액세스할 수 있습니다. 또는 result.success . 성공의 경우 테스트는 다음과 같습니다.

context "success" do
  it "delivers a car when all conditions are met" do
    delivery = toyota_dealer.deliver_car(2000, 'Avalon', 'blue', 'Austin')
    expect(delivery.success).to eq 'A blue 2000 Toyota Avalon will be delivered to Austin'
    expect(delivery.failure).to eq nil
  end
end

이제 deliver_car에 제공된 인수를 조정하여 실패 컨텍스트에서 다른 테스트를 추가할 수 있습니다. 방법. 유효하지 않은 인수가 제공되는 상황에 대해 코드에 다른 검사를 추가할 수도 있습니다(예:문자열이 연도 변수 및 이와 유사한 다른 값으로 제공됨). bundle exec rspec 실행 터미널에서 테스트를 실행하고 모든 테스트가 통과했음을 보여줍니다. 기본적으로 테스트에서 실패와 성공 결과에 대한 검사를 동시에 추가할 필요가 없습니다. 메서드의 출력으로 둘 다 가질 수 없기 때문입니다. 실패 결과가 있을 때 또는 그 반대의 경우 성공 결과가 어떻게 보이는지 이해를 돕기 위해 추가했습니다.

결론

이것은 dry-monad에 대한 소개일 뿐이며 앱에서 철도 지향 프로그래밍을 달성하는 데 사용할 수 있는 방법입니다. 이에 대한 기본적인 이해는 보다 복잡한 작업 및 트랜잭션에 적용할 수 있습니다. 우리가 보았듯이, 더 깨끗하고 읽기 쉬운 코드는 ROP를 사용하여 달성할 수 있을 뿐만 아니라 오류 처리가 상세하고 스트레스가 적습니다. 이 접근 방식은 오류가 발생한 위치와 이유를 식별하는 데 도움이 되므로 프로세스를 구성하는 다양한 방법에 간결한 실패/성공 메시지를 첨부하는 것을 항상 기억하십시오. ROP에 대한 자세한 정보를 원하시면 Scott Wlaschin의 이 프레젠테이션을 시청하는 것이 좋습니다.