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

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

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();

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

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

7 ответов

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

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

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

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

Вы также можете объединить два (переопределить против события) через:

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 в конструкторе.

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

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

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

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

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

Или же.

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

Резюме

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

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

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

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

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

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

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

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

Я склонен использовать метод с делегатами везде, где я могу, потому что

  1. Я предпочитаю композицию наследству

  2. Я нахожу метод с делегатами требовать меньше кода для записи

Например, бетон 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 вокруг класса с помощью набора дополнительных методов, реализованных как методы Extension или внутри класса.

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

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))
  • Решение 1 имеет больше движущихся частей, что позволяет более тонко разделить проблемы. Вы могли бы один объект решить, что Condition для данного StateTransition должно быть, другой объект определить ActionToTake, и так далее. Или вы можете иметь один объект, чтобы решить их все, но на основе разных критериев. Не самый полезный подход большую часть времени IMO - особенно учитывая небольшую дополнительную сложность затрат.

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

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

Другие вопросы по тегам