سؤال

لقد قمت على مدار بعض المشاريع بتطوير نمط لإنشاء كائنات غير قابلة للتغيير (للقراءة فقط) ورسوم بيانية للكائنات غير قابلة للتغيير.تتميز الكائنات غير القابلة للتغيير بكونها آمنة بنسبة 100% للخيوط، وبالتالي يمكن إعادة استخدامها عبر الخيوط.كثيرًا ما أستخدم هذا النمط في عملي في تطبيقات الويب لإعدادات التكوين والكائنات الأخرى التي أقوم بتحميلها وتخزينها مؤقتًا في الذاكرة.يجب أن تكون الكائنات المخزنة مؤقتًا غير قابلة للتغيير دائمًا لأنك تريد ضمان عدم تغييرها بشكل غير متوقع.

الآن، يمكنك بالطبع تصميم كائنات غير قابلة للتغيير بسهولة كما في المثال التالي:

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

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

إذًا كيف يمكنك إنشاء كائنات غير قابلة للتغيير باستخدام أدوات الضبط؟

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

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

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

فئة العنصر هي التنفيذ الافتراضي لواجهة IElement:

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

دعونا نعيد بناء فئة SampleElement أعلاه لتنفيذ نمط الكائن غير القابل للتغيير:

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

يمكنك الآن تغيير خاصية المعرف وخاصية الاسم طالما لم يتم وضع علامة على الكائن على أنه غير قابل للتغيير عن طريق استدعاء الأسلوب MakeReadOnly().بمجرد أن يصبح غير قابل للتغيير، سيؤدي استدعاء أداة الضبط إلى الحصول على ImmutableElementException.

ملاحظة أخيرة:النمط الكامل أكثر تعقيدًا من مقتطفات التعليمات البرمجية الموضحة هنا.ويحتوي أيضًا على دعم لمجموعات الكائنات غير القابلة للتغيير والرسوم البيانية للكائنات الكاملة للرسوم البيانية للكائنات غير القابلة للتغيير.يمكّنك النمط الكامل من تحويل الرسم البياني للكائن بالكامل إلى شكل غير قابل للتغيير عن طريق استدعاء الأسلوب MakeReadOnly() على الكائن الخارجي.بمجرد البدء في إنشاء نماذج كائنات أكبر باستخدام هذا النمط، يزداد خطر تسرب الكائنات.الكائن المتسرب هو كائن يفشل في استدعاء الأسلوب FailIfImmutable() قبل إجراء تغيير على الكائن.لاختبار التسربات، قمت أيضًا بتطوير فئة عامة لكشف التسرب لاستخدامها في اختبارات الوحدة.يستخدم الانعكاس لاختبار ما إذا كانت جميع الخصائص والأساليب ستطرح ImmutableElementException في الحالة غير القابلة للتغيير.وبعبارة أخرى يتم استخدام TDD هنا.

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

هل كانت مفيدة؟

المحلول

لمزيد من المعلومات، ويسمى النهج الثاني "ثبات المصاصة".

واريك ليبرت لديه سلسلة من مقالات بلوق على ثبات بدءا <لأ href = "http://blogs.msdn.com/ericlippert/archive/2007/11/13/immutability-in-c-part-one-kinds من ناتجها immutability.aspx "يختلط =" noreferrer "> هنا . أنا لا يزال الحصول على السيطرة على CTP (C # 4.0)، لكنها تبدو مثيرة للاهتمام ما الاختيارية / عين المعلمات (لل.ctor) قد تفعل هنا (عند تعيينها إلى الحقول للقراءة فقط) ... [تحديث: لقد المدون على هذا rel="noreferrer"> ]

لمزيد من المعلومات، وأنا ربما لا تجعل تلك الأساليب virtual - ونحن ربما لا تريد فرعية أن تكون قادرة على جعله غير freezable. إذا كنت تريد لها أن تكون قادرا على إضافة رمز إضافي، كنت تشير الى شيء من هذا القبيل:

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

وأيضا - اوب (مثل PostSharp) قد يكون خيارا قابلا للتطبيق لإضافة كل تلك ThrowIfFrozen () الشيكات

.

و(الاعتذار إذا كنت قد غيرت أسماء المصطلحات / طريقة - SO لا تبقي آخر الأصلي مرئية عند إنشاء ردود)

نصائح أخرى

وثمة خيار آخر يتمثل في خلق نوع من الدرجة البناء.

لمثال، في جافا (وC # و العديد من اللغات الأخرى) سلسلة هي ثابتة. إذا كنت ترغب في القيام بعمليات متعددة لإنشاء سلسلة كنت تستخدم ب StringBuilder. هذا هو قابلة للتغيير، ومن ثم بمجرد الانتهاء من ذلك لديك العودة إلى الجسم المراد سلسلة النهائي. ومنذ ذلك الحين انها غير قابل للتغيير.

هل يمكن أن تفعل شيئا من هذا القبيل لفئات الأخرى. لديك عنصر غير قابل للتغيير الخاص بك، وبعد ذلك ElementBuilder. جميع باني سوف القيام به هو تخزين الخيارات التي قمت بتعيينها، ثم عند الانتهاء من ذلك أنه يبني وإرجاع عنصر غير قابل للتغيير.

وانها رمز أكثر قليلا، ولكن أعتقد أنه أنظف من وجود اضعي على فئة من المفترض أن يكون غير قابل للتغيير.

وبعد تعبي الأولي عن حقيقة أنه كان على خلق System.Drawing.Point جديدة على كل تعديل، لقد تبنت بالكامل مفهوم قبل بضع سنوات. في الحقيقة، أنا الآن إنشاء كل مجال كما readonly افتراضيا فقط تغييره لتكون قابلة للتغيير إذا كان هناك سبب مقنع - التي لا يوجد من المستغرب نادرا

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

وأنت لا تزال تتعامل مع الدولة، وبالتالي لا يزال من الممكن للعض إذا تم بشكل متوازي الأشياء الخاصة بك قبل ان تتم غير قابل للتغيير.

ويمكن أن تكون هناك طريقة أكثر وظيفية للعودة مثيل جديد من وجوه مع كل اضع. أو إنشاء كائن قابلة للتغيير وتمرير ذلك في منشئ.

وو(نسبيا) جديد يسمى تصميم مدفوعة المجال نموذج تصميم البرمجيات، ويجعل التمييز بين الأشياء كيان والأشياء القيمة.

يتم تعريف

وكائنات الكيان كما أي شيء له لتعيين كائن مدفوعة رئيسيا في مخزن البيانات الثابتة، مثل موظف، أو عميل، أو فاتورة، الخ ... حيث تغيير خصائص الكائن يعني أن تحتاج إلى حفظ التغيير إلى مخزن بيانات في مكان ما، ووجود حالات متعددة من فئة مع "مفتاح" نفس imnplies حاجة إلى مزامنتها، أو تنسيق استمرارها إلى مخزن البيانات بحيث أن "التغييرات حالة واحدة لا الكتابة الآخرين. تغيير خصائص كائن كيان يعني كنت تقوم بتغيير شيء حول الكائن - لا تغيير في اي الكائن الذي يتم الرجوع ...

والأشياء القيمة OTOH، هي الكائنات التي يمكن اعتبارها غير قابل للتغيير، الذي يعرف بدقة قيم ممتلكاتهم، والتي لحالات متعددة، لا تحتاج إلى تنسيق بأي شكل من الأشكال ... مثل عناوين أو أرقام الهاتف فائدة، أو العجلات على سيارة، أو الحروف في وثيقة ... وتعرف هذه الأشياء تماما من ممتلكاتهم ... الأحرف كبيرة 'A' الكائن في محرر نص يمكن أن يكون متبادل بشفافية مع أي الأحرف الكبيرة الأخرى 'A' يعترض على مدار الوثيقة، التي لا تحتاج إلى مفتاح لتمييزه عن سائر "A في هذا المعنى هو غير قابل للتغيير، لأنه إذا كنت تغييره إلى 'B' (تماما مثل تغيير سلسلة رقم الهاتف في كائن رقم الهاتف، لا تقوم بتغيير البيانات المرتبطة ببعض كيان قابلة للتغيير، يمكنك التبديل من قيمة واحدة إلى أخرى ... تماما كما عند تغيير قيمة سلسلة ...

وSystem.String هو مثال جيد من فئة ثابتة مع واضعي وطرق تحور، إلا أن كل طريقة تحور بإرجاع مثيل جديد.

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

وهذا ما لا يعني أن في بعض الأحيان كنت تريد أن تعطي (قابلة للتغيير) كيان لشيء والتأكد من أنها ليست على وشك أن يتغير.

وهكذا، بدلا من تجميد الكائن نفسه، هناك احتمال آخر هو لنسخ دلالات ReadOnlyCollection

List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

وجوه الخاص بك يمكن أن تأخذ جزء منها قابلة للتغيير عندما يحتاج إليها، ومن ثم يكون غير قابل للتغيير عندما تريدها أن تكون.

لاحظ أن ReadOnlyCollection تنفذ أيضا ICollection التي لديها وسيلة Add( T item) في الواجهة. ولكن هناك أيضا bool IsReadOnly { get; } المحددة في واجهة بحيث يمكن للمستهلكين الاختيار قبل استدعاء الأسلوب الذي سوف يلقي استثناء.

والفرق هو أنه لا يمكنك تعيين فقط IsReadOnly إلى false. مجموعة إما هو أو لا تقرأ فقط، وأنه لا يتغير أبدا لعمر المجموعة.

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


على ICloneable - يمكنك اعتقدت أن مجرد الرجوع إلى ما يلي:

<اقتباس فقرة>   

ولا تطبيق ICloneable

     

لا تستخدم ICloneable في واجهات برمجة التطبيقات العامة

براد ابرامز - إرشادات التصميم، رمز وإدارتها. الإطار NET

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

يمكنك إنشاء فئة جزئية تحتوي على جميع الخصائص القابلة للتجميد.سيكون من السهل جدًا إنشاء قالب T4 قابل لإعادة الاستخدام لهذا الغرض.

سيأخذ القالب هذا للإدخال:

  • مساحة الاسم
  • اسم الفصل
  • قائمة اسم الخاصية/نوع الصفوف

وسيتم إخراج ملف C#، يحتوي على:

  • إعلان مساحة الاسم
  • فئة جزئية
  • كل خاصية، مع الأنواع المقابلة لها، وحقل دعم، وgetter، وSetter الذي يستدعي طريقة FailIfFrozen

يمكن أيضًا أن تعمل علامات AOP على الخصائص القابلة للتجميد، ولكنها تتطلب المزيد من التبعيات، في حين أن T4 مدمج في الإصدارات الأحدث من Visual Studio.

السيناريو الآخر الذي يشبه إلى حد كبير هذا هو INotifyPropertyChanged واجهه المستخدم.من المحتمل أن تكون حلول هذه المشكلة قابلة للتطبيق على هذه المشكلة.

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

وهذا هو السبب في أنني سيمتد هذا النمط الترميز مع ضبط وقت الترجمة في شكل فئة عامة، مثل هذا:

public class Immutable<T> where T : IElement
{
    private T value;

    public Immutable(T mutable) 
    {
        this.value = (T) mutable.Clone();
        this.value.MakeReadOnly();
    }

    public T Value 
    {
        get 
        {
            return this.value;
        }
    }

    public static implicit operator Immutable<T>(T mutable) 
    {
        return new Immutable<T>(mutable);
    }

    public static implicit operator T(Immutable<T> immutable)
    {
        return immutable.value;
    }
}

وهنا عينة كيف ستستخدم هذه:

// All elements of this list are guaranteed to be immutable
List<Immutable<SampleElement>> elements = 
    new List<Immutable<SampleElement>>();

for (int i = 1; i < 10; i++) 
{
    SampleElement newElement = new SampleElement();
    newElement.Id = Guid.NewGuid();
    newElement.Name = "Sample" + i.ToString();

    // The compiler will automatically convert to Immutable<SampleElement> for you
    // because of the implicit conversion operator
    elements.Add(newElement);
}

foreach (SampleElement element in elements)
    Console.Out.WriteLine(element.Name);

elements[3].Value.Id = Guid.NewGuid();      // This will throw an ImmutableElementException

ومجرد تلميح لتبسيط خصائص العنصر: استخدام <لأ href = "http://blogesh.wordpress.com/2008/02/09/property-shortcuts-in-c-30/" يختلط = "نوفولو noreferrer "> خصائص التلقائي مع private set وتجنب صراحة معلنا حقل البيانات. منها مثلا.

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}

وهنا شريط فيديو جديد على قناة 9 حيث أندرس هيلسبرغ من 36:30 في مقابلة يبدأ الحديث عن ثبات في C #. وقال انه يعطي حالة استخدام جيدة جدا للثبات المصاصة، ويشرح كيف أن هذا أمر مطلوب منك حاليا لتنفيذ نفسك. وكانت الموسيقى إلى أذني سماع وسلم يقول يجدر التفكير في تقديم دعم أفضل لخلق الرسوم البيانية وجوه ثابتة في الإصدارات المستقبلية من C #

خبير إلى خبير: أندرس هيلسبرغ - مستقبل C #

هناك خياران آخران لمشكلتك الخاصة التي لم تتم مناقشتها:

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

  2. ضع مُنشئًا في كل فئة يأخذ XElement (أو نكهة أخرى لنموذج كائن XML) ويملأ نفسه منه.من الواضح أنه مع زيادة عدد الفئات، يصبح هذا حلاً أقل استحسانًا.

وماذا عن وجود ThingBase فئة مجردة، مع الفئات الفرعية MutableThing وImmutableThing؟ ThingBase أن تحتوي على جميع البيانات في هيكل الحماية، وتوفير خصائص للقراءة فقط العامة للحقول وحمايتها خاصية للقراءة فقط لهيكلها. كما أنه يوفر طريقة AsImmutable للتجاوز الذي سيعود على ImmutableThing.

وMutableThing أن الظل الخصائص مع خصائص القراءة / الكتابة، وتقديم كل من منشئ افتراضي ومنشئ يقبل ThingBase.

سوف

وشيء غير قابل للتغيير أن يكون فئة مغلقة أن يتجاوز AsImmutable إلى العودة ببساطة في حد ذاته. ومن شأنه أيضا أن توفر منشئ يقبل ThingBase.

وأنا لا أحب فكرة أن تكون قادرة على تغيير كائن من قابلة للتغيير إلى حالة ثابتة، وهذا النوع من يبدو لهزيمة نقطة من تصميم لي. عندما أنت تحتاج إلى أن تفعل ذلك؟ الأشياء الوحيدة التي تمثل القيم يجب أن تكون غير قابلة للتغيير

ويمكنك استخدام الوسائط المسماة اختياري مع nullables لجعل اضع ثابتة مع النمطي القليل جدا. إذا كنت حقا لا تريد أن تعيين خاصية فارغة ثم قد تضطر بعض مزيد من المتاعب.

class Foo{ 
    ...
    public Foo 
        Set
        ( double? majorBar=null
        , double? minorBar=null
        , int?        cats=null
        , double?     dogs=null)
    {
        return new Foo
            ( majorBar ?? MajorBar
            , minorBar ?? MinorBar
            , cats     ?? Cats
            , dogs     ?? Dogs);
    }

    public Foo
        ( double R
        , double r
        , int l
        , double e
        ) 
    {
        ....
    }
}

ويمكنك أن استخدامها مثل ذلك

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top