Избегайте случая двойного переключения в шаблоне стратегии

Я знаю, что это много, но, пожалуйста, потерпите меня, я не знаю, как еще всесторонне описать проблему. Представьте, что есть машина с двумя устройствами A и B. Каждое из устройств имеет 2 оси, которые можно включать и выключать. (У осей на самом деле больше свойств для установки, но для простоты давайте сосредоточимся только на 1):

        mashine
           |
      ------------
      |          |
      A          B
    ------    ------
    |    |    |    |
    X    Y    X    Y
   ---  ---  ---  ---
   |||  |||  |||  |||
   ESP  ESP  ESP  ESP

E: Enable(bool) | S: Start(bool) | P: Position(double)

Машина представлена ​​следующим классом (который я не могу изменить!):

// МАШИНА

public static class Mashine
{
    public static bool Enable_B_X { get; set; }
    public static bool Enable_B_Y { get; set; }

    public static bool Enable_A_X { get; set; }
    public static bool Enable_A_Y { get; set; }
    // actually much more properties for each axis and device
}

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

// Интерфейс управления устройствами и 2 разных класса устройств A и B

public class DeviceControl
{
    public virtual IAxis Axis { get; set; }

    public void Enable()
    {
        Axis.Enable = true;
    }
    public void Disable()
    {
        Axis.Enable = false;
    }
}

public class DeviceControl_A : DeviceControl
{
    public override IAxis Axis
    {
        get { return base.Axis as IAxis_A; }
        set { base.Axis = value as IAxis_A; }
    }
}

public class DeviceControl_B : DeviceControl
{
    public override IAxis Axis
    {
        get { return base.Axis as IAxis_B; }
        set { base.Axis = value as IAxis_B; }
    }
}

Представление осей (интерфейс и явные классы): в основном они предназначены для сопоставления переменных в static классе Mashine со свойствами различных осей.

public interface IAxis
{
    bool Enable { get; set; }
}

// These Interfaces are to ensure that Axis_A goes only into Device A
// and Axis_B only with device B
public interface IAxis_A : IAxis { }

public interface IAxis_B : IAxis { }

public class X_Axis_A : IAxis_A
{
    public bool Enable
    {
        get => Mashine.Enable_A_X;
        set => Mashine.Enable_A_X = value;
    }
}

public class Y_Axis_A : IAxis_A
{
    public bool Enable
    {
        get => Mashine.Enable_A_Y;
        set => Mashine.Enable_A_Y = value;
    }
}

public class X_Axis_B : IAxis_B
{
    public bool Enable
    {
        get => Mashine.Enable_B_X;
        set => Mashine.Enable_B_X = value;
    }
}

public class Y_Axis_B : IAxis_B
{
    public bool Enable
    {
        get => Mashine.Enable_B_Y;
        set => Mashine.Enable_B_Y = value;
    }
}

И это класс управления, который предоставляет методы управления машиной в зависимости от устройства и соответствующей оси:

public enum Device { A, B }

public enum Axis { X, Y }


public class Control
{
    public DeviceControl devControl;

    public void Disable(Device dev, Axis dim)
    {
        // initialize
        InitAxisAndDevice(dev, dim);
        devControl.Disable();       
    }

    public void Enable(Device dev, Axis dim)
    {
        InitAxisAndDevice(dev, dim);
        devControl.Enable();
    }


    private void InitAxisAndDevice(Device dev, Axis dim)
    {
        switch (dev)
        {
            case Device.A:
                this.devControl = new DeviceControl_A();

                switch (dim)
                {
                    case Axis.X: this.devControl.Axis = new X_Axis_A(); break;
                    case Axis.Y: this.devControl.Axis = new Y_Axis_A(); break;
                    case Axis.Z: this.devControl.Axis = new Z_Axis_A(); break;
                }
                break;
            case Device.B:
                this.devControl = new DeviceControl_B();

                switch (dim)
                {
                    case Axis.X: this.devControl.Axis = new X_Axis_B(); break;
                    case Axis.Y: this.devControl.Axis = new Y_Axis_B(); break;
                    case Axis.Z: this.devControl.Axis = new Z_Axis_B(); break;
                }
                break;
        }
    }
}

Вопрос 1

Как я могу избежать этого случая двойного переключения в методе InitAxisAndDevice?

Вопрос 2

Есть ли лучший способ убедиться, что только оси типа A совместимы с устройством A?

У меня есть стойкое ощущение, что я что-то неправильно понял в применении этого шаблона. Может быть, есть другой подход/шаблон, который был бы более подходящим для решения этой проблемы сопоставления управления?

Любая помощь очень приветствуется. Заранее спасибо.

ИЗМЕНИТЬ

Поскольку я был слишком расплывчатым в объяснении, вот пример использования. Все оси имеют почти идентичный набор переменных, которые должны быть установлены, например: [enable(bool), position(double), start(bool)]

Точка абстракции состоит в том, чтобы иметь один класс управления, который может использовать один метод Enable f.e. для включения любой оси в зависимости от типа устройства и типа оси.

надеюсь стало немного понятнее


person Mong Zhu    schedule 15.11.2017    source источник
comment
Я думаю, что сказать, что вы слишком усложняете вещи, это ничего не сказать. Я прочитал ваш вопрос три раза и до сих пор не уверен, что понимаю, что делает ваш код или какова ваша цель. Например, какой смысл в public interface IAxis_A : IAxis { }? Пожалуйста, попробуйте описать желаемый Control интерфейс (т.е. публичные методы, которые вам нужны).   -  person Zohar Peled    schedule 15.11.2017
comment
Я даже не вижу схемы стратегии, о которой вы говорите.   -  person Fildor    schedule 15.11.2017
comment
@ZoharPeled спасибо за ваш комментарий. Это попытка сделать что-то по-другому, наверное, я перестарался. Интерфейс управления предназначен для установки переменных в машинном представлении и их записи в ПЛК. В зависимости от типа устройства и типа оси предполагается установить соответствующую переменную и записать ее в ПЛК, работающий на станке.   -  person Mong Zhu    schedule 15.11.2017
comment
@Fildor вполне может быть, что я неправильно понял реализацию шаблона, но общая идея состоит в том, чтобы установить разные переменные в зависимости от типа. Таким образом, вызов DeviceControl.Enabled устанавливает соответствующую переменную, соответствующую типу устройства и типу оси, и отправляет ее в ПЛК.   -  person Mong Zhu    schedule 15.11.2017
comment
Было бы хорошо, если бы вы более четко объяснили, как вы планируете использовать свою абстракцию. Нет реальной необходимости в абстракции, если в какой-то момент у вас не работает один и тот же код для разных устройств/осей.   -  person grek40    schedule 15.11.2017
comment
@grek40 в основном все оси имеют одинаковый набор параметров [enable, position, velocity, startBit, stopBit], и оба устройства имеют одинаковый набор осей [X, Y, Z]. Я хотел бы сделать 1 метод для установки определенного параметра. в зависимости от типа устройства и оси. Имеет ли это смысл? Мне приходится иметь дело с представительским классом машины. который работает как интерфейс между моей программой и ПЛК   -  person Mong Zhu    schedule 15.11.2017
comment
Нет, это действительно не имеет смысла. Вы впервые говорите о параметрах (третья тема рядом с устройствами и осью). Кроме того, если ваше фактическое действие зависит от фактического устройства и фактической оси, то вся унифицированная абстракция устройств и действий только взорвет код. Тем не менее, я опубликую предлагаемую структуру оболочки в качестве ответа.   -  person grek40    schedule 15.11.2017


Ответы (1)


Рассмотрим следующий код. Идея состоит в том, чтобы создать абстрактное представление оси устройства, устройств и машины как интерфейса. Затем используйте фабрику, чтобы создать конкретные устройства и оси для (учитывая, что они не изменяемы) Mashine.

public static class Mashine
{
    public static bool Enable_B_X { get; set; }
    public static bool Enable_B_Y { get; set; }

    public static bool Enable_A_X { get; set; }
    public static bool Enable_A_Y { get; set; }
}

/// <summary>
/// Represents a single axis of a single device
/// </summary>
public interface IDeviceAxis
{
    void Enable();
    void Disable();
}
// general device that has two axis, but doesn't care about anything else
public interface IDevice
{
    IDeviceAxis X { get; }
    IDeviceAxis Y { get; }
}
// data model for an alternative Mashine representation
public interface IMachineModel
{
    IDevice A { get; }
    IDevice B { get; }
}

Применение:

public enum Device { A, B }

public enum Axis { X, Y }

// your control class
public class Control
{
    public IMachineModel devControl;

    public Control()
    {
        // MachineFactory see below
        devControl = MachineFactory.GetMachine();
    }

    public void Disable(Device dev, Axis dim)
    {
        GetAxis(dev, dim).Disable();
    }

    public void Enable(Device dev, Axis dim)
    {
        GetAxis(dev, dim).Enable();
    }

    private IDeviceAxis GetAxis(Device dev, Axis dim)
    {
        var device = GetDevice(dev);
        switch (dim)
        {
            case Axis.X:
                return device.X;
            case Axis.Y:
                return device.Y;
            default:
                throw new ArgumentException("Invalid Axis", "dim");
        }
    }
    private IDevice GetDevice(Device dev)
    {
        switch (dev)
        {
            case Device.A:
                return devControl.A;
            case Device.B:
                return devControl.B;
            default:
                throw new ArgumentException("Invalid Device", "dev");
        }
    }
}

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

// concrete machine factory
public static class MachineFactory
{
    // factory for the whole Mashine wrapper
    public static IMachineModel GetMachine()
    {
        return new MachineModel
        {
            A = GetDeviceA(),
            B = GetDeviceB(),
        };
    }

    // factory methods specify the connection from the wrapper classes to the Mashine

    private static IDevice GetDeviceA()
    {
        return new MachineDevice(x => Mashine.Enable_A_X = x, y => Mashine.Enable_A_Y = y);
    }
    private static IDevice GetDeviceB()
    {
        return new MachineDevice(x => Mashine.Enable_B_X = x, y => Mashine.Enable_B_Y = y);
    }

    // concrete implementations can be used for the target Mashine

    private class MachineDeviceAxis : IDeviceAxis
    {
        Action<bool> _setterFunction;
        public MachineDeviceAxis(Action<bool> setter)
        {
            _setterFunction = setter;
        }

        public void Enable()
        {
            _setterFunction(true);
        }

        public void Disable()
        {
            _setterFunction(false);
        }
    }

    private class MachineDevice : IDevice
    {
        public MachineDevice(Action<bool> xSetter, Action<bool> ySetter)
        {
            X = new MachineDeviceAxis(xSetter);
            Y = new MachineDeviceAxis(ySetter);
        }
        public IDeviceAxis X { get; private set; }

        public IDeviceAxis Y { get; private set; }
    }

    private class MachineModel : IMachineModel
    {
        public IDevice A { get; set; }

        public IDevice B { get; set; }
    }
}

Что касается использования машинной абстракции, я думаю, что нет никакого реального преимущества в использовании enum Device и enum Axis. Сравните следующие коды:

// controllers
Control yourControl;
IMachineModel myMachineModel;
// usage
yourControl.Enable(Device.A, Axis.X);
myMachineModel.A.X.Enable();

Преимущество использования Control здесь на самом деле не улучшает код, если вы спросите меня.

Изменить другие параметры

Может быть, Enable и Disable уже переработаны? Как насчет некоторого общего Set(value) вместо этого:

public interface IAxisParameter<TParameter>
{
    void Set(TParameter value);
}

public interface IDeviceAxis
{
    IAxisParameter<bool> Enabled { get; }

    IAxisParameter<double> Position { get; }

    IAxisParameter<bool> Start { get; }
}

Применение:

public class Control
{
    public void Disable(Device dev, Axis dim)
    {
        GetAxis(dev, dim).Enabled.Set(false);
    }

    public void Enable(Device dev, Axis dim)
    {
        GetAxis(dev, dim).Enabled.Set(true);
    }

    // rest of the code as in the original answer above
}

Затем измените внутреннюю реализацию машины соответствующим образом:

// inside factory

    // added implementation
    private class MachineParameter<TParam> : IAxisParameter<TParam>
    {
        Action<TParam> _setterFunction;
        public MachineParameter(Action<TParam> setter)
        {
            _setterFunction = setter;
        }
        public void Set(TParam value)
        {
            _setterFunction(value);
        }
    }

    // changed implementation
    private class MachineDeviceAxis : IDeviceAxis
    {
        public IAxisParameter<bool> Enabled { get; set; }

        public IAxisParameter<double> Position { get; set; }

        public IAxisParameter<bool> Start { get; set; }
    }

    // changed factory methods

    private static IDeviceAxis GetDeviceAxisA_X()
    {
        return new MachineDeviceAxis
        {
            Enabled = new MachineParameter<bool>(x => Mashine.Enable_A_X = x),
            Position = null, // TODO
            Start = null, // TODO
        };
    }
    private static IDeviceAxis GetDeviceAxisA_Y()
    {
        return new MachineDeviceAxis
        {
            Enabled = new MachineParameter<bool>(y => Mashine.Enable_A_Y = y),
            Position = null, // TODO
            Start = null, // TODO
        };
    }
    private static IDevice GetDeviceA()
    {
        return new MachineDevice
        {
            X = GetDeviceAxisA_X(),
            Y = GetDeviceAxisA_Y(),
        };
    }
    private static IDevice GetDeviceB()
    {
        // TODO same thing for device B
        return null;
    }

Концепция должна быть ясной... в конце концов вам нужно написать код для каждого свойства реальной машины. Возможно, было бы неплохо реорганизовать создание параметров внутри фабрики так, как это сделал @Fildor в своем примере связанного кода.

Изменить: сделать заводской код более компактным

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

// concrete machine factory
public static class MachineFactory
{
    // factory for the whole Mashine wrapper
    public static IMachineModel GetMachine()
    {
        var enabledSetters = CreateEnabledParameterSetters();
        return new MachineModel
        {
            A = GetDevice(enabledSetters, 0 /*A*/),
            B = GetDevice(enabledSetters, 1 /*B*/),
        };
    }

    // factory methods specify the connection from the wrapper classes to the Mashine

    private static Action<bool>[,] CreateEnabledParameterSetters()
    {
        return new Action<bool>[,]
        {
            { x => Mashine.Enable_A_X = x, x => Mashine.Enable_A_Y = x },
            { x => Mashine.Enable_B_X = x, x => Mashine.Enable_B_Y = x },
        };
    }
    private static IDeviceAxis GetDeviceAxis(Action<bool>[,] enabledSetters, int deviceIndex, int axisIndex)
    {
        return new MachineDeviceAxis
        {
            Enabled = new MachineParameter<bool>(enabledSetters[deviceIndex, axisIndex]),
            Position = null, // TODO
            Start = null, // TODO
        };
    }
    private static IDevice GetDevice(Action<bool>[,] enabledSetters, int deviceIndex)
    {
        return new MachineDevice
        {
            X = GetDeviceAxis(enabledSetters, deviceIndex, 0 /*X*/),
            Y = GetDeviceAxis(enabledSetters, deviceIndex, 1 /*Y*/),
        };
    }

// ...
person grek40    schedule 15.11.2017
comment
спасибо за усилия. Это выглядит многообещающе. Я просто читаю и пытаюсь понять.... - person Mong Zhu; 15.11.2017
comment
@ grek40 Не могли бы вы взглянуть на это и посмотреть, может ли это даже упростить ваш подход? dotnetfiddle.net/gSd6am - person Fildor; 15.11.2017
comment
@Fildor за заданный вопрос, да. Но я думаю, что вся идея enum Device и enum Axis - это просто пережиток рассматриваемой испорченной реализации. А реальное моделирование IDevice и IDeviceAxis позволяет работать на устройстве совершенно по другому методу. Просто нужен лучший пример использования. - person grek40; 15.11.2017
comment
Да, я тоже так считаю. - person Fildor; 15.11.2017
comment
большое спасибо за ваше решение. Я согласен, что моя реализация испорчена, иначе я бы не задавал этот вопрос. Извините также за отсутствие информации в начале. Хорошо, пока я думаю, что получил большую часть подхода, который вы опубликовали. Итак, вы создали модель машины и модель устройства, которое представляет собой целое устройство со всеми осями (в отличие от моего кода). Важным трюком является использование Action<bool> _setterFunction;, который фактически отвечает за отображение между объектом и соответствующие поля в классе Mashine. - person Mong Zhu; 15.11.2017
comment
@MongZhu Звучит правильно. Action<bool> на самом деле не важный трюк, но в какой-то момент должен быть код, который обращается к фактическим свойствам машины по имени, и я решил сделать этот момент небольшим с помощью лямбда-функций. Важной частью является наличие понятного интерфейса устройств и осей, который является очень гибким и охватывает все, что вы действительно хотите делать со станком. Затем просто напишите специфичную для машины реализацию, которая позаботится о деталях в несколько удобочитаемом виде. - person grek40; 15.11.2017
comment
вы действительно очень помогли, и я узнал массу нового материала сегодня. Особенно использование лямбда-установщика :) Я пытаюсь изменить ваш код, чтобы установить переменную double. Думаю, мне нужно сделать _setterFunction общим, или все MachineDevice привязано к bool? - person Mong Zhu; 15.11.2017
comment
@MongZhu еще несколько обновлений о том, как можно обрабатывать несколько параметров без чрезмерного раздувания кода. - person grek40; 15.11.2017
comment
@Fildor спасибо за ваш вклад. Я это очень ценю. Совершенно другой и новый подход к этой проблеме для меня. Я определенно смотрю на это - person Mong Zhu; 15.11.2017
comment
Моя скрипка была быстрой и грязной свалкой моего мозга. Я думаю, что grek40 проделал большую работу, превратив эту идею в более чистое и гибкое решение. - person Fildor; 15.11.2017
comment
@grek40 спасибо за обновление. сжатие кода выглядит многообещающе и компактно. Хотя я должен признать, что использование индексов делает его менее читаемым или вводит необходимость в дополнительном перечислении, чтобы сделать его читаемым для психопата, который в будущем должен будет поддерживать мой код (и знает, где я живу;)) - person Mong Zhu; 15.11.2017
comment
@ grek40 grek40 Я должен признать, что гибкость этой концепции действительно хороша, и ее стоит учитывать. Еще раз спасибо за ваше время и усилия. Я многому научился. - person Mong Zhu; 15.11.2017
comment
@ grek40 Я забираю индексную часть. Ваше решение гениально. особенно индексация в массиве 2D, теперь я понял это полностью. Я немного преобразовал его, и теперь у меня есть рабочее решение с понятным уровнем абстракции и сложностью. Жаль, что я могу проголосовать только один раз;). Ура, приятель - person Mong Zhu; 20.11.2017