การแยกโครงการยูทิลิตี“ ปึกแผ่น” ออกเป็นส่วนประกอบแต่ละส่วนด้วยการพึ่งพา“ ทางเลือก”


26

ในช่วงหลายปีที่ผ่านการใช้ C # /. NET สำหรับโครงการภายในองค์กรเรามีห้องสมุดหนึ่งแห่งที่เติบโตขึ้นอย่างเป็นองค์กร มันถูกเรียกว่า "Util" และฉันแน่ใจว่าพวกคุณหลายคนเคยเห็นสัตว์ร้ายตัวนี้ในอาชีพของคุณ

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

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

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

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

เมื่อมองผ่านห้องสมุด "Util" นี้ฉันเห็นว่าห้องสมุดที่อาจแยกได้เกือบทั้งหมดมีคุณสมบัติที่เป็นทางเลือกเช่นนี้ซึ่งผูกไว้กับไลบรารีอื่น ๆ ถ้าฉันต้องการห้องสมุดเหล่านั้นจริงๆแล้วการพึ่งพาสิ่งต่าง ๆ นี้ไม่ได้ถูกยกเลิกการผูกมัดจริงๆ: คุณยังคงต้องการไลบรารีทั้งหมดถ้าคุณต้องการใช้เพียงอันเดียว

มีวิธีการที่จัดตั้งขึ้นเพื่อจัดการการพึ่งพาทางเลือกเช่นใน.


2
แม้ว่าห้องสมุดจะขึ้นอยู่กับแต่ละอื่น ๆ ก็ยังคงมีประโยชน์บางอย่างในการแยกพวกเขาออกเป็นห้องสมุดที่สอดคล้องกัน แต่แยกกันแต่ละคนมีฟังก์ชั่นหมวดหมู่กว้าง
Robert Harvey

คำตอบ:


20

Refactor ช้าๆ

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

วิธีการโดยรวม:

  1. อันดับแรกใช้เวลาสักครู่และคิดว่าคุณต้องการให้ชุดประกอบยูทิลิตี้เหล่านี้ดูอย่างไรเมื่อเสร็จสิ้น อย่ากังวลมากเกินไปเกี่ยวกับรหัสที่มีอยู่ของคุณคิดว่าเป้าหมายสุดท้าย ตัวอย่างเช่นคุณอาจต้องการ:

    • MyCompany.Utilities.Core (ประกอบด้วยอัลกอริธึมการบันทึก ฯลฯ )
    • MyCompany.Utilities.UI (รหัสการวาด ฯลฯ )
    • MyCompany.Utilities.UI.WinForms (รหัส System.Windows.Forms การควบคุมแบบกำหนดเอง ฯลฯ )
    • MyCompany.Utilities.UI.WPF (รหัสที่เกี่ยวข้องกับ WPF, คลาสพื้นฐาน MVVM)
    • MyCompany.Utilities.Serialization (รหัสซีเรียลไลซ์เซชั่น)
  2. สร้างโครงการที่ว่างเปล่าสำหรับแต่ละโครงการเหล่านี้และสร้างการอ้างอิงโครงการที่เหมาะสม (การอ้างอิง UI Core, UI.WinForms การอ้างอิง UI) เป็นต้น

  3. ย้ายผลไม้แขวนต่ำใด ๆ (คลาสหรือวิธีการที่ไม่ประสบปัญหาการพึ่งพา) จากแอสเซมบลี Utils ของคุณไปยังแอสเซมบลีเป้าหมายใหม่

  4. รับสำเนาของNDependและRefactoringของ Martin Fowler เพื่อเริ่มการวิเคราะห์ชุด Utils ของคุณเพื่อเริ่มทำงานกับคนที่ยากขึ้น เทคนิคสองอย่างที่จะเป็นประโยชน์:

    • ในหลายกรณีที่คุณจะต้องควบคุม Invert ด้วยวิธีการของอินเตอร์เฟซที่ได้รับมอบหมายหรือกิจกรรม
    • ในกรณีของการพึ่งพา "ทางเลือก" ของคุณดูด้านล่าง

การจัดการอินเทอร์เฟซเสริม

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

  1. ก่อนอื่นให้กำหนดอินเทอร์เฟซทั่วไปในชุดประกอบหลักของคุณ:

    ป้อนคำอธิบายรูปภาพที่นี่

    ตัวอย่างเช่นIStringColorerอินเทอร์เฟซจะมีลักษณะดังนี้:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. จากนั้นใช้อินเทอร์เฟซในแอสเซมบลีด้วยคุณลักษณะ ตัวอย่างเช่นStringColorerชั้นจะมีลักษณะ:

    using MyCompany.Utilities.Core.OptionalInterfaces;
    namespace MyCompany.Utilities.Console
    {
        class StringColorer : IStringColorer
        {
            #region IStringColorer Members
    
            public string Decorate(string s)
            {
                return "*" + s + "*";   //TODO: implement coloring
            }
    
            #endregion
        }
    }
    
  3. สร้างPluginFinderคลาส (หรือ InterfaceFinder อาจเป็นชื่อที่ดีกว่าในกรณีนี้) คลาสที่สามารถค้นหาอินเทอร์เฟซจากไฟล์ DLL ในโฟลเดอร์ปัจจุบัน นี่คือตัวอย่างง่ายๆ ตามคำแนะนำของ @Woodcock (และฉันเห็นด้วย) เมื่อโครงการของคุณเติบโตฉันขอแนะนำให้ใช้หนึ่งในเฟรมเวิร์กการพึ่งพาที่มีอยู่ ( Common Serivce Locator พร้อม UnityและSpring.NETมา) เพื่อการใช้งานที่แข็งแกร่งยิ่งขึ้น ความสามารถของฟีเจอร์นั้น "หรือที่เรียกว่ารูปแบบตัวระบุบริการ คุณสามารถปรับเปลี่ยนให้เหมาะกับความต้องการของคุณ

    using System;
    using System.Linq;
    using System.IO;
    using System.Reflection;
    
    namespace UtilitiesCore
    {
        public static class PluginFinder
        {
            private static bool _loadedAssemblies;
    
            public static T FindInterface<T>() where T : class
            {
                if (!_loadedAssemblies)
                    LoadAssemblies();
    
                //TODO: improve the performance vastly by caching RuntimeTypeHandles
    
                foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    foreach (Type type in assembly.GetTypes())
                    {
                        if (type.IsClass && typeof(T).IsAssignableFrom(type))
                            return Activator.CreateInstance(type) as T;
                    }
                }
    
                return null;
            }
    
            private static void LoadAssemblies()
            {
                foreach (FileInfo file in new DirectoryInfo(Directory.GetCurrentDirectory()).GetFiles())
                {
                    if (file.Extension != ".DLL")
                        continue;
    
                    if (!AppDomain.CurrentDomain.GetAssemblies().Any(a => a.Location == file.FullName))
                    {
                        try
                        {
                            //TODO: perhaps filter by certain known names
                            Assembly.LoadFrom(file.FullName);
                        }
                        catch { }
                    }
                }
            }
        }
    }
    
  4. สุดท้ายใช้อินเทอร์เฟซเหล่านี้ในแอสเซมบลีอื่นของคุณโดยการเรียกใช้วิธี FindInterface นี่คือตัวอย่างของCommandLineParser:

    static class CommandLineParser
    {
        public static string ParseCommandLine(string commandLine)
        {
            string parsedCommandLine = ParseInternal(commandLine);
    
            IStringColorer colorer = PluginFinder.FindInterface<IStringColorer>();
    
            if(colorer != null)
                parsedCommandLine = colorer.Decorate(parsedCommandLine);
    
            return parsedCommandLine;
        }
    
        private static string ParseInternal(string commandLine)
        {
            //TODO: implement parsing as desired
            return commandLine;
        }
    

    }

สำคัญที่สุด: ทดสอบทดสอบทดสอบระหว่างการเปลี่ยนแปลงแต่ละครั้ง


ฉันเพิ่มตัวอย่าง! :-)
Kevin McCormick

1
คลาส PluginFinder นั้นดูน่าสงสัยเหมือนตัวจัดการ DI แบบม้วนของคุณเอง (ใช้รูปแบบ ServiceLocator) แต่นี่เป็นคำแนะนำที่ดี บางทีคุณอาจจะดีกว่าที่จะชี้ OP ที่สิ่งที่คล้าย Unity เนื่องจากจะไม่มีปัญหากับการใช้งานหลาย ๆ อินเทอร์เฟซเฉพาะภายในไลบรารี (StringColourer vs StringColourerWithHtmlWrapper หรืออะไรก็ตาม)
Ed James

@EdWoodcock จุดดี Ed และฉันไม่อยากจะเชื่อเลยว่าฉันไม่ได้นึกถึงรูปแบบของตัวระบุบริการขณะที่เขียนสิ่งนี้ PluginFinder เป็นการใช้งานที่ยังไม่บรรลุนิติภาวะและ DI Framework จะทำงานที่นี่อย่างแน่นอน
Kevin McCormick

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

@romkyns คุณกำลังเส้นทางอะไร ปล่อยให้เป็น - :)
Max

5

คุณสามารถใช้อินเตอร์เฟสที่ประกาศในไลบรารีเพิ่มเติม

ลองแก้ไขสัญญา (คลาสผ่านอินเทอร์เฟซ) โดยใช้การฉีดพึ่งพา (MEF, Unity ฯลฯ ) หากไม่พบให้ตั้งค่าให้ส่งคืนอินสแตนซ์ที่เป็นค่าว่าง
จากนั้นตรวจสอบว่าอินสแตนซ์นั้นเป็นโมฆะหรือไม่ในกรณีนี้คุณจะไม่ใช้ฟังก์ชันพิเศษ

นี่เป็นเรื่องง่ายโดยเฉพาะอย่างยิ่งกับ MEF เนื่องจากเป็นตำราที่ใช้สำหรับมัน

มันจะช่วยให้คุณสามารถรวบรวมไลบรารีด้วยค่าใช้จ่ายในการแยกมันออกเป็น n + 1 dll

HTH


ฟังดูเกือบจะถูกต้องแล้ว - ถ้ามันไม่เหมาะกับ DLL พิเศษตัวเดียวซึ่งโดยทั่วไปแล้วจะเหมือนกับโครงกระดูกของโครงร่างดั้งเดิมของสิ่งต่าง ๆ การใช้งานจะแยกออกทั้งหมด แต่ยังมี "ปึกโครงกระดูก" เหลืออยู่ ผมคิดว่ามีข้อได้เปรียบบางอย่าง แต่ฉันไม่เชื่อว่าข้อได้เปรียบที่มีค่าเกินค่าใช้จ่ายทั้งหมดสำหรับชุดนี้โดยเฉพาะของห้องสมุด ...
โรมัน Starkov

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

2

ฉันคิดว่าฉันโพสต์ตัวเลือกที่เป็นไปได้มากที่สุดที่เราคิดขึ้นมาเพื่อดูว่าความคิดคืออะไร

โดยพื้นฐานแล้วเราจะแยกแต่ละองค์ประกอบออกเป็นไลบรารีโดยไม่มีการอ้างอิงเป็นศูนย์ รหัสทั้งหมดที่ต้องมีการอ้างอิงจะถูกวางใน#if/#endifบล็อกที่มีชื่อที่เหมาะสม ตัวอย่างเช่นรหัสในCommandLineParserที่จับConsoleColoredStrings #if HAS_CONSOLE_COLORED_STRINGจะถูกวางลง

วิธีการแก้ปัญหาใด ๆ ที่ต้องการรวมเพียงแค่CommandLineParserสามารถทำได้อย่างง่ายดายเนื่องจากไม่มีการอ้างอิงเพิ่มเติม อย่างไรก็ตามหากวิธีการแก้ปัญหารวมถึงConsoleColoredStringโครงการตอนนี้โปรแกรมเมอร์มีตัวเลือกเพื่อ:

  • เพิ่มการอ้างอิงในCommandLineParserการConsoleColoredString
  • เพิ่มการHAS_CONSOLE_COLORED_STRINGกำหนดให้กับCommandLineParserไฟล์โครงการ

สิ่งนี้จะทำให้ฟังก์ชั่นที่เกี่ยวข้องพร้อมใช้งาน

มีปัญหาหลายประการเกี่ยวกับสิ่งนี้:

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

ค่อนข้างไม่สวย แต่ก็ยังเป็นสิ่งที่ใกล้เคียงที่สุดที่เราเคยพบ

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


1

ฉันจะแนะนำหนังสือBrownfield การพัฒนาโปรแกรมประยุกต์ในสุทธิ บทที่เกี่ยวข้องโดยตรงสองบทคือ 8 & 9 บทที่ 8 พูดถึงการถ่ายทอดใบสมัครของคุณในขณะที่บทที่ 9 พูดถึงการพึ่งพาการฝึกฝนการผกผันของการควบคุมและผลกระทบที่มีต่อการทดสอบ


1

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

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

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

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

ความคิดที่เป็นไปได้อีกข้อหนึ่งที่คุณสามารถมองได้ว่าอาจเป็นคำสั่งของขนาดที่ง่ายกว่าสำหรับการใช้งานของคุณคือการใช้รูปแบบสถาปัตยกรรมแบบท่อและตัวกรอง นี่คือสิ่งที่ทำให้ UNIX เป็นระบบนิเวศที่ยืนยาวและประสบความสำเร็จซึ่งรอดชีวิตมาได้และพัฒนามาเป็นเวลาครึ่งศตวรรษ ดูบทความนี้ในPipes and Filters ใน. NETสำหรับแนวคิดบางประการเกี่ยวกับวิธีการใช้รูปแบบชนิดนี้ในกรอบงานของคุณ


0

บางทีหนังสือ "การออกแบบซอฟต์แวร์ C ++ ขนาดใหญ่" โดย John Lakos มีประโยชน์ (แน่นอน C # และ C ++ หรือไม่เหมือนกัน แต่คุณสามารถกลั่นกรองเทคนิคที่มีประโยชน์จากหนังสือได้)

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.