일반 읽기 모델 API 탐색

Arkency 전자상거래는 CQRS 패턴을 따르고 "읽기"를 위한 별도의 모듈을 생성합니다.

우리는 그것들을 읽기 모델이라고 부릅니다.

가장 간단한 읽기 모델은 다음으로 구성됩니다.
  • 하나 이상의 데이터베이스 테이블
  • 테이블의 열에 이벤트를 매핑하는 일부 구성 코드

  • 오늘은 읽기 모델을 만들려고 했습니다. 고객에게 제공되는 제품의 공개 제안.

    관리자/판매 패널에서 사용되는 읽기 모델Products이 이미 있습니다.

    처음에는 거의 같은 것 같고 그냥 재사용하고 싶은 유혹이 듭니다.

    이것은 CQRS에서 종종 유혹입니다. 이러한 유혹은 CRUD 시스템에서 명백합니다. 제품을 검색하는 한 가지 방법이 있습니다. 얼마나 복잡할까요?

    그러나 간단한 기능 세트에서도 서로 다른 열을 포함하고 있음을 이미 볼 수 있습니다.

    그들은 다른 필요를 충족시킵니다.

    그것을 재사용함으로써 나는 정말로 무엇을 절약하고 있는가?

    유일한 위험은 가격을 표시하기 위해 몇 가지 계산을 시작하는 경우(가능성이 매우 높지만 읽기 모델의 일부입니까?) 일부를 잊어버릴 수 있다는 것입니다.

    그래도.

    현재 읽기 모델을 살펴보았습니다.

    module Products
      class Product < ApplicationRecord
        self.table_name = "products"
      end
    
      class Configuration
        def call(cqrs)
          cqrs.subscribe(
            -> (event) { register_product(event) },
            [ProductCatalog::ProductRegistered]
          )
          cqrs.subscribe(
            ->(event) { change_stock_level(event) },
            [Inventory::StockLevelChanged]
          )
          cqrs.subscribe(
            -> (event) { set_price(event) },
            [Pricing::PriceSet])
          cqrs.subscribe(
            -> (event) { set_vat_rate(event) },
            [Taxes::VatRateSet])
        end
    
        private
    
        def register_product(event)
          Product.create(id: event.data.fetch(:product_id), name: event.data.fetch(:name))
        end
    
        def set_price(event)
          find(event.data.fetch(:product_id)).update_attribute(:price, event.data.fetch(:price))
        end
    
        def set_vat_rate(event)
          find(event.data.fetch(:product_id)).update_attribute(:vat_rate_code, event.data.fetch(:vat_rate).fetch(:code))
        end
    
        def change_stock_level(event)
          find(event.data.fetch(:product_id)).update_attribute(:stock_level, event.data.fetch(:stock_level))
        end
    
        def find(product_id)
          Product.where(id: product_id).first
        end
      end
    end
    
    


    나는 당신에 관한 것이 아니지만 그것은 나에게 비명을 지른다.

    THIS CODE IS MOSTLY DATA
    


    기본적으로 약간의 미묘함이 있는 매핑 선언입니다. 모두 ActiveRecord 위에 있습니다.

    db 스키마를 보여주기 위해:

      create_table "products", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
        t.string "name"
        t.decimal "price", precision: 8, scale: 2
        t.integer "stock_level"
        t.datetime "registered_at", precision: nil
        t.string "vat_rate_code"
      end
    


    (그리고 registered_at 열이 어떻게든 마법처럼 사용되었는지 또는 삭제할 수 있는지 확인하기 위해 TODO에 추가하고 있습니다.)

    그런 다음 테스트 방법을 살펴 보았습니다.

    우리는 도메인 수준에서 거의 100% 돌연변이 적용 ​​범위를 유지하지만 앱 수준(컨트롤러, 읽기 모델)에서는 유사한 품질을 공유하지 않습니다.

    이 읽기 모델에는 단위 테스트가 없다는 것을 알았습니다. 통합 테스트를 통해서만 테스트됩니다. 그거 슬프다. 나는 이것을 개선할 필요가 있다.

    그러나 두 번째 생각.

    일부 기계적 리팩토링(전체 테스트 범위 없이 수행할 수 있는 것)을 수행하면 이 코드는 대부분 선언 또는 일종의 DSL API가 됩니다.

    실제로 발생하면 사용 위치 대신 추출되는 결과 "프레임워크"가 필요합니다.

    선언적 코드 테스트의 문제는 일반적으로 프로덕션 코드에 있는 내용을 반복하는 "typotest"라는 것입니다.

    간단히 말해, 이것은 코드를 더 선언적으로 만들기 위한 첫 번째 시도입니다.

    module Products
      class Product < ApplicationRecord
        self.table_name = "products"
      end
    
      class Configuration
        def initialize(cqrs)
          @cqrs = cqrs
        end
    
        def call
          @cqrs.subscribe(-> (event) { register_product(event) }, [ProductCatalog::ProductRegistered])
          copy(Inventory::StockLevelChanged, :stock_level)
          copy(Pricing::PriceSet,            :price)
          copy_nested_to_column(Taxes::VatRateSet, :vat_rate, :code, :vat_rate_code)
        end
    


    그 두 줄은 나에게 매우 우아하지만:

    copy(Inventory::StockLevelChanged, :stock_level)
    copy(Pricing::PriceSet,            :price)
    


    (우연히) 이벤트 속성을 열 이름과 일치시키는 규칙을 따르기 때문에 멋져 보입니다.

    다른 라인은 여전히 ​​약간의 사랑이 필요합니다.

    첫째, 여기에서 레코드를 생성하려면 이름이 필요하며 빈 생성자가 아닙니다. 이 코드의 일반화를 찾기가 어려웠습니다.

    마지막 줄은 엣지 케이스였습니다. 이벤트에 데이터가 중첩되어 있는 경우가 있습니다. 또한 열 이름과 일치하지 않습니다.

    copy_nested_to_column(Taxes::VatRateSet, :vat_rate, :code, :vat_rate_code)
    


    다음은 모두 작동하도록 지원하는 나머지 코드입니다.

        private
    
        def copy(event, attribute)
          @cqrs.subscribe(-> (event) { copy_event_attribute_to_column(event, attribute, attribute) }, [event])
        end
    
        def copy_nested_to_column(event, top_event_attribute, nested_attribute, column)
          @cqrs.subscribe(
            -> (event) { copy_nested_event_attribute_to_column(event, top_event_attribute, nested_attribute, column) }, [event])
        end
    
        def register_product(event)
          Product.create(id: event.data.fetch(:product_id), name: event.data.fetch(:name))
        end
    
        def copy_event_attribute_to_column(event, event_attribute, column)
          product(event).update_attribute(column, event.data.fetch(event_attribute))
        end
    
        def copy_nested_event_attribute_to_column(event, top_event_attribute, nested_attribute, column)
          product(event).update_attribute(column, event.data.fetch(top_event_attribute).fetch(nested_attribute))
        end
    
        def product(event)
          find(event.data.fetch(:product_id))
        end
    
        def find(product_id)
          Product.where(id: product_id).first
        end
    


    이 코드는 여전히 더 일반적이어야 합니다.
    ActiveRecord 클래스 이름Product을 하드코딩 해제해야 합니다.

    그리고 당신을 상기시키기 위해. 나의 초기 목표는 새로운 읽기 모델을 만드는 것이었습니다. 여기서 제가 하는 일은 예비 리팩토링입니다. 그런 다음 몇 줄의 선언만으로 새로운 읽기 모델이 구현될 것으로 기대합니다.

    우리는 그것이 어떻게되는지 볼 것입니다 :)

    이러한 변경 사항이 포함된 커밋은 다음과 같습니다.
    https://github.com/RailsEventStore/ecommerce/commit/9e42950fc7eb34257938b0501fa6e50733d0d568

    읽어주셔서 감사합니다 ❤️

    좋은 웹페이지 즐겨찾기