Переопределение методов или назначение делегатов/событий в ООП

Это немного странный вопрос. Я хочу создать набор объектов (известных во время разработки), каждый из которых имеет определенные функции, связанные с ними. Я могу либо сделать это, указав свойства моих объектов, которые могут содержать "делегатов":

public class StateTransition {
    Func<bool> Condition { get; set; }
    Action ActionToTake { get; set; }
    Func<bool> VerifyActionWorked { get; set; }
}

StateTransition foo = new StateTransition {
    Condition = () => {//...}
    // etc
};

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

public abstract class StateTransition {
    public abstract bool Condition();
    public abstract void ActionToTake();
    public abstract bool VerifyActionWorked();
}

class Foo : StateTransition {
    public override bool Condition() {//...}
    // etc
}

Foo f = new Foo();

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

Как я могу выбрать, какой метод подходит для моего приложения?

Ответ 1

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

Ключевым фактором между ними является: кто контролирует, что происходит?

Если вызывающий абонент может на законных основаниях сделать что-либо там, тогда подход к мероприятию будет прекрасным. Система не заставляет вас подклассифицировать Button только для добавления того, что происходит, когда вы нажимаете на нее, в конце концов (хотя вы можете это сделать).

Если "вещи, которые могут произойти", достаточно контролируются, и вы не хотите, чтобы каждый вызывающий выполнял разные вещи, тогда подход подкласса более подходит. Это также позволяет избежать необходимости, чтобы каждый вызывающий имел, чтобы сказать, что делать, когда "что делать" может быть очень маленьким числом опций. Подход базового типа также дает возможность управлять подклассами, например, только имея конструктор internal для базового класса (так что только типы в одной и той же сборке или в узлах, отмеченные с помощью [InternalsVisibleTo(...)], могут быть подклассами она).

Вы также можете комбинировать два (переопределить vs event) с помощью:

public class StateTransition {
    public event Func<bool> Condition;
    protected virtual bool OnCondition() {
        var handler = Condition;
        return handler == null ? false : handler();
    }
    public event Action ActionToTake;
    protected virtual void OnActionToTake() {
        var handler = ActionToTake;
        if(handler != null) handler();
    }
    public event Func<bool> VerifyActionWorked;
    protected virtual bool OnVerifyActionWorked() {
        var handler = VerifyActionWorked;
        return handler == null ? true : handler();
    }
    // TODO: think about default return values
}

Еще одна вещь, которую следует учитывать при использовании подхода делегат/событие: что вы делаете, если делегат null? Если вам нужно все 3, то требовать все 3 в конструкторе было бы хорошей идеей.

Ответ 2

Решение делегата было бы полезно, если вы:

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

В других случаях я бы рекомендовал объектно-ориентированный подход.

Ответ 3

При условии, что это скорее мнение, чем твердый ответ...

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

В этом случае, когда у вас есть три метода, подход базового класса будет наиболее практичным.

Чтобы сделать две вещи полностью эквивалентными, вы можете использовать базовый не-абстрактный класс с пустыми виртуальными методами, чтобы, если производный класс не переопределяет его, он также имеет свойство null delegate.

Ответ 4

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

Ответ 5

  • Решение 1 имеет больше подвижных частей, что позволяет более тонко разделять проблемы. Один объект может решить, какой должен быть Condition для данного StateTransition, другой объект определяет ActionToTake и т.д. Или у вас может быть один объект, который решает все, но основывается на разных критериях. Не самый полезный подход в большинстве случаев ИМО - особенно учитывая небольшую дополнительную стоимость сложности.

  • В решении 2 каждая производная StateTransition является сплоченным целым, то, как она проверяет условие, не может быть отделена от того, как оно выполняет действие или проверяет его.

Оба решения могут быть использованы для выполнения инверсии управления - другими словами, они оба позволяют вам сказать, что прямой пользователь t21 не будет контролировать, какой вкус StateTransition он собирается использовать, но вместо этого решение делегироваться внешнему объекту.

Ответ 6

Как я могу выбрать, какой метод подходит для моего приложения?

Метод с делегатами делает код более функциональным, потому что он перемещается из классических методов Object Oriented Programming, таких как наследование и полиморфизм, в методы Functional Programming, такие как передача функций и использование закрытий.

Я обычно использую метод с делегатами всюду, потому что

  • Я предпочитаю состав над наследованием

  • Я нахожу метод, когда делегаты требуют меньше кода для записи

Например, конкретный экземпляр StateTransition может быть создан в 5 строках кода из делегатов и закрытий с использованием стандартного механизма инициализации .NET:

dim PizzaTransition as new StateTransition with {
    .Condition = function() Pizza.Baked,
    .ActionToTake = sub() Chef.Move(Pizza, Plate),
    .VerifyActionWorked = function() Plate.Contains(Pizza)
}
  1. Мне легко создавать Fluent API вокруг класса с набором дополнительных методов, реализованных как методы расширения или внутри класса.

Например, если в класс StateTransition добавлены методы Create, When, Do и Verify:

public class StateTransition
    public property Condition as func(of boolean)
    public property ActionToTake as Action
    public property VerifyActionWorked as func(of boolean)

    public shared function Create() as StateTransition
        return new StateTransition
    end function

    public function When(Condition as func(of boolean)) as StateTransition
        me.Condition = Condition
        return me
    end function

    public function Do(Action as Action) as StateTransition
        me.ActionToTake = Action
        return me
    end function

    public function Verify(Verify as func(of boolean)) as StateTransition
        me.VerifyActionWorked = Check
        return me
    end function
end class

Затем цепочку методов можно также использовать для создания конкретного экземпляра StateTransition:

dim PizzaTransition = StateTransition.Create.
    When(function() Pizza.Baked).
    Do(sub() Chef.Move(Pizza, Plate).
    Verify(function() Plate.Contains(Pizza))

Ответ 7

Как выбрать способ, подходящий для моего приложения?

Требуется ли вашему приложению определять новые объекты перехода, которые имеют дополнительные различные свойства, или другие различные методы? Тогда лучше сделать новые подклассы, методы перебора ( "Полиморфизм" ).

Или.

Требуется ли вашему приложению определять объекты перехода, которые изменяют только поведение метода? Тогда методы Делегаты или методы События лучше.

Резюме

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