模板方法是一种强大的设计模式,它允许我们为算法创建一个结构,同时仍能让算法的各个部分可互换

1.定义:
  • 在一个方法中定义一个算法的骨架:

  • 在这个方法中,我们将某些步骤推迟到子类中实现

  • 模板方法允许子类在不改变算法结构的前提下,重新定义该算法的某些特定步骤

  • 举个例子:制作咖啡

    • 我们思考一种制作咖啡的通用算法,将它划分为几个明确的步骤

    • 1.加水BoilWater(),2.加入咖啡粉AddCoffee(),3.加入牛奶AddMilk(),4.加入糖AddSugar()

    • 我们现在拥有一个制作咖啡算法的骨架,关键在于,我们会将其中某些步骤推迟到子类中实现。在制作咖啡算法中,有些步骤是必须执行的,有些步骤不一定执行,或者是可定制的。

      • 我们必须加水,必须加咖啡,可选加牛奶,可选加糖。

      • 烧水对于所有咖啡制作都是完全一样的(必须且不可配置),但是加入咖啡会因为咖啡的种类而不同(必须且可以配置)

    • 模板方法模式,为我们定义了一种结构。我们可以定义一个方法,这个方法定义了算法的骨架,在制作咖啡这个算法中的骨架是:

      • BoilWater()---->AddCoffee()---->AddMilk()---->AddSugar()

      • 其中一些方法必须由子类实现,另一些方法可以由子类选择性地实现

      • 算法的骨架,将定义在某个抽象类中,其中一些方法会在抽象类中实现,但是另一些方法则会委托给子类,子类必须实现这些方法。还有一些方法,子类可选的去实现。

因为我们讨论的是不同对象之间如何分配职责;这种设计模式属于“行为型模式”

2.类图

  • 在抽象类中

    • 定义算法骨架TemplateMethod()方法。--->MakeCoffee()

    • 定义子类必须实现的方法 PrimitiveOperation()方法,是子类必须实现的基本操作--->AddCoffee()

    • 定义可选的钩子方法 Hook()--->AddMilk()--->AddSugar()

  • 在具体类中

    • 必须实现基本操作

    • 可选的实现钩子操作

  • 客户端Client

    • 客户段调用 具体类.MakeCoffee(),但是整个算法的骨架定义在抽象类中,而具体类实现了个性化的部分

3.优缺点:
3.1优点:
  • 代码复用:我们在所有实现中都复用了完全相同的代码。我们只需要一次性正确地构建好算法的结构,为子类提供合适的插入点,让它们配置各自需要配置的内容,就能复用完全相同的代码。如果子类的算法实现逻辑确实是通用的,这将是一个巨大的优势,这能显著减少代码重复。

  • 算法结构的一致性:你只需要一次性地定义好算法结构,就能确保它在所有子类中被一致地复用。

  • 可拓展性和灵活性:作为子类,你可以非常轻松地插入,并精确修改你想改变的部分。你可以在抽象类中设置多个钩子Hook(),将它们嵌入到算法流程中,然后子类就能轻松地插入并添加自己的逻辑。

    • 在抽象类中:可以通过各个钩子,灵活地定制行为

    • 在具体类中:可以非常轻松地添加新的具体类来继承抽象类,而它们只需要实现自己需要修改的部分即可。仅仅通过继承基类,新类就能立即获得大量开箱即用的功能。

  • 抽象类完全掌控算法流程:算法中不变的部分被封装在模板方法内部,即使算法本身相当复杂,子类也无需了解所有内部细节,只需关注可以插入的钩子即可。如果设计得当,具体类的逻辑会非常简单,但它能执行很复杂的操作。

3.2缺点:
  • 依赖继承:依赖继承的系统,其逻辑分散在类的继承层次结构中,导致整个系统的逻辑,新人上手等各方面变的更加复杂

  • 算法固定:当你确保存在一个结构清晰,顺序步骤始终不变,且不太可能改变的算法时,才使用模板方法模式。如果你的算法很灵活,经常发生改变,模板方法可能就不是一个适合你的设计模式。

模板方法模式在Nodify中的使用:

InputElementState<TElement>

  • 抽象类InputElementState<TElement>是模板类,定义了算法骨架。

    • 算法骨架TemplateMethod()---->HandlerEvent()

      • 算法骨架中,根据传入的InputEventArgs进行判断,执行不同的分支

    • 可选的钩子方法 Hook()---->OnMouseDown(),OnMouseMove(),OnEvent()等等

      • 子类可选的重写钩子方法,定制自己个性化的逻辑

​
public abstract class InputElementState<TElement> : IInputHandler
    where TElement : FrameworkElement
{
    protected TElement Element { get; }
​
    public bool RequiresInputCapture { get; protected set; }
    public bool ProcessHandledEvents { get; protected set; }
​
    protected InputElementState(TElement element)
    {
        Element = element;
    }
​
    protected virtual void OnMouseDown(MouseButtonEventArgs e) { }
​
    protected virtual void OnMouseUp(MouseButtonEventArgs e) { }
​
    protected virtual void OnMouseMove(MouseEventArgs e) { }
​
    protected virtual void OnMouseWheel(MouseWheelEventArgs e) { }
​
    protected virtual void OnKeyUp(KeyEventArgs e) { }
​
    protected virtual void OnKeyDown(KeyEventArgs e) { }
​
    protected virtual void OnLostMouseCapture(MouseEventArgs e) { }
​
    protected virtual void OnEvent(InputEventArgs e) { }
​
    public void HandleEvent(InputEventArgs e)
    {
        if (e.RoutedEvent == UIElement.MouseMoveEvent)
        {
            OnMouseMove((MouseEventArgs)e);
        }
        else if (e.RoutedEvent == UIElement.MouseDownEvent)
        {
            OnMouseDown((MouseButtonEventArgs)e);
        }
        else if (e.RoutedEvent == UIElement.MouseUpEvent)
        {
            OnMouseUp((MouseButtonEventArgs)e);
        }
        else if (e.RoutedEvent == UIElement.MouseWheelEvent)
        {
            OnMouseWheel((MouseWheelEventArgs)e);
        }
        else if (e.RoutedEvent == UIElement.LostMouseCaptureEvent)
        {
            OnLostMouseCapture((MouseEventArgs)e);
        }
        else if (e.RoutedEvent == UIElement.KeyDownEvent)
        {
            OnKeyDown((KeyEventArgs)e);
        }
        else if (e.RoutedEvent == UIElement.KeyUpEvent)
        {
            OnKeyUp((KeyEventArgs)e);
        }
​
        OnEvent(e);
    }
}

DragState<TElement>

  • 进一步扩展了模板方法,所以继承自DragState的类,先进行显式接口实现模板方法流程,然后调用基类中的模板方法流程

using System.Windows;
using System.Windows.Input;
​
namespace Nodify.Interactivity
{
    public abstract class DragState<TElement> : InputElementState<TElement>, IInputHandler
        where TElement : FrameworkElement
    {
        private enum InteractionState
        {
    
            Ready,
​
            InProgress,
​
            Ending
        }
​
        protected InputGesture? CancelGesture { get; }
​
        protected InputGesture BeginGesture { get; }
​
        protected virtual bool HasContextMenu => Element.ContextMenu != null;
​
        protected virtual bool CanBegin { get; } = true;
​
        protected virtual bool CanCancel { get; } = true;
​
        protected virtual bool IsToggle { get; }
​
        protected IInputElement PositionElement { get; set; }
​
        private InteractionState _interactionState;
        private Point _initialPosition;
​
        public DragState(TElement element, InputGesture beginGesture) : base(element)
        {
            BeginGesture = beginGesture;
            PositionElement = element;
        }
        public DragState(TElement element, InputGesture beginGesture, InputGesture cancelGesture)
            : this(element, beginGesture)
        {
            CancelGesture = cancelGesture;
        }
​
        void IInputHandler.HandleEvent(InputEventArgs e)
        {
            if (_interactionState == InteractionState.Ready && TryBeginDragging(e))
            {
                return;
            }
​
            if (_interactionState == InteractionState.Ending && TryEndDragging(e))
            {
                return;
            }
​
            if (_interactionState == InteractionState.InProgress)
            {
                if (TryEndDragging(e) || TryCancelDragging(e) || TrySuppressContextMenu(e))
                {
                    return;
                }
            }
​
            TryHandleEvent(e);
        }
​
        #region Interaction logic
​
        // Begin the interaction on gesture press
        private bool TryBeginDragging(InputEventArgs e)
        {
            if (IsInputEventPressed(e) && CanBegin && BeginGesture.Matches(e.Source, e))
            {
                BeginDrag(e);
                return true;
            }
​
            return false;
        }
​
        private bool TryEndDragging(InputEventArgs e)
        {
            if (IsInputCaptureLost(e))
            {
                EndDrag(e);
                return true;
            }
​
            if (IsToggle && _interactionState == InteractionState.InProgress)
            {
                return TryDeferToggleInteractionEnd(e);
            }
​
            return TryEndInteraction(e);
        }
​
        // Delay ending toggle interaction until the gesture is released
        private bool TryDeferToggleInteractionEnd(InputEventArgs e)
        {
            if (IsInputEventPressed(e) && BeginGesture.Matches(e.Source, e))
            {
                _interactionState = InteractionState.Ending;
                HandleEvent(e);
                return true;
            }
​
            return false;
        }
​
        // End the interaction on gesture release
        private bool TryEndInteraction(InputEventArgs e)
        {
            if (IsInputEventReleased(e) && BeginGesture.Matches(e.Source, e))
            {
                EndDrag(e);
                return true;
            }
​
            return false;
        }
​
        // Cancel the interaction
        private bool TryCancelDragging(InputEventArgs e)
        {
            if (CanCancel && IsInputEventReleased(e) && CancelGesture?.Matches(e.Source, e) is true)
            {
                CancelDrag(e);
                return true;
            }
​
            return false;
        }
​
        // Suppress the context menu if a toggle interaction is in progress
        private bool TrySuppressContextMenu(InputEventArgs e)
        {
            if (IsToggle && e is MouseButtonEventArgs mbe && mbe.ChangedButton == MouseButton.Right)
            {
                e.Handled = true;
                HandleEvent(e);
                return true;
            }
​
            return false;
        }
​
        private void TryHandleEvent(InputEventArgs e)
        {
            if (_interactionState == InteractionState.InProgress || _interactionState == InteractionState.Ending)
            {
                HandleEvent(e);
            }
        }
​
        internal void BeginDrag(InputEventArgs e)
        {
            // Avoid stealing mouse capture from other elements
            if (CanCaptureInput(e))
            {
                RequiresInputCapture = IsToggle;
​
                _interactionState = InteractionState.InProgress;
                _initialPosition = GetInitialPosition(e);
​
                HandleEvent(e); // Handle the event, otherwise CaptureMouse will send a MouseMove event and the current event will be handled out of order
                OnBegin(e);
​
                e.Handled = true;
​
                Element.Focus();
                CaptureInput(e);
            }
        }
​
        private void EndDrag(InputEventArgs e)
        {
            _interactionState = InteractionState.Ready;
            HandleEvent(e);
​
            // Suppress the context menu if the mouse moved beyond the defined drag threshold
            if (HasContextMenu && e is MouseButtonEventArgs mbe && mbe.ChangedButton == MouseButton.Right)
            {
                double dragThreshold = NodifyEditor.MouseActionSuppressionThreshold * NodifyEditor.MouseActionSuppressionThreshold;
                double dragDistance = (mbe.GetPosition(PositionElement) - _initialPosition).LengthSquared;
​
                if (dragDistance > dragThreshold)
                {
                    OnEnd(e);
                    e.Handled = true;
                }
                else
                {
                    OnCancel(e);
                }
            }
            else
            {
                OnEnd(e);
                e.Handled = true;
            }
​
            RequiresInputCapture = false;
        }
​
        private void CancelDrag(InputEventArgs e)
        {
            _interactionState = InteractionState.Ready;
            HandleEvent(e);
            OnCancel(e);
​
            e.Handled = true;
            RequiresInputCapture = false;
        }
​
        #endregion
​
        protected virtual Point GetInitialPosition(InputEventArgs e)
        {
            if (e is MouseEventArgs me)
            {
                return me.GetPosition(PositionElement);
            }
​
            return default;
        }
​
        protected virtual bool CanCaptureInput(InputEventArgs e)
            => Mouse.Captured == null || Element.IsMouseCaptured;
​
        protected virtual void CaptureInput(InputEventArgs e)
            => Element.CaptureMouse();
​
        protected virtual bool IsInputCaptureLost(InputEventArgs e)
            => e.RoutedEvent == UIElement.LostMouseCaptureEvent;
​
        protected virtual bool IsInputEventReleased(InputEventArgs e)
        {
            if (e is MouseButtonEventArgs mbe && mbe.ButtonState == MouseButtonState.Released)
                return true;
​
            if (e is KeyEventArgs ke && ke.IsUp)
                return true;
​
            if (e is MouseWheelEventArgs mwe && mwe.MiddleButton == MouseButtonState.Released)
                return true;
​
            return false;
        }
        protected virtual bool IsInputEventPressed(InputEventArgs e)
        {
            if (e is MouseButtonEventArgs mbe && mbe.ButtonState == MouseButtonState.Pressed)
                return true;
​
            if (e is KeyEventArgs ke && ke.IsDown)
                return true;
​
            if (e is MouseWheelEventArgs mwe && mwe.MiddleButton == MouseButtonState.Pressed)
                return true;
​
            return false;
        }
        protected virtual void OnBegin(InputEventArgs e)
        {
        }
        protected virtual void OnEnd(InputEventArgs e)
        {
        }
        protected virtual void OnCancel(InputEventArgs e)
        {
        }
    }
}
​
  • IInputHandler.HandleEvent:模板方法算法骨架

  • 一些钩子函数(子类具体实现):OnBegin(),OnEnd(),OnCancel()

  • 一些钩子属性(子类实现,用于控制算法分支):HasContextMenu,CanBegin,CanCancel,IsToggle,

  • 状态模式和钩子属性和手势判断:共同决定算法程序流。

    • 定义了三种状态:

      • Ready:空闲,等待开始拖拽

      • InProgress:拖拽进行中

      • Ending:假设有一个按钮,按下可以切换成抓取状态,此时可以抓取移动图表,再次按下按钮结束抓取状态,Ending表示结束抓取状态之前。

  • 当用户触发的UI元素的行为,是继承自DragState时,进入IInputHandler.HandleEvent这个算法骨架中。

  • 根据一些程序流,调用InputElementState中的模板,进入InputElementState的HandleEvent这个算法骨架中。

具体类Zooming:继承自InputElementState

using System;
using System.Windows.Input;
​
namespace Nodify.Interactivity
{
    public static partial class EditorState
    {
        public class Zooming : InputElementState<NodifyEditor>
        {
            public Zooming(NodifyEditor editor) : base(editor)
            {
            }
​
            protected override void OnMouseWheel(MouseWheelEventArgs e)
            {
                EditorGestures.NodifyEditorGestures gestures = EditorGestures.Mappings.Editor;
                if (gestures.ZoomModifierKey == Keyboard.Modifiers && IsZoomingAllowed())
                {
                    double zoom = Math.Pow(2.0, e.Delta / 3.0 / Mouse.MouseWheelDeltaForOneLine);
                    Element.ZoomAtPosition(zoom, Element.MouseLocation);
                    e.Handled = true;
                }
            }
​
            private bool IsZoomingAllowed()
            {
                return !Element.DisableZooming
                    && (AllowZoomingWhileSelecting || !Element.IsSelecting)
                    && (AllowZoomingWhileCutting || !Element.IsCutting)
                    && (AllowZoomingWhilePushingItems || !Element.IsPushingItems)
                    && (AllowZoomingWhilePanning || !Element.IsPanning);
            }
        }
    }
}
​
  • Zooming重写了InputElementState中的OnMouseWheel方法,计算缩放比例,改变Element的视口

具体类Selecting:继承自DragState

using System.Windows.Input;
​
namespace Nodify.Interactivity
{
    public static partial class EditorState
    {
        public class Selecting : DragState<NodifyEditor>
        {
            protected override bool HasContextMenu => Element.HasContextMenu;
            protected override bool CanBegin => Element.CanSelectMultipleItems && !Element.IsPanning && !Element.IsCutting && !Element.IsPushingItems;
            protected override bool CanCancel => NodifyEditor.AllowSelectionCancellation;
            protected override bool IsToggle => EnableToggledSelectingMode;
​
            public Selecting(NodifyEditor editor)
                : base(editor, EditorGestures.Mappings.Editor.Selection.Select, EditorGestures.Mappings.Editor.Selection.Cancel)
            {
            }
​
            protected override void OnBegin(InputEventArgs e)
            {
                var selectionType = EditorGestures.Mappings.Editor.Selection.GetSelectionType(e);
                Element.BeginSelecting(selectionType);
            }
​
            protected override void OnMouseMove(MouseEventArgs e)
                => Element.UpdateSelection(Element.MouseLocation);
​
            protected override void OnEnd(InputEventArgs e)
                => Element.EndSelecting();
​
            protected override void OnCancel(InputEventArgs e)
                => Element.CancelSelecting();
        }
    }
}
​
  • 重写了DragState算法骨架中的OnBegin(),OnEnd(),OnCancel(),重写了流程控制属性HasContextMenu,CanBegin,CanCancel,IsToggle

  • 重写了InputElementState算法骨架中的OnMouseMove()方法,更新选中。

其他具体类,类似,如果继承自抽象输入InputElementState类,则根据InputEventArgs,进入不同的OnMouseUP,OnMouseMove等分支。如果继承自DragState,则会有更复杂的行为,进入一个由状态机,控制流属性Bool,输入手势共同控制的算法流中,在必要时会调用InputElementState中的算法骨架,最终实现复杂的用户输入行为响应。

更多推荐