Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การเขียนโปรแกรมเชิงรถไฟในรางโดยใช้ Dry-Monads

การจัดการข้อผิดพลาดเป็นส่วนสำคัญของทุกโปรแกรม สิ่งสำคัญคือต้องดำเนินการเชิงรุกเกี่ยวกับข้อผิดพลาดที่อาจเกิดขึ้นระหว่างการนำโค้ดไปใช้ ข้อผิดพลาดเหล่านี้ควรได้รับการจัดการในลักษณะที่ทำให้มั่นใจว่ามีการผลิตเอาต์พุตที่อธิบายข้อผิดพลาดแต่ละข้อและขั้นตอนของแอปพลิเคชันที่เกิดขึ้นอย่างเหมาะสม อย่างไรก็ตาม การทำสิ่งนี้ให้สำเร็จในลักษณะที่มั่นใจว่าโค้ดของคุณยังคงทำงานและอ่านได้ก็เป็นสิ่งสำคัญเช่นกัน เริ่มต้นด้วยการตอบคำถามที่คุณอาจมีอยู่แล้ว:Railway Oriented Programming คืออะไร

การเขียนโปรแกรมเชิงรถไฟ

ฟังก์ชันที่บรรลุวัตถุประสงค์เฉพาะอาจเป็นองค์ประกอบของฟังก์ชันที่เล็กกว่า ฟังก์ชันเหล่านี้ดำเนินการตามขั้นตอนต่างๆ ที่นำไปสู่การบรรลุเป้าหมายสุดท้ายในที่สุด ตัวอย่างเช่น ฟังก์ชันที่อัปเดตที่อยู่ของผู้ใช้ในฐานข้อมูลและแจ้งให้ผู้ใช้ทราบการเปลี่ยนแปลงนี้ในภายหลัง อาจประกอบด้วยขั้นตอนต่อไปนี้:

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

แต่ละขั้นตอนเหล่านี้อาจล้มเหลวหรือสำเร็จก็ได้ และความล้มเหลวของขั้นตอนใดๆ นำไปสู่ความล้มเหลวของกระบวนการทั้งหมด เนื่องจากวัตถุประสงค์ของฟังก์ชันไม่บรรลุผล

การเขียนโปรแกรมเชิงรถไฟ (ROP) เป็นคำที่คิดค้นโดย Scott Wlaschin ที่ใช้การเปรียบเทียบสวิตช์รางรถไฟกับการจัดการข้อผิดพลาดในฟังก์ชันเช่นนี้ สวิตช์รถไฟ (เรียกว่า "จุด" ในสหราชอาณาจักร) นำทางรถไฟจากรางหนึ่งไปยังอีกรางหนึ่ง สกอตต์ใช้ความคล้ายคลึงนี้ในแง่ที่ว่าผลลัพธ์ของความสำเร็จ/ความล้มเหลวของแต่ละขั้นตอนจะทำหน้าที่เหมือนกับสวิตช์รางรถไฟ เนื่องจากสามารถย้ายคุณไปยังเส้นทางแห่งความสำเร็จหรือเส้นทางที่ล้มเหลวได้

การเขียนโปรแกรมเชิงรถไฟในรางโดยใช้ Dry-Monads ผลลัพธ์ความสำเร็จ/ความล้มเหลวที่ทำหน้าที่เป็นสวิตช์รางรถไฟ

เมื่อมีข้อผิดพลาดในขั้นตอนใดๆ เราจะถูกย้ายไปยังแทร็กความล้มเหลวโดยเอาต์พุตของความล้มเหลว ดังนั้นจึงข้ามขั้นตอนที่เหลือ อย่างไรก็ตาม เมื่อผลลัพธ์สำเร็จ ผลลัพธ์จะเชื่อมต่อกับอินพุตของขั้นตอนต่อไป เพื่อช่วยนำเราไปยังจุดหมายสุดท้ายดังที่แสดงในภาพด้านล่าง

การเขียนโปรแกรมเชิงรถไฟในรางโดยใช้ Dry-Monads ผลสำเร็จ/ล้มเหลวของหลายขั้นตอนที่เชื่อมโยงเข้าด้วยกัน

การเปรียบเทียบแบบสองทางนี้เป็นแนวคิดเบื้องหลังการเขียนโปรแกรมเชิงรถไฟ จะพยายามส่งคืนผลลัพธ์ความสำเร็จ/ความล้มเหลวเหล่านี้ในทุกขั้นตอน (เช่น ทุกวิธีที่เป็นส่วนหนึ่งของกระบวนการ) เพื่อให้แน่ใจว่าความล้มเหลวในขั้นตอนเดียวเป็นความล้มเหลวของกระบวนการทั้งหมด ความสำเร็จในแต่ละขั้นตอนเท่านั้นที่นำไปสู่ความสำเร็จโดยรวม

ตัวอย่างชีวิตประจำวัน

ลองนึกภาพว่าคุณมีเป้าหมายที่จะซื้อกล่องนมด้วยตนเองจากร้านที่ชื่อว่า The Milk Shakers . ขั้นตอนที่เป็นไปได้ที่เกี่ยวข้องจะเป็นดังนี้:

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

หากคุณออกจากบ้านไม่ได้ กระบวนการทั้งหมดก็ล้มเหลวเพราะขั้นตอนแรกคือความล้มเหลว เกิดอะไรขึ้นถ้าคุณออกจากบ้านและไปที่ Walmart? กระบวนการนี้ยังคงล้มเหลวเนื่องจากคุณไม่ได้ไปที่ร้านค้าที่กำหนด ความจริงที่ว่าคุณสามารถรับนมจาก Walmart ไม่ได้หมายความว่ากระบวนการนี้จะดำเนินต่อไป ROP หยุดกระบวนการที่ Walmart และส่งคืนผลลัพธ์ความล้มเหลวเพื่อแจ้งให้คุณทราบว่ากระบวนการล้มเหลวเนื่องจากร้านค้าไม่ใช่ The Milk Shakers อย่างไรก็ตาม หากคุณไปที่ร้านค้าที่ถูกต้อง กระบวนการก็จะดำเนินต่อไป ตรวจสอบผลลัพธ์และสิ้นสุดกระบวนการหรือดำเนินการในขั้นตอนต่อไป วิธีนี้ช่วยให้จัดการข้อผิดพลาดที่อ่านง่ายและสวยงามยิ่งขึ้น และทำสำเร็จอย่างมีประสิทธิภาพโดยไม่ต้องใช้ if/else และ return ข้อความเชื่อมโยงแต่ละขั้นตอน

ใน Rails เราสามารถบรรลุผลลัพธ์ทางรถไฟสองทางนี้โดยใช้อัญมณีที่เรียกว่า Dry Monads .

ความรู้เบื้องต้นเกี่ยวกับ Monad แบบแห้งและวิธีการทำงาน

Monads เดิมเป็นแนวคิดทางคณิตศาสตร์ โดยพื้นฐานแล้ว สิ่งเหล่านี้เป็นองค์ประกอบหรือนามธรรมของฟังก์ชันพิเศษหลายอย่าง ซึ่งเมื่อใช้ในโค้ด จะสามารถขจัดการจัดการค่า stateful ได้อย่างชัดเจน นอกจากนี้ยังสามารถเป็นนามธรรมของรหัสสำเร็จรูปที่คำนวณได้ซึ่งจำเป็นโดยตรรกะของโปรแกรม ค่า Stateful ไม่ได้อยู่ภายในฟังก์ชันเฉพาะ ตัวอย่างบางส่วน ได้แก่ อินพุต ตัวแปรส่วนกลาง และเอาต์พุต Monads มีฟังก์ชันผูกที่ทำให้ค่าเหล่านี้สามารถส่งผ่านจาก Monad หนึ่งไปยังอีกค่าหนึ่งได้ ดังนั้นจึงไม่มีการจัดการอย่างชัดเจน สิ่งเหล่านี้สามารถสร้างขึ้นเพื่อจัดการกับข้อยกเว้น การคอมมิตย้อนกลับ ตรรกะการลองใหม่ ฯลฯ คุณสามารถหาข้อมูลเพิ่มเติมเกี่ยวกับ monads ได้ที่นี่

ตามที่ระบุไว้ในเอกสารประกอบ ซึ่งฉันแนะนำให้คุณตรวจสอบ monads แห้งคือชุดของ monads ทั่วไปสำหรับ Ruby Monads มอบวิธีที่สง่างามในการจัดการข้อผิดพลาด ข้อยกเว้น และฟังก์ชันการโยง เพื่อให้โค้ดเข้าใจได้ง่ายขึ้นและมีการจัดการข้อผิดพลาดที่ต้องการทั้งหมดโดยไม่ต้องใช้ ifs and elses ทั้งหมด . เราจะมุ่งเน้นไปที่ ผลลัพธ์ Monad เนื่องจากเป็นสิ่งที่เราต้องการเพื่อให้บรรลุผลสำเร็จ/ล้มเหลวที่เราพูดถึงก่อนหน้านี้

มาเริ่มแอป Rails ใหม่ที่ชื่อแอป Railway โดยใช้คำสั่งต่อไปนี้:

rails new railway-app -T

-T ในคำสั่งหมายความว่าเราจะข้ามโฟลเดอร์ทดสอบเนื่องจากเราต้องการใช้ RSpec สำหรับการทดสอบ

ต่อไป เราเพิ่มอัญมณีที่จำเป็นลงใน Gemfile ของเรา:gem dry-monads เพื่อความสำเร็จ/ล้มเหลวและ gem rspec-rails ในกลุ่มทดสอบและพัฒนาเป็นกรอบการทดสอบของเรา ตอนนี้ เราสามารถเรียกใช้ bundle install ในแอพของเราเพื่อติดตั้งอัญมณีที่เพิ่มเข้ามา ในการสร้างไฟล์ทดสอบและตัวช่วย เราต้องเรียกใช้คำสั่งต่อไปนี้:

rails generate rspec:install

แบ่งฟังก์ชันออกเป็นหลายขั้นตอน

ขอแนะนำให้แบ่งหน้าที่ของคุณออกเป็นวิธีการที่เล็กกว่าซึ่งทำงานร่วมกันเพื่อบรรลุเป้าหมายสุดท้ายของคุณเสมอ ข้อผิดพลาดจากวิธีการเหล่านี้ (หากมี) ช่วยให้เราระบุได้อย่างชัดเจนว่ากระบวนการของเราล้มเหลวที่ใด และทำให้รหัสของเราสะอาดและอ่านได้ เพื่อให้ง่ายที่สุด เราจะสร้างกลุ่มตัวแทนจำหน่ายรถยนต์โตโยต้าที่ส่งมอบรถให้กับผู้ใช้ หากมีรุ่นและสีที่ต้องการ หากไม่ใช่ปีที่ผลิตก่อนปี 2000 และเมืองที่จะจัดส่งให้อยู่ในรายชื่อเมืองใกล้เคียง นี้ควรจะน่าสนใจ :)

เริ่มต้นด้วยการแบ่งขั้นตอนการจัดส่งออกเป็นหลายขั้นตอน:

  • ตรวจสอบว่าปีที่ผลิตไม่ใช่ก่อนปี 2000
  • ตรวจสอบว่าโมเดลพร้อมใช้งาน
  • ตรวจสอบว่ามีสีหรือไม่
  • ตรวจสอบว่าเมืองที่จะจัดส่งเป็นเมืองใกล้เคียง
  • ส่งข้อความแจ้งว่ารถจะมาส่ง

ตอนนี้เราได้จัดการขั้นตอนต่างๆ กันแล้ว มาดำดิ่งลงไปในโค้ดกัน

การป้อนผลลัพธ์ที่สำเร็จ/ล้มเหลว

ในโฟลเดอร์แอป/รุ่น มาสร้างไฟล์ชื่อ car_dealership.rb และเริ่มต้นคลาสนี้ด้วยรายละเอียดที่สำคัญ ที่ด้านบนของไฟล์ เราต้องกำหนดให้ dry/monads และหลังชื่อคลาส เราต้องใส่ DryMonads[:result, :do] . สิ่งนี้ทำให้ monad ผลลัพธ์และสัญกรณ์ do (ซึ่งทำให้เราสามารถรวมการดำเนินการ monadic หลายรายการโดยใช้คำที่ให้ผล) ได้

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 เมธอด ซึ่งจะประกอบด้วยขั้นตอนอื่นๆ ที่เกี่ยวข้องและส่งคืนข้อความแสดงความสำเร็จหากขั้นตอนทั้งหมดสำเร็จ เราเพิ่มคำแสดงผลลัพธ์เพื่อรวมหรือผูกขั้นตอนเหล่านี้เข้าด้วยกัน ซึ่งหมายความว่าข้อความแสดงความล้มเหลวในขั้นตอนใดๆ เหล่านี้จะกลายเป็นข้อความแสดงความล้มเหลวของ 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 กระบวนการ. คุณยังสามารถเพิ่มการตรวจสอบอื่นๆ ในโค้ดของคุณสำหรับสถานการณ์ที่มีการระบุอาร์กิวเมนต์ที่ไม่ถูกต้อง (เช่น สตริงถูกจัดเตรียมเป็นค่าสำหรับตัวแปร year และอื่นๆ ที่คล้ายกัน) กำลังเรียกใช้ bundle exec rspec ในเทอร์มินัลของคุณจะทำการทดสอบและแสดงว่าการทดสอบทั้งหมดผ่าน โดยพื้นฐานแล้วคุณไม่จำเป็นต้องตรวจสอบผลการทดสอบความล้มเหลวและความสำเร็จพร้อมกัน เนื่องจากเราไม่สามารถมีทั้งสองอย่างเป็นผลลัพธ์ของวิธีการได้ ฉันได้เพิ่มเข้าไปเพื่อช่วยให้เข้าใจว่าผลลัพธ์ของความสำเร็จจะเป็นอย่างไรเมื่อเรามีผลความล้มเหลวและในทางกลับกัน

บทสรุป

นี่เป็นเพียงข้อมูลเบื้องต้นเกี่ยวกับ dry-monads และวิธีที่มันสามารถนำมาใช้ในแอปของคุณเพื่อให้เกิดการเขียนโปรแกรมเชิงรถไฟ ความเข้าใจพื้นฐานเกี่ยวกับสิ่งนี้สามารถนำไปใช้กับการดำเนินการและธุรกรรมที่ซับซ้อนมากขึ้นได้ ดังที่เราได้เห็นแล้วว่า โค้ดที่สะอาดและอ่านง่ายขึ้นไม่เพียงแต่สามารถทำได้โดยใช้ ROP เท่านั้น แต่การจัดการข้อผิดพลาดยังมีรายละเอียดและเครียดน้อยลงอีกด้วย โปรดอย่าลืมแนบข้อความแสดงความล้มเหลว/ความสำเร็จโดยย่อกับวิธีการต่างๆ ที่ประกอบเป็นกระบวนการของคุณ เนื่องจากวิธีการนี้จะช่วยในการระบุว่าข้อผิดพลาดเกิดขึ้นที่ใดและเพราะเหตุใด หากคุณต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับ ROP เราขอแนะนำให้ดูการนำเสนอนี้โดย Scott Wlaschin