การเขียนโปรแกรมเชิงประกาศ: มันเป็นเรื่องจริงหรือไม่?

เผยแพร่แล้ว: 2022-03-11

ปัจจุบันการเขียนโปรแกรมเชิงประกาศเป็นกระบวนทัศน์ที่โดดเด่นของชุดโดเมนที่กว้างขวางและหลากหลาย เช่น ฐานข้อมูล การจัดการเทมเพลต และการกำหนดค่า

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

แม้ว่ากระบวนทัศน์นี้เป็นการปรับปรุงที่โดดเด่นเหนือแนวทางความจำเป็นที่แทนที่ ข้าพเจ้าขอยืนยันว่าการเขียนโปรแกรมเชิงประกาศมีข้อจำกัดที่สำคัญ ข้อจำกัดที่ฉันสำรวจในบทความนี้ ยิ่งกว่านั้น ฉันยังเสนอแนวทางสองแนวทางที่รวบรวมประโยชน์ของการเขียนโปรแกรมแบบเปิดเผย ในขณะที่แทนที่ข้อจำกัดของมัน

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

หากการเขียนโปรแกรมเชิงประกาศใช้ได้ผลดีสำหรับคุณ ฉันก็ไม่สามารถบอกคุณเป็นอย่างอื่น ได้

คุณสามารถรักหรือเกลียดการเขียนโปรแกรมที่เปิดเผยได้ แต่คุณไม่สามารถเพิกเฉยได้
ทวีต

ข้อดีของการเขียนโปรแกรม Declarative

ก่อนที่เราจะสำรวจข้อจำกัดของการเขียนโปรแกรมแบบเปิดเผย จำเป็นต้องเข้าใจข้อดีของมันเสียก่อน

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

  • ภาษาเฉพาะโดเมน (DSL) : อินเทอร์เฟซสากลสำหรับฐานข้อมูลเชิงสัมพันธ์คือ DSL ชื่อ Structured Query Language หรือที่รู้จักกันทั่วไปว่า SQL
  • DSL ซ่อนเลเยอร์ที่ต่ำกว่าจากผู้ใช้ : นับตั้งแต่เอกสารต้นฉบับของ Edgar F. Codd บน RDBs เป็นเรื่องธรรมดาที่พลังของโมเดลนี้คือการแยกการสืบค้นที่ต้องการออกจากลูป ดัชนี และเส้นทางการเข้าถึงที่ใช้งานพวกมัน

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

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

อะไรคือข้อดีที่ระบุไว้โดยทั่วไปของการเขียนโปรแกรมเชิงประกาศ?

ข้อดีของการเขียนโปรแกรมการประกาศตามรายการด้านล่าง แต่แต่ละรายการมีไอคอนตัวแทน

ผู้เสนอโปรแกรมการประกาศสามารถชี้ให้เห็นข้อดีได้อย่างรวดเร็ว อย่างไรก็ตาม แม้พวกเขายอมรับว่ามันมาพร้อมกับการแลกเปลี่ยน
ทวีต
  1. ความสามารถใน การอ่าน/การใช้งาน : DSL มักจะใกล้เคียงกับภาษาธรรมชาติ (เช่น ภาษาอังกฤษ) มากกว่า pseudocode ดังนั้นจึงอ่านง่ายและเรียนรู้ได้ง่ายกว่าโดยผู้ที่ไม่ใช่โปรแกรมเมอร์
  2. ความ รัดกุม : ต้นแบบส่วนใหญ่ถูกแยกออกโดย DSL เหลือบรรทัดให้ทำงานแบบเดียวกันน้อยลง
  3. Reuse : ง่ายต่อการสร้างรหัสที่สามารถใช้เพื่อวัตถุประสงค์ที่แตกต่างกัน บางสิ่งที่ยากอย่างฉาวโฉ่เมื่อใช้โครงสร้างที่จำเป็น
  4. Idempotence : คุณสามารถทำงานกับ สถานะสิ้นสุด และปล่อยให้โปรแกรมคิดออกสำหรับคุณ ตัวอย่างเช่น ผ่านการดำเนินการ upsert คุณสามารถแทรกแถวหากไม่มีอยู่ หรือแก้ไขหากมีอยู่แล้ว แทนที่จะเขียนโค้ดเพื่อจัดการกับทั้งสองกรณี
  5. การกู้คืนข้อผิดพลาด : ง่ายต่อการระบุโครงสร้างที่จะหยุดที่ข้อผิดพลาดแรก แทนที่จะต้องเพิ่มตัวฟังข้อผิดพลาดสำหรับทุกข้อผิดพลาดที่เป็นไปได้ (หากคุณเคยเขียนการเรียกกลับที่ซ้อนกันสามครั้งใน node.js คุณก็รู้ว่าฉันหมายถึงอะไร)
  6. ความโปร่งใสในการอ้างอิง : แม้ว่าข้อได้เปรียบนี้มักเกี่ยวข้องกับการเขียนโปรแกรมเชิงฟังก์ชัน แต่จริงๆ แล้วใช้ได้กับแนวทางใดๆ ที่ลดการจัดการสถานะแบบแมนนวลและอาศัยผลข้างเคียง
  7. Commutativity : ความเป็นไปได้ในการแสดงสถานะสิ้นสุดโดยไม่ต้องระบุลำดับจริงที่จะนำไปใช้

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

  1. เลเยอร์ระดับสูงที่ปรับแต่งให้เข้ากับโดเมนเฉพาะ : การเขียนโปรแกรมเชิงประกาศจะสร้างเลเยอร์ระดับสูงโดยใช้ข้อมูลของโดเมนที่นำไปใช้ เป็นที่ชัดเจนว่าถ้าเรากำลังจัดการกับฐานข้อมูล เราต้องการชุดของการดำเนินการสำหรับจัดการกับข้อมูล ข้อดีเจ็ดประการข้างต้นส่วนใหญ่มาจากการสร้างเลเยอร์ระดับสูงที่ปรับให้เข้ากับโดเมนของปัญหาโดยเฉพาะอย่างแม่นยำ
  2. Poka-yoke (fool-proofness) : เลเยอร์ระดับสูงที่ปรับแต่งโดเมนจะซ่อนรายละเอียดที่จำเป็นของการใช้งาน ซึ่งหมายความว่าคุณสร้างข้อผิดพลาดน้อยลงมาก เนื่องจากระบบไม่สามารถเข้าถึงรายละเอียดระดับต่ำได้ ข้อจำกัดนี้ช่วยขจัดข้อผิดพลาดหลาย คลาส จากโค้ดของคุณ

ปัญหาสองประการเกี่ยวกับการเขียนโปรแกรมเชิงประกาศ

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

ปัญหากับ DSL: การแยกจากกัน

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

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

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

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

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

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

DSL ไม่เคยมีพลังเต็มที่ของภาษาการเขียนโปรแกรม หากเป็นเช่นนั้น มันก็จะไม่ใช่ DSL อีกต่อไป แต่เป็นภาษาการเขียนโปรแกรมเต็มรูปแบบ

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

  • แทนที่ข้อความภายในเทมเพลต : การแทนที่ตัวแปร
  • การทำซ้ำของเทมเพลต : ลูป
  • หลีกเลี่ยงการพิมพ์เทมเพลตหากไม่ตรงตาม เงื่อนไข : conditionals
  • บาง ส่วน : รูทีนย่อย.
  • Helpers : รูทีนย่อย (ความแตกต่างเพียงอย่างเดียวกับบางส่วนคือผู้ช่วยเหลือสามารถเข้าถึงภาษาการเขียนโปรแกรมพื้นฐานและให้คุณออกจาก DSL straightjacket)

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

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

การแยกจากกันเป็นปัญหาโดยธรรมชาติของ DSL ไม่ว่าจะออกแบบมาอย่างดีเพียงใด

ตอนนี้เราหันไปหาปัญหาที่สองของเครื่องมือการประกาศ ซึ่งแพร่หลายแต่ไม่มีอยู่ในตัว

ปัญหาอื่น: การไม่เปิดเผยนำไปสู่ความซับซ้อน

ถ้าฉันเขียนบทความนี้เมื่อสองสามเดือนก่อน ส่วนนี้จะมีชื่อว่า Most Declarative Tools Are #@!$#@! ซับซ้อน แต่ฉันไม่รู้ว่าทำไม ในขั้นตอนการเขียนบทความนี้ ฉันพบวิธีที่ดีกว่าในการวาง: เครื่องมือการประกาศส่วนใหญ่ซับซ้อนกว่าที่พวกเขาต้องการ ฉันจะใช้เวลาที่เหลือในส่วนนี้เพื่ออธิบายว่าทำไม ในการวิเคราะห์ความซับซ้อนของเครื่องมือ ฉันเสนอการวัดที่เรียกว่า ช่องว่างความซับซ้อน ช่องว่างของความซับซ้อนคือความแตกต่างระหว่างการแก้ปัญหาที่กำหนดด้วยเครื่องมือกับการแก้ปัญหาในระดับล่าง (น่าจะเป็นรหัสความจำเป็นธรรมดา) ที่เครื่องมือตั้งใจจะเปลี่ยน เมื่อวิธีแก้ปัญหาแบบเดิมซับซ้อนกว่าแบบหลัง เราก็กำลังเผชิญกับช่องว่างความซับซ้อน ซับซ้อนกว่า นั้น ฉันหมายถึงโค้ดที่มากขึ้น โค้ดที่อ่านยากขึ้น แก้ไขได้ยากขึ้น และดูแลรักษายากขึ้น แต่ไม่จำเป็นต้องทั้งหมดนี้พร้อมกัน

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

สัญญาณของเครื่องมือที่มีช่องว่างความซับซ้อนมากคือ:

  • สิ่งที่ใช้เวลาสองสามนาทีในการอธิบายอย่างละเอียดในเงื่อนไขที่จำเป็น จะใช้เวลาหลายชั่วโมงในการเขียนโปรแกรมโดยใช้เครื่องมือ แม้ว่าคุณจะรู้วิธีใช้เครื่องมือก็ตาม
  • คุณรู้สึกว่าคุณกำลังทำงานกับเครื่องมืออย่างต่อเนื่องมากกว่าที่จะทำงานกับเครื่องมือ
  • คุณกำลังดิ้นรนเพื่อแก้ปัญหาตรงไปตรงมาที่อยู่ในโดเมนของเครื่องมือที่คุณใช้อยู่ แต่คำตอบ Stack Overflow ที่ดีที่สุดที่คุณพบนั้นอธิบาย วิธีแก้ปัญหา
  • เมื่อปัญหาตรงไปตรงมามากสามารถแก้ไขได้โดยคุณลักษณะบางอย่าง (ซึ่งไม่มีอยู่ในเครื่องมือ) และคุณเห็นปัญหา Github ในไลบรารีที่มีการอภิปรายยาวเกี่ยวกับคุณลักษณะดังกล่าวโดยมี +1 กระจายอยู่
  • อาการคันเรื้อรัง ความปรารถนาที่จะทิ้งเครื่องมือและทำทุกอย่างด้วยตัวเองใน _ for- loop_

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

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

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

ข้อโต้แย้งของฉันคือเครื่องมือใดๆ ที่มีอินเทอร์เฟซระดับสูงเพื่อสรุประดับที่ต่ำกว่าจะต้อง เปิดเผย ระดับที่สูงกว่านี้จากเครื่องมือที่ต่ำกว่า แนวคิดของการ เผยแผ่ มาจากผลงานชิ้นโบแดงของคริสโตเฟอร์ อเล็กซานเดอร์เรื่อง The Nature of Order - โดยเฉพาะเล่มที่ 2 (อย่างสิ้นหวัง) ที่อยู่นอกเหนือขอบเขตของบทความนี้ (ไม่ต้องพูดถึงความเข้าใจของฉัน) เพื่อสรุปความหมายของงานอันยิ่งใหญ่นี้สำหรับการออกแบบซอฟต์แวร์ ฉันเชื่อว่าผลกระทบจะมหาศาลในอีกไม่กี่ปีข้างหน้า นอกเหนือไปจากบทความนี้เพื่อให้คำจำกัดความที่เข้มงวดของกระบวนการแฉ ฉันจะใช้แนวคิดนี้ในรูปแบบฮิวริสติก

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

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

เพื่อเน้นย้ำว่าเหตุใดจึงเป็นตัวอย่างที่ดีเยี่ยมในการแฉ ฉันต้องการอ้างอิงข้อความที่ตัดตอนมาจากบทความปี 1979 โดย Dennis Ritchie หนึ่งในผู้เขียน Unix:

ในงานแบทช์ :

… แผนการควบคุมกระบวนการใหม่ได้แสดงคุณสมบัติที่มีค่ามากบางอย่างที่นำไปใช้ได้ทันที ตัวอย่างเช่น กระบวนการที่แยกออกมา (ด้วย & ) และการใช้เชลล์แบบเรียกซ้ำเป็นคำสั่ง ระบบส่วนใหญ่ต้องจัดหาสิ่งอำนวยความสะดวก batch job submission พิเศษบางประเภทและตัวแปลคำสั่งพิเศษสำหรับไฟล์ที่แตกต่างจากที่ใช้แบบโต้ตอบ

บนคอรูทีน :

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

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

ในกรณีที่ไม่มีกระบวนการเปิดเผย ระดับสูงจะซับซ้อนกว่าที่จำเป็นมาก กล่าวอีกนัยหนึ่ง ความซับซ้อนของเครื่องมือที่เปิดเผยส่วนใหญ่เกิดจากข้อเท็จจริงที่ว่าระดับสูงไม่ได้คลี่คลายจากระดับต่ำที่พวกเขาตั้งใจจะเปลี่ยน

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

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

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

แต่สิ่งที่เกี่ยวกับการแยกข้อกังวล? ไม่ควรดูและตรรกะแยกจากกัน? ข้อผิดพลาดหลักในที่นี้คือการวางตรรกะทางธุรกิจและตรรกะการนำเสนอไว้ในกระเป๋าใบเดียวกัน ตรรกะทางธุรกิจไม่มีที่ในเทมเพลตอย่างแน่นอน แต่ตรรกะของการนำเสนอยังคงมีอยู่ การยกเว้นตรรกะจากเทมเพลตจะผลักตรรกะของการนำเสนอไปยังเซิร์ฟเวอร์ซึ่งรองรับได้แบบงุ่มง่าม ฉันเป็นหนี้การกำหนดที่ชัดเจนของประเด็นนี้กับ Alexei Boronine ซึ่งเป็นกรณีที่ดีเยี่ยมสำหรับเรื่องนี้ในบทความนี้

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

สรุป; เครื่องมือสร้างเทมเพลตที่ประกาศประสบเพราะ:

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

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

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

วิธีแฝด

ในการเอาชนะปัญหาสองประการของการเขียนโปรแกรมแบบเปิดเผย ซึ่งผมได้นำเสนอที่นี่ ผมได้เสนอแนวทางแบบคู่:

  • ใช้ภาษาเฉพาะของโดเมนโครงสร้างข้อมูล (dsDSL) เพื่อเอาชนะความแตกแยก
  • สร้างระดับสูงที่แผ่ออกมาจากระดับล่างเพื่อเอาชนะช่องว่างความซับซ้อน

dsDSL

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

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

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

เช่นเดียวกับ JSON dsDSL มีแอตทริบิวต์ดังต่อไปนี้:

  1. ประกอบด้วยชุดฟังก์ชันขนาดเล็กมาก: JSON มีฟังก์ชันหลัก 2 ฟังก์ชันคือ parse และ stringify
  2. หน้าที่ของมันมักได้รับอาร์กิวเมนต์ที่ซับซ้อนและเรียกซ้ำ: JSON ที่แยกวิเคราะห์คืออาร์เรย์หรืออ็อบเจ็กต์ ซึ่งมักจะมีอาร์เรย์และอ็อบเจ็กต์เพิ่มเติมอยู่ภายใน
  3. อินพุตของฟังก์ชันเหล่านี้สอดคล้องกับรูปแบบที่เฉพาะเจาะจงมาก: JSON มีสคีมาการตรวจสอบที่ชัดเจนและบังคับใช้อย่างเข้มงวดเพื่อบอกว่าถูกต้องจากโครงสร้างที่ไม่ถูกต้อง
  4. ทั้งอินพุตและเอาต์พุตของฟังก์ชันเหล่านี้สามารถบรรจุและสร้างโดยภาษาการเขียนโปรแกรมโดยไม่ต้องมีไวยากรณ์แยกต่างหาก

แต่ dsDSL ไปไกลกว่า JSON ในหลายๆ ด้าน มาสร้าง dsDSL สำหรับสร้าง HTML โดยใช้ Javascript ต่อไปฉันจะพูดถึงประเด็นที่ว่าวิธีการนี้อาจขยายไปสู่ภาษาอื่นได้หรือไม่ (สปอยเลอร์: สามารถทำได้ใน Ruby และ Python อย่างแน่นอน แต่อาจไม่ใช่ในภาษา C)

HTML เป็นภาษามาร์กอัปที่ประกอบด้วย tags ที่คั่นด้วยวงเล็บมุม ( < และ > ) แท็กเหล่านี้อาจมีแอตทริบิวต์และเนื้อหาที่ไม่บังคับ แอตทริบิวต์เป็นเพียงรายการแอตทริบิวต์ของคีย์/ค่า และเนื้อหาอาจเป็นข้อความหรือแท็กอื่นๆ ทั้งแอตทริบิวต์และเนื้อหาเป็นตัวเลือกสำหรับแท็กที่ระบุ ฉันลดความซับซ้อนลงบ้าง แต่ถูกต้อง

วิธีตรงไปตรงมาในการแสดงแท็ก HTML ใน dsDSL คือการใช้อาร์เรย์ที่มีสามองค์ประกอบ: - แท็ก: สตริง - คุณสมบัติ: วัตถุ (ของธรรมดา ประเภทคีย์/ค่า) หรือ undefined (ถ้าไม่มีแอตทริบิวต์ที่จำเป็น) - เนื้อหา: สตริง (ข้อความ), อาร์เรย์ (แท็กอื่น) หรือ undefined (หากไม่มีเนื้อหา)

ตัวอย่างเช่น <a href="views">Index</a> สามารถเขียนเป็น ['a', {href: 'views'}, 'Index']

หากเราต้องการฝังองค์ประกอบจุดยึดนี้ลงใน div ด้วย links คลาส เราสามารถเขียน: ['div', {class: 'links'}, ['a', {href: 'views'}, 'Index']] .

ในการแสดงรายการแท็ก html หลายรายการในระดับเดียวกัน เราสามารถรวมแท็กเหล่านี้ไว้ในอาร์เรย์ได้:

 [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]

หลักการเดียวกันนี้อาจใช้กับการสร้างแท็กหลายรายการภายในแท็กเดียว:

 ['body', [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]]

แน่นอน dsDSL นี้จะไม่ทำให้เราห่างไกลหากเราไม่สร้าง HTML จากมัน เราต้องการฟังก์ชัน create ซึ่งจะใช้ generate ของเราและให้สตริงที่มี HTML ดังนั้นหากเราเรียกใช้ generate (['a', {href: 'views'}, 'Index']) เราจะได้รับสตริง <a href="views">Index</a>

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

ทีนี้ อะไรคือข้อดีของ dsDSL ที่ขัดแย้งกับ DSL?

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

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

 var DATA = [ {id: 1, description: 'Product 1', price: 20, onSale: true, categories: ['a']}, {id: 2, description: 'Product 2', price: 60, onSale: false, categories: ['b']}, {id: 3, description: 'Product 3', price: 120, onSale: false, categories: ['a', 'c']}, {id: 4, description: 'Product 4', price: 45, onSale: true, categories: ['a', 'b']} ]

ในแอปพลิเคชันจริง DATA จะถูกสร้างขึ้นแบบไดนามิกจากการสืบค้นฐานข้อมูล

นอกจากนี้เรายังมีตัวแปร FILTER ซึ่งเมื่อเริ่มต้นจะเป็นอาร์เรย์ที่มีหมวดหมู่ที่เราต้องการแสดง

เราต้องการให้โต๊ะของเรา:

  • แสดงส่วนหัวของตาราง
  • สำหรับแต่ละผลิตภัณฑ์ แสดงฟิลด์: คำอธิบาย ราคา และประเภท
  • อย่าพิมพ์ฟิลด์ id แต่เพิ่มเป็นแอตทริบิวต์ id สำหรับแต่ละแถว รุ่นสำรอง: เพิ่มแอตทริบิวต์ id ให้กับแต่ละองค์ประกอบ tr
  • วางคลาส onSale หากสินค้ากำลังลดราคา
  • จัดเรียงสินค้าตามราคาจากมากไปน้อย
  • กรองสินค้าบางประเภทตามหมวดหมู่ หาก FILTER เป็นอาร์เรย์ว่าง เราจะแสดงผลิตภัณฑ์ทั้งหมด มิฉะนั้น เราจะแสดงเฉพาะผลิตภัณฑ์ที่มีหมวดหมู่ของผลิตภัณฑ์อยู่ภายใน FILTER

เราสามารถสร้างตรรกะการนำเสนอที่ตรงกับข้อกำหนดนี้ในโค้ดประมาณ 20 บรรทัด:

 function drawTable (DATA, FILTER) { var printableFields = ['description', 'price', 'categories']; DATA.sort (function (a, b) {return a.price - b.price}); return ['table', [ ['tr', dale.do (printableFields, function (field) { return ['th', field]; })], dale.do (DATA, function (product) { var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; }); return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]]; })]; }) ]]; }

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

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

 var drawTable = function (DATA, FILTER) {

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

 var printableFields = ['description', 'price', 'categories'];

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

 DATA.sort (function (a, b) {return a.price - b.price});

ที่นี่เราส่งคืนวัตถุตามตัวอักษร อาร์เรย์ที่มี table เป็นองค์ประกอบแรกและมีเนื้อหาเป็นองค์ประกอบที่สอง นี่คือการแสดง dsDSL ของ <table> ที่เราต้องการสร้าง

 return ['table', [

ตอนนี้เราสร้างแถวที่มีส่วนหัวของตาราง ในการสร้างเนื้อหา เราใช้ dale.do ซึ่งเป็นฟังก์ชันเช่น Array.map แต่ใช้งานได้กับวัตถุด้วย เราจะทำซ้ำ printableFields และสร้างส่วนหัวของตารางสำหรับแต่ละรายการ:

 ['tr', dale.do (printableFields, function (field) { return ['th', field]; })],

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

ตอนนี้ ทำซ้ำผ่านผลิตภัณฑ์ที่มีอยู่ใน DATA

 dale.do (DATA, function (product) {

เราตรวจสอบว่าผลิตภัณฑ์นี้ถูกทิ้งโดย FILTER หรือไม่ หาก FILTER ว่างเปล่า เราจะพิมพ์ผลิตภัณฑ์ หาก FILTER ไม่ว่างเปล่า เราจะทำซ้ำตามหมวดหมู่ของผลิตภัณฑ์จนกว่าเราจะพบตัวกรองที่อยู่ใน FILTER เราทำสิ่งนี้โดยใช้ dale.stop

 var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; });

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

หาก matches เป็น false เราจะส่งคืนอาร์เรย์ว่าง (ดังนั้นเราจึงไม่พิมพ์ผลิตภัณฑ์นี้) มิฉะนั้น เราจะส่งคืน <tr> ด้วย id และ class ที่ถูกต้อง และเราทำซ้ำผ่าน printableFields เพื่อพิมพ์ฟิลด์

 return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]];

แน่นอนเราปิดทุกสิ่งที่เราเปิด วากยสัมพันธ์ไม่สนุกเหรอ?

 })]; }) ]]; }

ทีนี้ เราจะรวมตารางนี้เข้ากับบริบทที่กว้างขึ้นได้อย่างไร เราเขียนฟังก์ชันชื่อ drawAll ที่จะเรียกใช้ฟังก์ชันทั้งหมดที่สร้างมุมมอง นอกเหนือจาก drawTable เราอาจมี drawHeader , drawFooter และฟังก์ชันอื่นๆ ที่เปรียบเทียบกันได้ ซึ่งทั้งหมด จะคืนค่า dsDSL

 var drawAll = function () { return generate ([ drawHeader (), drawTable (DATA, FILTER), drawFooter () ]); }

หากคุณไม่ชอบรูปลักษณ์ของโค้ดด้านบน ไม่มีอะไรที่ฉันพูดจะโน้มน้าวใจคุณได้ นี่คือ dsDSL ที่ดีที่สุด You might as well stop reading the article (and drop a mean comment too because you've earned the right to do so if you've made it this far!). But seriously, if the code above doesn't strike you as elegant, nothing else in this article will.

For those who are still with me, I would like to go back to the main claim of this section, which is that a dsDSL has the advantages of both the high and the low level :

  • The advantage of the low level resides in writing code whenever we want, getting out of the straightjacket of the DSL.
  • The advantage of the high level resides in using literals that represent what we want to declare and letting the functions of the tool convert that into the desired end state (in this case, a string with HTML).

But how is this truly different from purely imperative code? I think ultimately the elegance of the dsDSL approach boils down to the fact that code written in this way mostly consists of expressions, instead of statements. More precisely, code that uses a dsDSL is almost entirely composed of:

  • Literals that map to lower level structures.
  • Function invocations or lambdas within those literal structures that return structures of the same kind.

Code that consists mostly of expressions and which encapsulate most statements within functions is extremely succinct because all patterns of repetition can be easily abstracted. You can write arbitrary code as long as that code returns a literal that conforms to a very specific, non-arbitrary form.

A further characteristic of dsDSLs (which we don't have time to explore here) is the possibility of using types to increase the richness and succinctness of the literal structures. I will expound on this issue on a future article.

Might it be possible to create dsDSLs beyond Javascript, the One True Language? I think that it is, indeed, possible, as long as the language supports:

  • Literals for: arrays, objects (associative arrays), function invocations, and lambdas.
  • Runtime type detection
  • Polymorphism and dynamic return types

I think this means that dsDSLs are tenable in any modern dynamic language (ie: Ruby, Python, Perl, PHP), but probably not in C or Java.

Walk, Then Slide: How To Unfold The High From The Low

In this section I will attempt to show a way for unfolding a high level tool from its domain. In a nutshell, the approach consists of the following steps

  1. Take two to four problems that are representative instances of a problem domain. These problems should be real. Unfolding the high level from the low one is a problem of induction, so you need real data to come up with representative solutions.
  2. Solve the problems with no tool in the most straightforward way possible.
  3. Stand back, take a good look at your solutions, and notice the common patterns among them.
  4. Find the patterns of representation (high level).
  5. Find the patterns of generation (low level).
  6. Solve the same problems with your high level layer and verify that the solutions are indeed correct.
  7. If you feel that you can easily represent all the problems with your patterns of representation, and the generation patterns for each of these instances produce correct implementations, you're done. Otherwise, go back to the drawing board.
  8. If new problems appear, solve them with the tool and modify it accordingly.
  9. The tool should converge asymptotically to a finished state, no matter how many problems it solves. In other words, the complexity of the tool should remain constant, rather than growing with the amount of problems it solves.

Now, what the hell are patterns of representation and patterns of generation ? I'm glad you asked. The patterns of representation are the patterns in which you should be able to express a problem that belongs to the domain that concerns your tool. It is an alphabet of structures that allows you to write any pattern you might wish to express within its domain of applicability. In a DSL, these would be the production rules. Let's go back to our dsDSL for generating HTML.

Breaking down an HTML snippet. The line

The humble HTML tag is a good example of patterns of representation. Let's take a closer look at these basic patterns.
ทวีต

The patterns of representation for HTML are the following:

  • A single tag: ['TAG']
  • A single tag with attributes: ['TAG', {attribute1: value1, attribute2: value2, ...}]
  • A single tag with contents: ['TAG', 'CONTENTS']
  • A single tag with both attributes and contents: ['TAG', {attribute1: value1, ...}, 'CONTENTS']
  • A single tag with another tag inside: ['TAG1', ['TAG2', ...]]
  • A group of tags (standalone or inside another tag): [['TAG1', ...], ['TAG2', ...]]
  • Depending on a condition, place a tag or no tag: condition ? ['TAG', ...] : [] / Depending on a condition, place an attribute or no attribute: ['TAG', {class: condition ? 'someClass': undefined}, ...]

These instances can be represented with the dsDSL notation we determined in the previous section. And this is all you need to represent any HTML you might need. More sophisticated patterns, such as conditional iteration through an object to generate a table, may be implemented with functions that return the patterns of representation above, and these patterns map directly to HTML tags.

If the patterns of representation are the structures you use to express what you want, the patterns of generation are the structures your tool will use to convert patterns of representation into the lower level structures. For HTML, these are the following:

  • Validate the input (this is actually is an universal pattern of generation).
  • Open and close tags (but not the void tags, like <input> , which are self-closing).
  • Place attributes and contents, escaping special characters (but not the contents of the <style> and <script> tags).

Believe it or not, these are the patterns you need to create an unfolding dsDSL layer that generates HTML. Similar patterns can be found for generating CSS. In fact, lith does both, in ~250 lines of code.

One last question remains to be answered: What do I mean by walk, then slide ? When we deal with a problem domain, we want to use a tool that delivers us from the nasty details of that domain. In other words, we want to sweep the low level under the rug, the faster the better. The walk, then slide approach proposes exactly the opposite: spend some time on the low level. Embrace its quirks, and understand which are essential and which can be avoided in the face of a set of real, varied, and useful problems.

After walking in the low level for some time and solving useful problems, you will have a sufficiently deep understanding of their domain. The patterns of representation and generation will then arise naturally; they are wholly derived from the nature of the problem they intend to solve. You can then write code that employs them. If they work, you will be able to slide through problems where you recently had to walk through them. Sliding means many things; it implies speed, precision and lack of friction. Maybe more importantly, this quality can be felt; when solving problems with this tool, do you feel like you're walking through the problem, or do you feel that you're sliding through it?

Maybe the most important thing about an unfolded tool is not the fact that it frees us from having to deal with the low level. Rather, by capturing the empiric patterns of repetition in the low level, a good high level tool allows us to understand fully the domain of applicability.

An unfolded tool will not just solve a problem - it will enlighten you about the problem's structure.

So, don't run away from a worthy problem. First walk around it, then slide through it.

Related: Introduction To Concurrent Programming: A Beginner's Guide