Как управлять подписками на сменяемые классы

#c# #events #design-patterns

#c# #Мероприятия #шаблоны проектирования

Вопрос:

Я пытаюсь лучше понять, как поддерживать подписку на класс, который может меняться (стратегии изменения). Я постараюсь придерживаться этого направления, даже если примеры будут надуманными.

Предположим, что есть оболочка класса

 public class Skin
{
  //Raised when the form needs to turn on/off a blinking light
  public event BlinkEventHandler BlinkEvent;
  //The back color that forms should use
  public Color BackColor{ get; protected set; }
}
  

При запуске приложения оно прочитает каталог, полный файлов конфигурации для разных классов оболочки. Пользователь может переключить текущий скин в любое время.

В моей текущей работе используется очень странная стратегия (IMO), которая выглядит следующим образом:

 /// <summary>
/// Some class that can see when the Skin Changes
/// </summary>
public class SkinManager
{
  //Raised when the Skin changes
  public event SkinChangedEventHandler SkinChangedEvent;
  private static Skin currentSkin;
  public static Skin CurrentSkin {get;}

  public SkinManager(){/* gets a skin into currentSkin */}
  public void ChangeSkin()
  {
    //... do something to change the skin
    if(SkinChangedEvent != null)
    {
      SkinChangedEvent(this, new SkinChangedEventArgs(/*args*/));
    }
  }
}

/// <summary>
/// Some form that follows the Skinning Strategy
/// </summary>
public class SkinnedForm : Form
{
  private Skin skin;
  public SkinnedForm()
  {
    skin = SkinManager.CurrentSkin;
    if(skin != null)
    {
      skin.BlinkEvent  = OnBlink;
    }
    SkinManager.SkinChangedEvent  = OnSkinChanged;
  }

  private void OnSkinChanged(object sender, SkinChangedEventArgs e)
  {
    //unregister if we have a current skin
    //the local was to ensure that the form unsubscribes
    //when skin changes
    if(skin != null)
    {
       skin.BlinkEvent -= OnBlink;
    }
    skin = SkinManager.CurrentSkin;
    if(skin != null)
    {
       skin.BlinkEvent  = OnBlink;
    }
    SkinChanged();
  }

  private void SkinChanged(){ Invalidate(); }

  private void OnBlink(object sender, BlinkEventArgs e)
  {
     //... do something for blinking
  }
}
  

Я не могу поверить, что это хорошая реализация, и вместо этого хотел бы видеть что-то вроде этого:

 /// <summary>
/// Some class that can see when the Skin Changes
/// </summary>
public class SkinManager
{
  //Raised when the Skin changes
  public event SkinChangedEventHandler SkinChangedEvent;
  //Relays the event from Skin
  public event BlinkEventHander BlinkEvent;
  private static Skin currentSkin;
  public static Skin CurrentSkin {get;}

  public SkinManager()
  {
    //... gets a skin into currentSkin
    currentSkin.BlinkEvent  = OnBlink;
  }

  /// <summary>
  /// Relays the event from Skin
  /// </summary>
  private void OnBlink(object sender, BlinkEventArgs e)
  {
     if(BlinkEvent != null)
     {
       BlinkEvent(this, e);
     }
  }
  public void ChangeSkin()
  {
    //... do something to change the skin
    if(SkinChangedEvent != null)
    {
      SkinChangedEvent(this, new SkinChangedEventArgs(/*args*/));
    }
  }
}

/// <summary>
/// Some form that follows the Skinning Strategy
/// </summary>
public class SkinnedForm : Form
{
  //Do not need the local anymore
  //private Skin skin;
  public SkinnedForm()
  {
    SkinManager.CurrentSkin.BlinkEvent  = OnBlink;
    SkinManager.SkinChangedEvent  = OnSkinChanged;
  }

  private void OnSkinChanged(object sender, SkinChangedEventArgs e)
  {
    //Only register with the manager, so no need to deal with
    //subscription maintenance, could just directly to go SkinChanged();
    SkinChanged();
  }

  private void SkinChanged() { Invalidate(); }

  private void OnBlink(object sender, BlinkEventArgs e)
  {
     //... do something for blinking
  }
}
  

Я не уверен, что это понятно, но в основном существует локальная переменная, которая используется строго для того, чтобы мы отписывались от событий, прежде чем подписываться на события в новом классе. Я рассматриваю это следующим образом: мы внедрили шаблон стратегии для скининга (выберите стратегию скининга, которую вы хотите использовать, и запустите с ней), но у каждой реализации стратегии есть события, на которые мы напрямую подписываемся. Когда стратегия меняется, мы хотим, чтобы наши подписчики смотрели правильного издателя, поэтому мы используем локальные. Опять же, я думаю, что это ужасная методология.

Есть ли название для преобразования, которое я предложил, используя менеджер для отслеживания всех событий класса, которым он управляет, и передачи их, чтобы стратегия могла измениться, а подписчики продолжали прослушивать правильные уведомления о событиях? Предоставленный код был создан «на лету», когда я формировал вопрос, поэтому простите за любые ошибки.

Ответ №1:

Как правило, когда вы хотите создать прокси (оболочку) для класса, который запускает события, вам нужно отменить подписку (отсоединить) предыдущего экземпляра, поменять местами с новым, а затем подписаться (прикрепиться) к его событиям.

Допустим, ваш пользовательский интерфейс выглядит следующим образом:

 interface ISkin
{
    void RenderButton(IContext ctx);
    event EventHandler Blink;
}
  

Тогда часть, в которой вы это изменяете, должна выглядеть следующим образом:

 public void SetSkin(ISkin newSkin)
{
    // detach handlers from previous instance
    DetachHandlers();

    // swap the instance
    _skin = newSkin;

    // attach handlers to the new instance
    AttachHandlers();
}

void DetachHandlers()
{
    if (_skin != null)
       _skin.Blink -= OnBlink;
}

void AttachHandlers()
{
    if (_skin != null)
       _skin.Blink  = OnBlink;
}
  

Полный прокси-сервер выглядел бы примерно так:

 interface IChangeableSkin : ISkin
{
    event EventHandler SkinChanged;
}

public class SkinProxy : IChangeableSkin 
{
    private ISkin _skin; // actual underlying skin

    public void SetSkin(ISkin newSkin)
    {
        if (newSkin == null)
           throw new ArgumentNullException("newSkin");

        if (newSkin == _skin)
           return; // nothing changed

        // detach handlers from previous instance
        DetachHandlers();

        // swap the instance
        _skin = newSkin;

        // attach handlers to the new instance
        AttachHandlers();

        // fire the skin changed event
        SkinChanged(this, EventArgs.Empty);
    }

    void DetachHandlers()
    {
        if (_skin != null)
           _skin.BlinkEvent -= OnBlink;
    }

    void AttachHandlers()
    {
        if (_skin != null)
           _skin.BlinkEvent  = OnBlink;
    }

    void OnBlink(object sender, EventArgs e)
    {
        // just forward the event
        BlinkEvent(this, e);
    }

    // constructor
    public SkinProxy(ISkin initialSkin)
    {
        SetSkin(initialSkin);
    }


    #region ISkin members

    public void RenderButton(IContext ctx)
    {
        // just calls the underlying implementation
        _skin.RenderButton(ctx);
    }

    // this is fired inside OnBlink
    public event EventHandler BlinkEvent = delegate { }; 

    #endregion


    #region IChangeableSkin members

    public event EventHandler SkinChanged = delegate { }; 

    #region
}
  

Ваша форма должна содержать только ссылку на реализацию IChangeableSkin .

Комментарии:

1. Потрясающе, спасибо. Это именно то, что я предлагал, но хотел убедиться, что я не ошибся. Значит, это называется проксированием? По какой-то причине они называют это Provider здесь, но когда я посмотрел шаблон, он, похоже, не соответствовал.

Ответ №2:

Довольно сложно, и бремя переключения ложится на подписчика. Это не так уж хорошо.

При замене оболочек старая оболочка может удалить своих подписчиков на события и, вероятно, также присоединить их к новой оболочке.

Но более аккуратным шаблоном может быть держатель обложки, который не меняется и который предоставляет события.

Комментарии:

1. Я согласен, что это не очень хорошая ситуация с тем, что у вас есть. Отличается ли предлагаемое изменение от владельца обложки, который вы предлагаете? Нормально ли иметь менеджера также в качестве держателя?

2. Да, я думаю, ваш менеджер делает примерно то же самое.

Ответ №3:

SkinnedForm может иметь свойство типа iSkin —

 public class SkinnedForm : Form
{
  private ISkin _Skin;
  ...
}
  

Предоставьте это через общедоступное свойство и установите его в любой момент. Таким образом, SkinnedForm никогда не заботится о том, как работает iSkin, или о модели событий, которую он содержит. Когда вы передаете новую ссылку на класс оболочки, новое событие OnBlink автоматически вступит во владение. Классы, реализующие iSkin, должны содержать логику для OnBlink.

Затем у вас есть класс manager (не слишком далеко от того, что вы указали), который может получить ссылку на новый скин и соответствующую форму SkinnedForm. Единственной задачей менеджера является обновление свойства iSkin в SkinnedForm.

Комментарии:

1. Вау, я не знал, что сохранение локального интерфейса позволит мне управлять подписками. Чтобы проверить, вы это говорите… SkinnedForm(){_Skin = SkinManager. CurrentSkin; _Skin. BlinkEvent = OnBlink; GetDifferentSkin();} GetDifferentSkin(){_Skin = SomeOtherSkin;} После вызова GetDifferentSkin события все еще связаны?

2. Привет, не совсем. Я говорю, что в любой момент SkinnedForm должен иметь ссылку на один объект оболочки, который имеет единственную реализацию BlinkEvent. Если вы замените весь объект Skin, то нет необходимости отменять / перерегистрировать новые события. Особенно не в объекте SkinnedForm, его не должно волновать, как реализованы события оболочки, он должен знать только, что у него есть оболочка, и он может вызывать методы / вызывать события в этой оболочке.

3. Фух, на секунду подумал, что мне не хватает огромной части языка. Я провел некоторое тестирование и обнаружил, что действительно, он не выполняет автоматическую замену. Я полагаю, что ваше предложение похоже на то, что предложил Groo. Спасибо: D