الاستخدام الإبداعي لطرق الامتداد في C #

مرحبا هبر!



استمرارًا لاستكشافنا لموضوع C # ، قمنا بترجمة المقالة القصيرة التالية بخصوص الاستخدام الأصلي لطرق الامتداد. نوصي بإيلاء اهتمام خاص للقسم الأخير المتعلق بالواجهات ، بالإضافة إلى ملف تعريف المؤلف.







أنا متأكد من أن أي شخص لديه خبرة بسيطة في C # يدرك وجود طرق الامتداد. هذه ميزة رائعة تسمح للمطورين بتوسيع الأنواع الحالية بطرق جديدة.



هذا مفيد للغاية في الحالات التي تريد فيها إضافة وظائف لأنواع لا تتحكم فيها. في الواقع ، كان على أي شخص عاجلاً أم آجلاً كتابة امتداد لـ BCL فقط لتسهيل الوصول إلى الأشياء.



ولكن ، جنبًا إلى جنب مع حالات الاستخدام الواضحة نسبيًا ، هناك أيضًا أنماط مثيرة للاهتمام مرتبطة بشكل مباشر باستخدام طرق التمديد وتوضح كيف يمكن استخدامها بطريقة غير تقليدية.



إضافة طرق إلى التعداد



التعداد هو ببساطة مجموعة من القيم الرقمية الثابتة ، لكل منها اسم فريد. على الرغم من أن التعدادات في C # ترث من الفئة المجردة Enum ، إلا أنها لا تُفسر على أنها فئات حقيقية. على وجه الخصوص ، يمنعهم هذا القيد من امتلاك طرق.



في بعض الحالات ، قد يكون من المفيد برمجة المنطق في تعداد. على سبيل المثال ، إذا كان من الممكن أن توجد قيمة تعداد في عدة طرق عرض مختلفة وترغب في تحويل واحدة إلى أخرى بسهولة.



على سبيل المثال ، تخيل النوع التالي في تطبيق نموذجي يسمح لك بحفظ الملفات بتنسيقات مختلفة:



public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}


يحدد هذا التعداد قائمة التنسيقات التي يدعمها التطبيق ويمكن استخدامها في أجزاء مختلفة من التطبيق لبدء منطق التفريع بناءً على قيمة محددة.



نظرًا لأنه يمكن تمثيل كل تنسيق ملف كملحق ملف ، فسيكون من الجيد أن يكون FileFormatلدى كل تنسيق طريقة للحصول على هذه المعلومات. باستخدام طريقة الامتداد يمكن القيام بذلك ، شيء من هذا القبيل:



public static class FileFormatExtensions
{
    public static string GetFileExtension(this FileFormat self)
    {
        if (self == FileFormat.PlainText)
            return "txt";

        if (self == FileFormat.OfficeWord)
            return "docx";

        if (self == FileFormat.Markdown)
            return "md";

        //  ,      ,
        //      
        throw new ArgumentOutOfRangeException(nameof(self));
    }
}


والذي بدوره يسمح لنا بالقيام بذلك:



var format = FileFormat.Markdown;
var fileExt = format.GetFileExtension(); // "md"
var fileName = $"output.{fileExt}"; // "output.md"


فئات نموذج إعادة البناء



هناك أوقات لا تريد فيها إضافة طريقة مباشرة إلى الفصل ، على سبيل المثال ، إذا كنت تعمل مع نموذج فقر الدم .



عادة ما يتم تمثيل نماذج فقر الدم من خلال مجموعة من الخصائص العامة الثابتة ، get-only. لذلك ، فإن إضافة طرق إلى فئة النموذج قد تعطي الانطباع بأن نقاء الكود قد تم انتهاكه ، أو قد تشك في أن الأساليب تشير إلى حالة خاصة. لا تتسبب طرق الامتداد في حدوث هذه المشكلة ، حيث لا يمكنها الوصول إلى الأعضاء الخاصين في النموذج وليست بطبيعتها جزءًا من النموذج.



لذلك ، ضع في اعتبارك المثال التالي مع نموذجين ، أحدهما يمثل قائمة عناوين مغلقة والآخر يمثل صف عنوان منفصل:



public class ClosedCaption
{
    //  
    public string Text { get; }

    //       
    public TimeSpan Offset { get; }

    //       
    public TimeSpan Duration { get; }

    public ClosedCaption(string text, TimeSpan offset, TimeSpan duration)
    {
        Text = text;
        Offset = offset;
        Duration = duration;
    }
}

public class ClosedCaptionTrack
{
    // ,    
    public string Language { get; }

    //   
    public IReadOnlyList<ClosedCaption> Captions { get; }

    public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions)
    {
        Language = language;
        Captions = captions;
    }
}


في الحالة الحالية ، إذا احتجنا إلى عرض سلسلة الترجمة في وقت معين ، فسنقوم بتشغيل LINQ على النحو التالي:



var time = TimeSpan.FromSeconds(67); // 1:07

var caption = track.Captions
    .FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);


هذا يستدعي حقًا نوعًا من الطريقة المساعدة التي يمكن تنفيذها إما كطريقة عضو أو طريقة امتداد. أنا أفضل الخيار الثاني.



public static class ClosedCaptionTrackExtensions
{
    public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) =>
        self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
}


في هذه الحالة ، تسمح لك طريقة الامتداد بتحقيق نفس الطريقة المعتادة ، ولكنها تعطي عددًا من المكافآت غير الواضحة:



  1. من الواضح أن هذه الطريقة تعمل فقط مع أفراد الطبقة العامة ولا تغير حالتها الخاصة بطريقة غامضة.
  2. من الواضح أن هذه الطريقة تسمح لك فقط بقطع الزاوية ويتم توفيرها هنا للراحة فقط.
  3. تنتمي هذه الطريقة إلى فئة منفصلة تمامًا (أو حتى تجميع) هدفها فصل البيانات عن المنطق.


بشكل عام ، عند استخدام نهج طريقة الامتداد ، من الملائم رسم خط بين الضروري والمفيد.



جعل الواجهات متعددة الاستخدامات



عند تصميم واجهة ، فأنت تريد دائمًا تقليل العقد إلى الحد الأدنى ، لأنه يسهل تنفيذه. يساعد كثيرًا عندما توفر الواجهة وظائف بالطريقة الأكثر عمومية ، بحيث يمكن لزملائك (أو نفسك) البناء عليها للتعامل مع حالات أكثر تحديدًا.



إذا كان هذا يبدو لك هراء ، ففكر في واجهة نموذجية تحفظ نموذجًا في ملف:



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);
}


كل شيء يعمل بشكل جيد ، ولكن في غضون أسبوعين ، قد يأتي متطلب جديد في الوقت المناسب: IExportServiceلا يجب أن تقوم الفئات التي يتم تنفيذها بالتصدير إلى ملف فحسب ، بل يجب أيضًا أن تكون قادرة على الكتابة إلى ملف.



لذلك ، للوفاء بهذا المطلب ، أضفنا طريقة جديدة إلى العقد:



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);

    byte[] SaveToMemory(Model model);
}


أدى هذا التغيير للتو إلى كسر جميع التطبيقات الحالية IExportService، حيث إنها بحاجة الآن إلى التحديث لدعم الكتابة في الذاكرة أيضًا.



ولكن من أجل عدم القيام بكل هذا ، كان بإمكاننا تصميم الواجهة بشكل مختلف قليلاً عن البداية:



public interface IExportService
{
    void Save(Model model, Stream output);
}


في هذا النموذج ، تجبرك الواجهة على كتابة الوجهة بالصيغة الأكثر عمومية ، أي هذا Stream. الآن لم نعد مقيدًا بالملفات عند العمل ويمكننا أيضًا استهداف العديد من خيارات الإخراج الأخرى.



العيب الوحيد في هذا النهج هو أن معظم العمليات الأساسية ليست بهذه البساطة التي اعتدنا عليها: الآن علينا تعيين مثيل معين Stream، ولفه في تعليمة use وتمريره كمعامل.



لحسن الحظ ، يتم إلغاء هذا العيب تمامًا عند استخدام طرق الامتداد:



public static class ExportServiceExtensions
{
    public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)
    {
        using (var output = File.Create(filePath))
        {
            self.Save(model, output);
            return new FileInfo(filePath);
        }
    }

    public static byte[] SaveToMemory(this IExportService self, Model model)
    {
        using (var output = new MemoryStream())
        {
            self.Save(model, output);
            return output.ToArray();
        }
    }
}


من خلال إعادة هيكلة الواجهة الأصلية ، جعلناها أكثر تنوعًا ولم نضحي بقابلية الاستخدام باستخدام طرق الامتداد.



وبالتالي ، أجد طرق الامتداد أداة لا تقدر بثمن في الحفاظ على البساطة وتحويل المعقد إلى ممكن .



All Articles