Style Specifics (รายละเอียดเฉพาะของ Style)

แก่นแท้ของ microkernel architecture ประกอบด้วย component สองประเภท คือ core system และ plug-in .

Core System (Core System)

core system ถูกนิยามอย่างเป็นทางการว่าคือฟังก์ชันขั้นต่ำที่จำเป็นในการรันระบบ ลองดู Eclipse IDE เป็นตัวอย่างที่ดี core system ของ Eclipse ก็แค่ text editor พื้นฐาน: เปิดไฟล์ แก้ไขข้อความ และบันทึกไฟล์ ต้องรอจนกว่าคุณจะเพิ่ม plug-in เข้าไป Eclipse ถึงจะเริ่มกลายเป็นผลิตภัณฑ์ที่ใช้งานได้จริง

อย่างไรก็ตาม อีกนิยามหนึ่งของ core system คือ happy path : flow การประมวลผลทั่วไปผ่านแอปพลิเคชันที่มีการประมวลผลแบบ custom น้อยมากหรือไม่มีเลย microkernel architecture จะนำ cyclomatic complexity ของแอปพลิเคชันออกจาก core system แล้วย้ายไปไว้ใน plug-in component แยกต่างหาก วิธีนี้ช่วยให้ extensibility และ maintainability ดีขึ้น รวมถึงเพิ่ม testability ด้วย

เพื่อเจาะลึกยิ่งขึ้น เรากลับไปดู Going Green แอปพลิเคชัน recycling อุปกรณ์อิเล็กทรอนิกส์ที่เราแนะนำไว้ใน Chapter 7 สมมติว่าคุณกำลังทำงานกับแอปพลิเคชันของ Going Green ซึ่งต้องประเมินอุปกรณ์อิเล็กทรอนิกส์แต่ละชิ้นที่ได้รับตามกฎการประเมินเฉพาะแบบ custom โค้ด Java สำหรับการประมวลผลลักษณะนี้อาจมีหน้าตาดังนี้:

public void assessDevice(String deviceID) {
   if (deviceID.equals("iPhone6s")) {
      assessiPhone6s();
   } else if (deviceID.equals("iPad1"))
      assessiPad1();
   } else if (deviceID.equals("Galaxy5"))
      assessGalaxy5();
   } else ...
      ...
   }
}

แทนที่จะใส่ client-specific customization ทั้งหมดนี้—ซึ่งมี cyclomatic complexity สูง—ไว้ใน core system คุณสามารถสร้าง plug-in component แยกสำหรับอุปกรณ์อิเล็กทรอนิกส์แต่ละชนิดที่ต้องประเมินได้ plug-in component เฉพาะของแต่ละ client ไม่เพียงแยก logic ของอุปกรณ์ที่เป็นอิสระออกจาก processing flow ที่เหลือเท่านั้น แต่ยังรองรับการขยายตัวด้วย: การเพิ่มอุปกรณ์ใหม่ที่จะประเมินเป็นเพียงแค่การเพิ่ม plug-in component ใหม่และอัปเดต registry ด้วย microkernel architecture style การประเมินอุปกรณ์อิเล็กทรอนิกส์ต้องการแค่ให้ core system ค้นหาและเรียกใช้ device plug-in ที่ตรงกันเท่านั้น ดังแสดงในโค้ดต้นฉบับที่แก้ไขแล้วนี้:

public void assessDevice(String deviceID) {
    String plugin = pluginRegistry.get(deviceID);
    Class<?> theClass = Class.forName(plugin);
    Constructor<?> constructor = theClass.getConstructor();
    DevicePlugin devicePlugin =
        (DevicePlugin)constructor.newInstance();
    devicePlugin.assess();
}

ในตัวอย่างนี้ กฎและคำสั่งที่ซับซ้อนทั้งหมดสำหรับการประเมินอุปกรณ์อิเล็กทรอนิกส์ชิ้นหนึ่งจะถูกบรรจุไว้ในตัวเองภายใน plug-in component แบบ standalone ที่เป็นอิสระ ซึ่ง core system สามารถเรียกใช้แบบ generic ได้

ขึ้นอยู่กับขนาดและความซับซ้อน คุณสามารถ implement core system เป็น layered architecture หรือเป็น modular monolith ก็ได้ (ดังแสดงใน Figure 13-2 ) ในบางกรณี คุณอาจถึงขั้นแยก core system ออกเป็น domain service ที่ deploy แยกกัน โดยแต่ละ domain service บรรจุ plug-in component เฉพาะของ domain นั้น เพื่อความง่ายในตัวอย่างนี้ เราจะสมมติว่าคุณ implement มันเป็น layered architecture สำหรับ Going Green

สมมติว่า Payment Processing คือ domain service ที่แทน core system วิธีการชำระเงินแต่ละแบบ (บัตรเครดิต, PayPal, store credit, gift card และ purchase order) จะมี plug-in component แยกเฉพาะของ payment domain นั้น

Microkernel architecture core system variants

Figure 13-2. รูปแบบต่าง ๆ ของ core system ใน microkernel architecture

Presentation layer ของ core system สามารถฝังอยู่ภายใน core system หรือ implement เป็น user interface แยกต่างหาก โดย core system ทำหน้าที่เป็น backend service ก็ได้ อันที่จริง คุณยัง implement UI แยกด้วย microkernel architecture style ได้เช่นกัน Figure 13-3 แสดงรูปแบบของ Presentation layer เหล่านี้ในความสัมพันธ์กับ core system .

User Interface Variants

Figure 13-3. รูปแบบต่าง ๆ ของ user interface

Plug-In Components (Plug-in Component)

plug-in component คือ component แบบ standalone ที่เป็นอิสระ ซึ่งบรรจุการประมวลผลเฉพาะทาง feature เพิ่มเติม และโค้ดแบบ custom ที่มีไว้เพื่อเสริมหรือขยาย core system นอกจากนี้ยังแยก highly volatile code ออกไป ทำให้ maintainability และ testability ของแอปพลิเคชันดีขึ้น โดยหลักการแล้ว plug-in component ไม่ควรมี dependency ระหว่างกัน

การสื่อสารระหว่าง plug-in component กับ core system โดยทั่วไปเป็นแบบ point-to-point หมายความว่า “pipe” ที่เชื่อมต่อ plug-in กับ core system มักเป็นการเรียก method หรือ function call ไปยัง entry-point class ของ plug-in component นอกจากนี้ plug-in component ยังสามารถเป็นแบบ compile-based หรือ runtime-based ก็ได้ plug-in component แบบ Runtime สามารถเพิ่มหรือลบได้ขณะ runtime โดยไม่ต้อง redeploy core system หรือ plug-in อื่น และมักถูกจัดการผ่าน framework เช่น Open Service Gateway Initiative (OSGi) for Java , Penrose (Java) , Jigsaw (Java) หรือ Prism (.NET) . plug-in component แบบ Compile-based จัดการง่ายกว่ามาก แต่การแก้ไข ลบ หรือเพิ่ม plug-in ต้อง redeploy แอปพลิเคชัน monolithic ทั้งหมด

plug-in component แบบ point-to-point สามารถ implement เป็น shared library (เช่น JAR, DLL หรือ Gem), package name ในภาษา Java หรือ namespace ในภาษา C# ก็ได้ ในแอปพลิเคชัน Going Green ของเรา plug-in ของอุปกรณ์อิเล็กทรอนิกส์แต่ละชนิดสามารถเขียนและ implement เป็น JAR, DLL หรือ Ruby Gem (หรือ shared library อื่นใดก็ได้) โดยตั้งชื่ออุปกรณ์ให้ตรงกับชื่อของ shared library ที่เป็นอิสระ ดังแสดงใน Figure 13-4 .

Shared library plug-in implementation

Figure 13-4. การ implement plug-in แบบ shared library

แนวทางที่ง่ายกว่า ดังแสดงใน Figure 13-5 คือการ implement plug-in component แต่ละตัวเป็น namespace หรือ package name แยกภายใน code base หรือ IDE project เดียวกัน เมื่อสร้าง namespace เราแนะนำให้ใช้ semantics ดังนี้: app.plug-in.<domain>.<context> ตัวอย่างเช่น ลองพิจารณา namespace app.plug-in.assessment.iphone6s node ที่สอง ( plug-in ) ทำให้ชัดเจนว่า component นี้เป็น plug-in และดังนั้นจึงควรปฏิบัติตามกฎพื้นฐานอย่างเคร่งครัด (ต้องเป็น self-contained และแยกจาก plug-in อื่น) node ที่สามอธิบาย domain (ในกรณีนี้คือ assessment ) ทำให้สามารถจัดกลุ่ม plug-in component ตามจุดประสงค์ร่วมกันได้ node ที่สี่ ( iphone6s ) อธิบาย context เฉพาะของ plug-in ทำให้ค้นหา plug-in ของอุปกรณ์นั้นเพื่อแก้ไขหรือทดสอบได้ง่าย

Package or namespace plug-in implementation

Figure 13-5. การ implement plug-in แบบ package หรือ namespace

plug-in component ไม่จำเป็นต้องใช้การสื่อสารแบบ point-to-point กับ core system เสมอไป ทางเลือกอื่นได้แก่การใช้ REST หรือ messaging ในการเรียกใช้งาน plug-in โดยแต่ละ plug-in เป็น service แบบ standalone (หรืออาจเป็น microservice ที่ implement ด้วย container ก็ได้) แม้ว่าฟังดูเหมือนเป็นวิธีที่ดีในการเพิ่ม scalability โดยรวม แต่โปรดสังเกตว่า topology นี้ (ดังแสดงใน Figure 13-6 ) ยังคงเป็น architecture quantum เดียวเท่านั้น เนื่องจาก core system ยังเป็น monolithic ทุกคำขอต้องผ่าน core system ก่อนที่จะไปถึง plug-in service

ประโยชน์ของแนวทาง remote-access สำหรับ plug-in component ที่ implement เป็น service แยกกันคือ ช่วยให้ component decoupling โดยรวมดีขึ้น, scalability และ throughput ดีขึ้น และเปลี่ยนแปลงได้ขณะ runtime โดยไม่ต้องใช้ framework พิเศษ (เช่น OSGi, Jigsaw หรือ Prism) นอกจากนี้ยังรองรับการสื่อสารแบบ asynchronous ไปยัง plug-in ซึ่งขึ้นอยู่กับสถานการณ์ อาจช่วยปรับปรุง responsiveness โดยรวมของผู้ใช้ได้อย่างมาก จากตัวอย่าง Going Green แทนที่จะต้องรอให้การประเมินอุปกรณ์อิเล็กทรอนิกส์ทำงานเสร็จ core system สามารถส่ง request แบบ asynchronous เพื่อเริ่มการประเมินอุปกรณ์ชิ้นหนึ่งได้ เมื่อการประเมินเสร็จสิ้น plug-in สามารถแจ้ง core system ผ่าน messaging channel แบบ asynchronous อีกช่องทางหนึ่ง ซึ่งจะแจ้งผู้ใช้ต่อว่าการประเมินเสร็จแล้ว

Remote Plug-in Access

Figure 13-6. การเข้าถึง plug-in ระยะไกลด้วย REST

พร้อมกับประโยชน์เหล่านี้ก็มี trade-off ตามมา remote plug-in access เปลี่ยน microkernel architecture ให้กลายเป็น distributed architecture แทนที่จะเป็น monolithic ทำให้ยากต่อการ implement และ deploy สำหรับผลิตภัณฑ์ on-prem ของ third party ส่วนใหญ่ นอกจากนี้ยังสร้างความซับซ้อนและต้นทุนโดยรวมมากขึ้น และทำให้ deployment topology โดยรวมซับซ้อนขึ้น ถ้า plug-in หยุดตอบสนองหรือหยุดทำงาน โดยเฉพาะถ้าระบบใช้ REST คำขอนั้นจะไม่สามารถสำเร็จได้ ซึ่งจะไม่เกิดขึ้นกับ monolithic deployment การเลือกระหว่าง point-to-point กับ remote communication ควรขึ้นอยู่กับความต้องการเฉพาะของโปรเจกต์และการวิเคราะห์ trade-off อย่างรอบคอบ

The Spectrum of “Microkern-ality” (สเปกตรัมของ “ความเป็น Microkernel”)

ไม่ใช่ทุกระบบที่รองรับ plug-in จะเป็น microkernel แต่ทุก microkernel รองรับ plug-in ระดับ “ความเป็น microkernel” ของระบบหนึ่ง ๆ ตามที่เราเรียก ขึ้นอยู่กับว่ามีฟังก์ชันแบบ standalone อยู่ใน core system มากแค่ไหน สิ่งนี้สะท้อนอยู่ในสเปกตรัมที่แสดงใน Figure 13-7 .

The spectrum of microkern-ality

Figure 13-7. สเปกตรัมของ “ความเป็น microkernel”

ใน Figure 13-7 microkernel architecture แบบ “บริสุทธิ์” (เช่น Eclipse IDE หรือเครื่องมือ linter ที่กล่าวถึงข้างต้น) มีฟังก์ชันหลักน้อยมาก ตัวอย่างเช่น linter จะ parse source code และส่งมอบ abstract syntax tree เพื่อให้นักพัฒนาเขียนกฎเกี่ยวกับการใช้ภาษาได้ core ทำหน้าที่ parse โค้ด แต่จนกว่าจะมีใครเขียน plug-in มาใช้ประโยชน์จากมัน มันก็แทบไม่มีประโยชน์ ลองเทียบกับ web browser ซึ่งรองรับ plug-in แต่ก็ใช้งานได้สมบูรณ์แม้ไม่มี plug-in เลย browser จึงอยู่ทางด้านขวาของสเปกตรัม

การพิจารณาความผันผวน (volatility) ของ core ช่วยให้สถาปนิกตัดสินใจได้มากว่าระบบควรเป็นเพียงระบบที่รองรับ plug-in หรือควรเป็น microkernel แบบ “บริสุทธิ์” มากกว่า

Registry (Registry)

core system จำเป็นต้องรู้ว่ามี plug-in module ใดบ้างและจะเข้าถึงได้อย่างไร วิธีทั่วไปวิธีหนึ่งในการ implement สิ่งนี้คือผ่าน plug-in registry registry นี้เก็บข้อมูลเกี่ยวกับ plug-in module แต่ละตัว รวมถึงชื่อ, data contract และรายละเอียด remote access protocol (ขึ้นอยู่กับว่า plug-in เชื่อมต่อกับ core system อย่างไร) ตัวอย่างเช่น plug-in สำหรับซอฟต์แวร์ภาษี ที่ทำหน้าที่ตรวจสอบรายการที่มีความเสี่ยงสูงในการถูกตรวจสอบภาษี (tax audit) อาจมี registry entry ที่บรรจุชื่อของ service (AuditChecker), data contract (input data และ output data) และรูปแบบ contract (XML)

registry อาจเรียบง่ายเพียงแค่เป็นโครงสร้าง map ภายในที่ core system เป็นเจ้าของ ซึ่งบรรจุ key และ reference ของ plug-in component หรืออาจซับซ้อนถึงขั้นเป็น registry และ discovery tool ที่ฝังอยู่ภายใน core system หรือ deploy แยกภายนอก (เช่น Apache ZooKeeper หรือ Consul ) จากตัวอย่างการ recycling อุปกรณ์อิเล็กทรอนิกส์ โค้ด Java ต่อไปนี้ implement registry แบบง่ายภายใน core system โดยแสดงตัวอย่าง entry แบบ point-to-point, entry แบบ messaging และ entry แบบ RESTful สำหรับการประเมินอุปกรณ์ iPhone 6S:

Map<String, String> registry = new HashMap<String, String>();
static {
  //point-to-point access example
  registry.put("iPhone6s", "Iphone6sPlugin");

  //messaging example
  registry.put("iPhone6s", "iphone6s.queue");

  //restful example
  registry.put("iPhone6s", "https://atlas:443/assess/iphone6s");
}

Contracts (Contract)

contract ระหว่าง plug-in component กับ core system มักเป็นมาตรฐานเดียวกันทั่วทั้ง domain ของ plug-in component และรวมถึง behavior, input data และ output data ที่ส่งกลับจาก plug-in component custom contract มักถูกใช้ในสถานการณ์ที่ plug-in component ถูกพัฒนาโดย third party ทำให้สถาปนิกไม่มีอำนาจควบคุม contract ที่ plug-in ใช้ ในกรณีเช่นนี้ มักสร้าง adapter ระหว่าง contract ของ plug-in กับ contract มาตรฐานของคุณ เพื่อให้ core system ไม่ต้องมีโค้ดเฉพาะสำหรับ plug-in แต่ละตัว

plug-in contract สามารถ implement เป็น XML, JSON หรือแม้แต่ object ที่ส่งไปมาระหว่าง plug-in กับ core system ในแอปพลิเคชัน recycling อุปกรณ์อิเล็กทรอนิกส์ contract ต่อไปนี้ (implement เป็น Java interface มาตรฐานชื่อ AssessmentPlugin ) นิยาม behavior โดยรวม ( assess() , register() และ deregister() ) พร้อมกับ output data ที่คาดหวังจาก plug-in component ( AssessmentOutput ):

public interface AssessmentPlugin {
    public AssessmentOutput assess();
    public String register();
    public String deregister();
}

public class AssessmentOutput {
    public String assessmentReport;
    public Boolean resell;
    public Double value;
    public Double resellPrice;
}

ในตัวอย่าง contract นี้ plug-in ประเมินอุปกรณ์คาดว่าจะส่งกลับรายงานการประเมินพร้อมกับ:

  • string ที่จัดรูปแบบแล้ว

  • flag การขายต่อ (resell) (true หรือ false) ที่ระบุว่าอุปกรณ์นี้สามารถขายต่อในตลาด third party ได้หรือควรถูกกำจัดทิ้งอย่างปลอดภัย

  • ถ้าสามารถขายต่อได้ มูลค่าที่คำนวณและราคาขายต่อที่แนะนำ

สังเกตแบบจำลองบทบาทและความรับผิดชอบระหว่าง core system กับ plug-in component ในตัวอย่างนี้ โดยเฉพาะกับ field assessmentReport ไม่ใช่หน้าที่ของ core system ที่จะจัดรูปแบบและทำความเข้าใจรายละเอียดของรายงานการประเมิน มีแค่หน้าที่พิมพ์หรือแสดงให้ผู้ใช้เห็นเท่านั้น