portaldacalheta.pt
  • หลัก
  • การจัดการวิศวกรรม
  • บุคลากรและทีมงานของผลิตภัณฑ์
  • อื่น ๆ
  • นวัตกรรม
แบ็คเอนด์

การสร้าง Ruby DSL: คำแนะนำสำหรับการเขียนโปรแกรมขั้นสูง



ภาษาเฉพาะโดเมน (DSL) เป็นเครื่องมือที่ทรงพลังอย่างไม่น่าเชื่อสำหรับการตั้งโปรแกรมหรือกำหนดค่าระบบที่ซับซ้อนได้ง่ายขึ้น นอกจากนี้ยังมีอยู่ทุกหนทุกแห่งในฐานะวิศวกรซอฟต์แวร์คุณมักจะใช้ DSL ต่างๆเป็นประจำทุกวัน

ในบทความนี้คุณจะได้เรียนรู้ว่าภาษาเฉพาะโดเมนคืออะไรควรใช้เมื่อใดและสุดท้ายจะสร้าง DSL ของคุณเองใน Ruby ได้อย่างไรโดยใช้เทคนิคการเขียนโปรแกรมเมตาขั้นสูง



บทความนี้สร้างจาก Nikola Todorovic’s บทนำ ไปยัง Ruby metaprogramming ซึ่งเผยแพร่บน ApeeScape Blog ดังนั้นหากคุณยังใหม่กับ metaprogramming อย่าลืมอ่านก่อน



ภาษาเฉพาะโดเมนคืออะไร?

คำจำกัดความทั่วไปของ DSL คือเป็นภาษาเฉพาะสำหรับโดเมนแอปพลิเคชันเฉพาะหรือกรณีการใช้งาน ซึ่งหมายความว่าคุณสามารถใช้สิ่งเหล่านี้สำหรับบางสิ่งเท่านั้นซึ่งไม่เหมาะสำหรับการพัฒนาซอฟต์แวร์ที่ใช้งานทั่วไป หากฟังดูกว้างนั่นก็เป็นเพราะ DSL มีหลายรูปทรงและขนาด หมวดหมู่ที่สำคัญบางส่วนมีดังนี้



  • ภาษามาร์กอัปเช่น HTML และ CSS ได้รับการออกแบบมาเพื่ออธิบายสิ่งที่เฉพาะเจาะจงเช่นโครงสร้างเนื้อหาและรูปแบบของหน้าเว็บ เป็นไปไม่ได้ที่จะเขียนอัลกอริทึมโดยพลการกับพวกเขาดังนั้นจึงเหมาะกับคำอธิบายของ DSL
  • มาโครและภาษาคิวรี (เช่น SQL) อยู่เหนือระบบใดระบบหนึ่งหรือภาษาโปรแกรมอื่นและโดยปกติจะมีข้อ จำกัด ในสิ่งที่ทำได้ ดังนั้นจึงเห็นได้ชัดว่ามีคุณสมบัติเป็นภาษาเฉพาะของโดเมน
  • DSL จำนวนมากไม่มีไวยากรณ์ของตัวเอง แต่ใช้ไวยากรณ์ของภาษาโปรแกรมที่กำหนดขึ้นด้วยวิธีที่ชาญฉลาดซึ่งให้ความรู้สึกเหมือนใช้มินิภาษาแยกต่างหาก

ประเภทสุดท้ายนี้เรียกว่าไฟล์ DSL ภายใน และเป็นหนึ่งในสิ่งเหล่านี้ที่เรากำลังจะสร้างเป็นตัวอย่างในเร็ว ๆ นี้ แต่ก่อนที่เราจะเข้าไปดูตัวอย่าง DSL ภายในที่เป็นที่รู้จักกันดี ไวยากรณ์นิยามเส้นทางใน Rails เป็นหนึ่งในนั้น:

Rails.application.routes.draw do root to: 'pages#main' resources :posts do get :preview resources :comments, only: [:new, :create, :destroy] end end

นี่คือรหัส Ruby แต่ให้ความรู้สึกเหมือนเป็นภาษากำหนดเส้นทางที่กำหนดเองมากขึ้นด้วยเทคนิคการเขียนโปรแกรมเมตาต่างๆที่ทำให้อินเทอร์เฟซที่สะอาดและใช้งานง่ายเป็นไปได้ สังเกตว่าโครงสร้างของ DSL ถูกนำไปใช้โดยใช้บล็อก Ruby และการเรียกเมธอดเช่น get และ resources ใช้สำหรับกำหนดคีย์เวิร์ดของมินิภาษานี้



Metaprogramming ถูกใช้อย่างมากในไลบรารีการทดสอบ RSpec:

describe UsersController, type: :controller do before do allow(controller).to receive(:current_user).and_return(nil) end describe 'GET #new' do subject { get :new } it 'returns success' do expect(subject).to be_success end end end

โค้ดชิ้นนี้ยังมีตัวอย่างสำหรับ อินเทอร์เฟซที่คล่องแคล่ว ซึ่งช่วยให้การประกาศสามารถอ่านออกเสียงเป็นประโยคภาษาอังกฤษธรรมดาได้ทำให้ง่ายต่อการเข้าใจว่าโค้ดกำลังทำอะไรอยู่:



# Stubs the `current_user` method on `controller` to always return `nil` allow(controller).to receive(:current_user).and_return(nil) # Asserts that `subject.success?` is truthy expect(subject).to be_success

อีกตัวอย่างหนึ่งของอินเทอร์เฟซที่คล่องแคล่วคืออินเทอร์เฟซแบบสอบถามของ ActiveRecord และ Arel ซึ่งใช้ไฟล์ ต้นไม้ไวยากรณ์นามธรรม ภายในสำหรับการสร้างแบบสอบถาม SQL ที่ซับซ้อน:

Post. # => select([ # SELECT Post[Arel.star], # `posts`.*, Comment[:id].count. # COUNT(`comments`.`id`) as('num_comments'), # AS num_comments ]). # FROM `posts` joins(:comments). # INNER JOIN `comments` # ON `comments`.`post_id` = `posts`.`id` where.not(status: :draft). # WHERE `posts`.`status` 'draft' where( # AND Post[:created_at].lte(Time.now) # `posts`.`created_at` <= ). # '2017-07-01 14:52:30' group(Post[:id]) # GROUP BY `posts`.`id`

แม้ว่าไวยากรณ์ที่ชัดเจนและแสดงออกของ Ruby พร้อมกับความสามารถในการเขียนโปรแกรม metaprogram ทำให้เหมาะอย่างยิ่งสำหรับการสร้างภาษาเฉพาะโดเมน DSL ก็มีอยู่ในภาษาอื่นเช่นกัน นี่คือตัวอย่างของการทดสอบ JavaScript โดยใช้กรอบจัสมิน:



describe('Helper functions', function() { beforeEach(function() { this.helpers = window.helpers; }); describe('log error', function() { it('logs error message to console', function() { spyOn(console, 'log').and.returnValue(true); this.helpers.log_error('oops!'); expect(console.log).toHaveBeenCalledWith('ERROR: oops!'); }); }); });

ไวยากรณ์นี้อาจไม่สะอาดเท่าตัวอย่าง Ruby แต่แสดงให้เห็นว่าด้วยการตั้งชื่อที่ชาญฉลาดและการใช้ไวยากรณ์อย่างสร้างสรรค์ DSL ภายในสามารถสร้างได้โดยใช้เกือบทุกภาษา

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



สิ่งที่เราต้องยอมแพ้ในทางกลับกันคือความอิสระทางวากยสัมพันธ์ - DSL ภายในจะต้องมีความถูกต้องทางวากยสัมพันธ์ในภาษาการนำไปใช้งาน คุณต้องประนีประนอมมากน้อยเพียงใดในเรื่องนี้ขึ้นอยู่กับภาษาที่เลือกเป็นส่วนใหญ่โดยใช้ verbose ภาษาที่พิมพ์แบบคงที่เช่น Java และ VB.NET อยู่ที่ปลายด้านหนึ่งของสเปกตรัมและภาษาไดนามิกที่มีความสามารถในการเขียนโปรแกรม metaprogram อย่างกว้างขวางเช่น Ruby บนอีก จบ.

วิธีแก้ไขหน่วยความจำรั่วใน java

การสร้างของเราเอง - Ruby DSL สำหรับการกำหนดค่าคลาส

DSL ตัวอย่างที่เรากำลังจะสร้างใน Ruby เป็นเครื่องมือกำหนดค่าที่ใช้ซ้ำได้สำหรับการระบุแอตทริบิวต์การกำหนดค่าของคลาส Ruby โดยใช้ไวยากรณ์ที่เรียบง่ายมาก การเพิ่มความสามารถในการกำหนดค่าให้กับคลาสเป็นข้อกำหนดทั่วไปในโลกของ Ruby โดยเฉพาะอย่างยิ่งเมื่อต้องกำหนดค่าอัญมณีภายนอกและไคลเอ็นต์ API วิธีแก้ปัญหาปกติคืออินเทอร์เฟซเช่นนี้:



MyApp.configure do |config| config.app_id = 'my_app' config.title = 'My App' config.cookie_name = 'my_app_session' end

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

เราต้องการอะไรเพื่อให้อินเทอร์เฟซนี้ทำงานได้? MyApp ชั้นเรียนควรมี configure เมธอดคลาสที่ใช้บล็อกแล้วรันบล็อกนั้นโดยยอมให้มันส่งผ่านอ็อบเจ็กต์คอนฟิกูเรชันที่มีเมธอด accessor สำหรับอ่านและเขียนค่าคอนฟิกูเรชัน:

class MyApp # ... class << self def config @config ||= Configuration.new end def configure yield config end end class Configuration attr_accessor :app_id, :title, :cookie_name end end

เมื่อบล็อกการกำหนดค่าทำงานเราสามารถเข้าถึงและแก้ไขค่าได้อย่างง่ายดาย:

MyApp.config => # MyApp.config.title => 'My App' MyApp.config.app_id = 'not_my_app' => 'not_my_app'

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

ทำให้สามารถนำกลับมาใช้ใหม่ได้

ในตอนนี้หากเราต้องการเพิ่มความสามารถในการกำหนดค่าที่คล้ายกันในคลาสอื่นเราจะต้องคัดลอกทั้ง Configuration คลาสและวิธีการตั้งค่าที่เกี่ยวข้องลงในคลาสอื่น ๆ รวมทั้งแก้ไข attr_accessor รายการเพื่อเปลี่ยนแอตทริบิวต์การกำหนดค่าที่ยอมรับ เพื่อหลีกเลี่ยงการทำเช่นนี้ให้ย้ายคุณลักษณะการกำหนดค่าไปไว้ในโมดูลแยกต่างหากที่เรียกว่า Configurable ด้วยเหตุนี้ MyApp ของเรา ชั้นเรียนจะมีลักษณะดังนี้:

class MyApp #BOLD include Configurable #BOLDEND # ... end

ทุกอย่างที่เกี่ยวข้องกับการกำหนดค่าถูกย้ายไปที่ Configurable โมดูล:

#BOLD module Configurable def self.included(host_class) host_class.extend ClassMethods end module ClassMethods #BOLDEND def config @config ||= Configuration.new end def configure yield config end #BOLD end #BOLDEND class Configuration attr_accessor :app_id, :title, :cookie_name end #BOLD end #BOLDEND

ที่นี่มีการเปลี่ยนแปลงไม่มากนักยกเว้นใหม่ self.included วิธี. เราต้องการวิธีนี้เนื่องจากการรวมโมดูลจะผสมในวิธีการอินสแตนซ์เท่านั้นดังนั้น config ของเรา และ configure เมธอดคลาสจะไม่ถูกเพิ่มลงในคลาสโฮสต์ตามค่าเริ่มต้น อย่างไรก็ตามหากเรากำหนดวิธีการพิเศษที่เรียกว่า included บนโมดูล Ruby จะเรียกมันเมื่อใดก็ตามที่โมดูลนั้นรวมอยู่ในคลาส เราสามารถขยายคลาสโฮสต์ได้ด้วยตนเองด้วยวิธีการใน ClassMethods:

รับเวลาเป็นมิลลิวินาที javascript
def self.included(host_class) # called when we include the module in `MyApp` host_class.extend ClassMethods # adds our class methods to `MyApp` end

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

class MyApp #BOLD include Configurable.with(:app_id, :title, :cookie_name) #BOLDEND # ... end

อาจจะค่อนข้างน่าแปลกใจที่โค้ดด้านบนนี้ถูกต้องตามหลักไวยากรณ์ - include ไม่ใช่คีย์เวิร์ด แต่เป็นเพียงวิธีการปกติที่ต้องใช้ Module วัตถุเป็นพารามิเตอร์ ตราบใดที่เราส่งนิพจน์ที่ส่งกลับ Module มันก็จะรวมมันไว้อย่างมีความสุข ดังนั้นแทนที่จะรวม Configurable โดยตรงเราต้องการวิธีการที่มีชื่อ with ที่สร้างโมดูลใหม่ที่ปรับแต่งด้วยแอตทริบิวต์ที่ระบุ:

module Configurable #BOLD def self.with(*attrs) #BOLDEND # Define anonymous class with the configuration attributes #BOLD config_class = Class.new do attr_accessor *attrs end #BOLDEND # Define anonymous module for the class methods to be 'mixed in' #BOLD class_methods = Module.new do define_method :config do @config ||= config_class.new end #BOLDEND def configure yield config end #BOLD end #BOLDEND # Create and return new module #BOLD Module.new do singleton_class.send :define_method, :included do |host_class| host_class.extend class_methods end end end #BOLDEND end

ที่นี่มีอะไรให้แกะเยอะมาก ทั้งหมด Configurable ตอนนี้โมดูลประกอบด้วย with วิธีการกับทุกสิ่งที่เกิดขึ้นภายในวิธีการนั้น ขั้นแรกเราสร้างคลาสที่ไม่ระบุชื่อใหม่ด้วย Class.new เพื่อเก็บวิธีการเข้าถึงแอตทริบิวต์ของเรา เพราะ Class.new ใช้นิยามคลาสเป็นบล็อกและบล็อกมีการเข้าถึงตัวแปรภายนอกเราสามารถส่งผ่าน attrs ตัวแปรเป็น attr_accessor ไม่มีปัญหา

def self.with(*attrs) # `attrs` is created here # ... config_class = Class.new do # class definition passed in as a block attr_accessor *attrs # we have access to `attrs` here end

ความจริงที่ว่าบล็อกใน Ruby สามารถเข้าถึงตัวแปรภายนอกได้ก็เป็นสาเหตุที่บางครั้งเรียกว่าบล็อกใน Ruby การปิด ตามที่รวมไว้หรือ 'ปิดทับ' สภาพแวดล้อมภายนอกที่กำหนดไว้โปรดทราบว่าฉันใช้วลี 'กำหนดใน' ไม่ใช่ 'ดำเนินการใน' ถูกต้อง - ไม่ว่าจะ define_method ของเราเมื่อใดและที่ไหน บล็อกจะถูกดำเนินการในที่สุดพวกเขาจะสามารถเข้าถึงตัวแปร config_class ได้เสมอ และ class_methods แม้ หลังจาก ที่ with วิธีการทำงานเสร็จสิ้นและส่งคืน ตัวอย่างต่อไปนี้แสดงให้เห็นถึงพฤติกรรมนี้:

def create_block foo = 'hello' # define local variable return Proc.new { foo } # return a new block that returns `foo` end block = create_block # call `create_block` to retrieve the block block.call # even though `create_block` has already returned, => 'hello' # the block can still return `foo` to us

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

config_class = # ... # `config_class` is defined here # ... class_methods = Module.new do # define new module using a block define_method :config do # method definition with a block @config ||= config_class.new # even two blocks deep, we can still end # access `config_class`

สุดท้ายเราเรียก Module.new เพื่อสร้างโมดูลที่เราจะกลับมา ที่นี่เราต้องกำหนด self.included ของเรา แต่น่าเสียดายที่เราไม่สามารถทำได้ด้วย def คีย์เวิร์ดเนื่องจากเมธอดต้องการเข้าถึงภายนอก class_methods ตัวแปร. ดังนั้นเราจึงต้องใช้ define_method ด้วยบล็อกอีกครั้ง แต่คราวนี้ในคลาสซิงเกิลตันของโมดูลขณะที่เรากำลังกำหนดวิธีการในอินสแตนซ์ของโมดูลเอง โอ้และตั้งแต่ define_method เป็นเมธอดส่วนตัวของคลาสซิงเกิลตันเราต้องใช้ send เพื่อเรียกใช้แทนการเรียกโดยตรง:

class_methods = # ... # ... Module.new do singleton_class.send :define_method, :included do |host_class| host_class.extend class_methods # the block has access to `class_methods` end end

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

class SomeClass include Configurable.with(:foo, :bar) # ... end SomeClass.configure do |config| config.foo = 'wat' config.bar = 'huh' end SomeClass.config.foo => 'wat'

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

การทำความสะอาดไวยากรณ์

มีสิ่งสุดท้ายที่ยังรบกวนฉันกับการใช้งานปัจจุบันของเรา - เราต้องทำซ้ำ config ในทุกบรรทัดในบล็อกการกำหนดค่า DSL ที่เหมาะสมจะรู้ว่าทุกสิ่งภายใน configure บล็อกควรดำเนินการในบริบทของวัตถุกำหนดค่าของเราและช่วยให้เราสามารถบรรลุสิ่งเดียวกันได้ด้วยสิ่งนี้:

MyApp.configure do app_id 'my_app' title 'My App' cookie_name 'my_app_session' end

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

module Configurable def self.with(*attrs) #BOLD not_provided = Object.new #BOLDEND config_class = Class.new do #BOLD attrs.each do |attr| define_method attr do |value = not_provided| if value === not_provided instance_variable_get('@#{attr}') else instance_variable_set('@#{attr}', value) end end end attr_writer *attrs #BOLDEND end class_methods = Module.new do # ... def configure(&block) #BOLD config.instance_eval(&block) #BOLDEND end end # Create and return new module # ... end end

การเปลี่ยนแปลงที่ง่ายกว่านี้คือการเรียกใช้ configure บล็อกในบริบทของวัตถุการกำหนดค่า เรียก Ruby's instance_eval วิธีการบนวัตถุช่วยให้คุณสามารถเรียกใช้บล็อกโค้ดโดยพลการราวกับว่ามันกำลังทำงานอยู่ภายในออบเจ็กต์นั้นซึ่งหมายความว่าเมื่อบล็อกการกำหนดค่าเรียกใช้ app_id วิธีการในบรรทัดแรกการเรียกนั้นจะไปที่อินสแตนซ์คลาสคอนฟิกูเรชันของเรา

การเปลี่ยนแปลงวิธีการเข้าถึงแอตทริบิวต์ใน config_class ซับซ้อนกว่าเล็กน้อย ในการทำความเข้าใจเราต้องเข้าใจก่อนว่า attr_accessor กำลังทำเบื้องหลัง ดำเนินการดังต่อไปนี้ attr_accessor ตัวอย่างเช่น:

class SomeClass attr_accessor :foo, :bar end

สิ่งนี้เทียบเท่ากับการกำหนดวิธีการอ่านและตัวเขียนสำหรับแต่ละแอตทริบิวต์ที่ระบุ:

class SomeClass def foo @foo end def foo=(value) @foo = value end # and the same with `bar` end

ดังนั้นเมื่อเราเขียน attr_accessor *attrs ในโค้ดดั้งเดิม Ruby ได้กำหนดวิธีการอ่านแอตทริบิวต์และวิธีการเขียนสำหรับเราสำหรับทุกแอตทริบิวต์ใน attrs - นั่นคือเรามีวิธีการเข้าถึงมาตรฐานดังต่อไปนี้: app_id, app_id=, title, title= และอื่น ๆ ในเวอร์ชันใหม่ของเราเราต้องการคงวิธีการเขียนมาตรฐานไว้เพื่อให้งานเช่นนี้ยังคงทำงานได้อย่างถูกต้อง:

MyApp.config.app_id = 'not_my_app' => 'not_my_app'

เราสามารถสร้างวิธีการเขียนโดยอัตโนมัติได้โดยเรียก attr_writer *attrs อย่างไรก็ตามเราไม่สามารถใช้วิธีการอ่านมาตรฐานได้อีกต่อไปเนื่องจากต้องสามารถเขียนแอตทริบิวต์เพื่อรองรับไวยากรณ์ใหม่นี้:

MyApp.configure do app_id 'my_app' # assigns a new value app_id # reads the stored value end

ในการสร้างวิธีการอ่านด้วยตัวเองเราวนซ้ำ attrs อาร์เรย์และกำหนดวิธีการสำหรับแต่ละแอตทริบิวต์ที่ส่งคืนค่าปัจจุบันของตัวแปรอินสแตนซ์ที่ตรงกันหากไม่มีการระบุค่าใหม่และเขียนค่าใหม่หากระบุไว้:

not_provided = Object.new # ... attrs.each do |attr| define_method attr do |value = not_provided| if value === not_provided instance_variable_get('@#{attr}') else instance_variable_set('@#{attr}', value) end end end

ที่นี่เราใช้ Ruby’s instance_variable_get วิธีการอ่านตัวแปรอินสแตนซ์ด้วยชื่อที่กำหนดเองและ instance_variable_set เพื่อกำหนดค่าใหม่ให้กับมัน น่าเสียดายที่ชื่อตัวแปรต้องขึ้นต้นด้วยเครื่องหมาย“ @” ในทั้งสองกรณีด้วยเหตุนี้การแก้ไขสตริง

คุณอาจสงสัยว่าทำไมเราต้องใช้วัตถุเปล่าเป็นค่าเริ่มต้นสำหรับ“ not provided” และทำไมเราไม่สามารถใช้ nil เพื่อจุดประสงค์นั้น เหตุผลนั้นง่ายมาก - nil เป็นค่าที่ถูกต้องซึ่งบางคนอาจต้องการตั้งค่าสำหรับแอตทริบิวต์การกำหนดค่า หากเราทดสอบ nil เราจะไม่สามารถแยกสองสถานการณ์นี้ออกจากกันได้:

MyApp.configure do app_id nil # expectation: assigns nil app_id # expectation: returns current value end

วัตถุว่างนั้นเก็บไว้ใน not_provided จะมีค่าเท่ากับตัวมันเองเท่านั้นดังนั้นวิธีนี้เราจึงมั่นใจได้ว่าจะไม่มีใครถ่ายทอดมันเข้าไปในวิธีการของเราและทำให้เกิดการอ่านโดยไม่ตั้งใจแทนที่จะเป็นการเขียน

การเพิ่มการสนับสนุนสำหรับการอ้างอิง

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

MyApp.configure do app_id 'my_app' title 'My App' cookie_name { '#{app_id}_session' } End MyApp.config.cookie_name => 'my_app_session'

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

SomeClass.configure do foo '#{bar}_baz' # expression evaluated here bar 'hello' end SomeClass.config.foo => '_baz' # not actually funny

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

SomeClass.configure do foo { '#{bar}_baz' } # stores block, does not evaluate it yet bar 'hello' end SomeClass.config.foo # `foo` evaluated here => 'hello_baz' # correct!

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

define_method attr do |value = not_provided, &block| if value === not_provided && block.nil? result = instance_variable_get('@#{attr}') result.is_a?(Proc) ? instance_eval(&result) : result else instance_variable_set('@#{attr}', block || value) end end

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

การอ้างอิงที่สนับสนุนมาพร้อมกับคำเตือนและกรณีขอบของตัวเองแน่นอน ตัวอย่างเช่นคุณอาจทราบได้ว่าจะเกิดอะไรขึ้นหากคุณอ่านแอตทริบิวต์ใด ๆ ในการกำหนดค่านี้:

SomeClass.configure do foo { bar } bar { foo } end

โมดูลสำเร็จรูป

ในท้ายที่สุดเรามีโมดูลที่ค่อนข้างเรียบร้อยสำหรับการสร้างคลาสที่กำหนดเองได้จากนั้นระบุค่าการกำหนดค่าเหล่านั้นโดยใช้ DSL ที่สะอาดและเรียบง่ายซึ่งช่วยให้เราอ้างอิงแอตทริบิวต์การกำหนดค่าหนึ่งรายการจากอีกรายการหนึ่ง:

class MyApp include Configurable.with(:app_id, :title, :cookie_name) # ... end SomeClass.configure do app_id 'my_app' title 'My App' cookie_name { '#{app_id}_session' } end

นี่คือเวอร์ชันสุดท้ายของโมดูลที่ใช้ DSL ของเรา - โค้ดทั้งหมด 36 บรรทัด:

ข้อใดอธิบายตัวอย่างการใช้งานได้ดีที่สุด
module Configurable def self.with(*attrs) not_provided = Object.new config_class = Class.new do attrs.each do |attr| define_method attr do |value = not_provided, &block| if value === not_provided && block.nil? result = instance_variable_get('@#{attr}') result.is_a?(Proc) ? instance_eval(&result) : result else instance_variable_set('@#{attr}', block || value) end end end attr_writer *attrs end class_methods = Module.new do define_method :config do @config ||= config_class.new end def configure(&block) config.instance_eval(&block) end end Module.new do singleton_class.send :define_method, :included do |host_class| host_class.extend class_methods end end end end

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

Ruby DSL - เมื่อใดควรใช้และเมื่อใดที่ไม่ควรใช้

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

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

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

ปรับแต่งทักษะของคุณ: คุณค่าของการออกแบบสหสาขาวิชาชีพ

การออกแบบ Ui

ปรับแต่งทักษะของคุณ: คุณค่าของการออกแบบสหสาขาวิชาชีพ
อย่าฟังลูกค้า - เหตุใดการวิจัยผู้ใช้จึงมีความสำคัญ

อย่าฟังลูกค้า - เหตุใดการวิจัยผู้ใช้จึงมีความสำคัญ

การออกแบบ Ux

โพสต์ยอดนิยม
แหล่งข้อมูลสำหรับธุรกิจขนาดเล็กสำหรับ COVID-19: เงินกู้เงินช่วยเหลือและสินเชื่อ
แหล่งข้อมูลสำหรับธุรกิจขนาดเล็กสำหรับ COVID-19: เงินกู้เงินช่วยเหลือและสินเชื่อ
วิธีออกแบบประสบการณ์ที่ยอดเยี่ยมสำหรับอินเทอร์เน็ตในทุกสิ่ง
วิธีออกแบบประสบการณ์ที่ยอดเยี่ยมสำหรับอินเทอร์เน็ตในทุกสิ่ง
กลยุทธ์การสื่อสารที่มีประสิทธิภาพสำหรับนักออกแบบ
กลยุทธ์การสื่อสารที่มีประสิทธิภาพสำหรับนักออกแบบ
เรียนรู้ Markdown: เครื่องมือการเขียนสำหรับนักพัฒนาซอฟต์แวร์
เรียนรู้ Markdown: เครื่องมือการเขียนสำหรับนักพัฒนาซอฟต์แวร์
แนวโน้มต่อไปนี้: การแสดงความเคารพกับการลอกเลียนแบบการออกแบบ
แนวโน้มต่อไปนี้: การแสดงความเคารพกับการลอกเลียนแบบการออกแบบ
 
คู่มือสไตล์ Sass: บทช่วยสอน Sass เกี่ยวกับวิธีการเขียนโค้ด CSS ที่ดีขึ้น
คู่มือสไตล์ Sass: บทช่วยสอน Sass เกี่ยวกับวิธีการเขียนโค้ด CSS ที่ดีขึ้น
ทำลายกระบวนการคิดเชิงออกแบบ
ทำลายกระบวนการคิดเชิงออกแบบ
การออกแบบเว็บไซต์ CMS: คู่มือการใช้งานเนื้อหาแบบไดนามิก
การออกแบบเว็บไซต์ CMS: คู่มือการใช้งานเนื้อหาแบบไดนามิก
ทำคณิตศาสตร์: การปรับขนาดแอปพลิเคชันไมโครเซอร์วิสด้วย Orchestrators
ทำคณิตศาสตร์: การปรับขนาดแอปพลิเคชันไมโครเซอร์วิสด้วย Orchestrators
การปฏิวัติหุ่นยนต์เชิงพาณิชย์ที่กำลังจะเกิดขึ้น
การปฏิวัติหุ่นยนต์เชิงพาณิชย์ที่กำลังจะเกิดขึ้น
โพสต์ยอดนิยม
  • วิธีเลี่ยงผ่านหน้าการชำระเงิน
  • สัญญาจ้างเครื่องคำนวณเงินเดือนประจำ
  • ui คืออะไรในการเล่นเกม
  • ทำความเข้าใจรหัส c ++
  • วิธีการสร้างราสเบอร์รี่ pi 3
หมวดหมู่
  • การจัดการวิศวกรรม
  • บุคลากรและทีมงานของผลิตภัณฑ์
  • อื่น ๆ
  • นวัตกรรม
  • © 2022 | สงวนลิขสิทธิ์

    portaldacalheta.pt