如何重构具有每种具体类型的公共成员/属性的抽象类?

时间:2019-04-17 17:41:13

标签: c# inheritance abstraction solid-principles

我正在查看一些旧代码,并遇到了一种抽象,该抽象为其每种派生/具体类型具有属性。我无法共享确切的代码,但是请想象一下,有许多操作要复杂得多,而不是简单的操作。

我以前从未遇到过类似问题,并且有很多问题吗?首先,这是我不知道的模式吗?如果是这样,那是什么?第二个问题,我应该如何重构它以便遵循扎实的原则?

如果需要,我会尽力想出一个更好的例子。

public enum ToolType
{
    Unknown = 0,
    HRMonitor,
    Dumbell,
    SomeForceDevice
}

public abstract class ToolData
{
    private ToolData()
    {
        IsValid = false;
        this.ToolType = ToolType.Unknown;
    }

    public ToolData(ToolType toolType)
    {
        this.ToolType = toolType;
    }

    public ToolType ToolType { get; }
    public virtual bool IsValid { get; protected set; } = true;
    public double LinkQuality { get; set; }


    public NullToolDataValue NullData => this as NullToolDataValue;
    public DumbellDataValue DumbellData => this as DumbellDataValue;
    public HeartRateDataValue HRData => this as HeartRateDataValue;
    public SomeForceDataValue SomeForceData => this as SomeForceDataValue;
}

public class NullToolDataValue : ToolData
{
    public NullToolDataValue() : base(ToolType.Unknown)
    {
        IsValid = false;
    }
}

public class DumbellDataValue : ToolData
{
    public double WeightValue { get; private set; }

    public DumbellDataValue(double weightValue) : base(ToolType.Dumbell)
    {
        this.WeightValue = weightValue;
    }

    public override string ToString()
    {

        return WeightValue.ToString(CultureInfo.InvariantCulture);
    }
}

public class HeartRateDataValue : ToolData
{
    public int HeartRate { get; private set; }

    public HeartRateDataValue(int heartRate) : base(ToolType.HRMonitor)
    {
        this.HeartRate = heartRate;
    }

    public override string ToString()
    {
        return HeartRate.ToString(CultureInfo.InvariantCulture);
    }
}

public class SomeForceDataValue : ToolData
{
    public double LeftHandForceValue { get; private set; }
    public double RightHandForceValue { get; private set; }

    public int LeftHandPosition { get; private set; }
    public int RightHandPosition { get; private set; }

    public SomeForceDataValue(double lefthandValue, double rightHandValue, int leftHandPosition, int rightHandPosition) : base(ToolType.SomeForceDevice)
    {
        this.LeftHandForceValue = lefthandValue;
        this.LeftHandPosition = leftHandPosition;
        this.RightHandForceValue = rightHandValue;
        this.RightHandPosition = rightHandPosition;
    }

    public override string ToString()
    {
        return $"{LeftHandForceValue.ToString(CultureInfo.InvariantCulture)}" +
            $"| {LeftHandPosition.ToString(CultureInfo.InvariantCulture)}" +
            $"| {RightHandForceValue.ToString(CultureInfo.InvariantCulture)}" +
            $"| {RightHandPosition.ToString(CultureInfo.InvariantCulture)}";                
    }
}

正在通过以下方式使用/消耗它,它也缺少一些继承性和简洁性:

public class DumbellExcercise
{
    public void ToolDataReceived(ToolData data)
    {
        if (data?.DumbellData == null) return;

        //add value to some collection 
        Collection.Add(data.DumbellData.WeightValue);
    }

}

public class HRExcercise
{
    public void ToolDataReceived(ToolData data)
    {
        if (data?.HRData == null) return;

        //add value to some collection 
        Collection.Add(data.HRData.HeartRate);
    }

}

2 个答案:

答案 0 :(得分:0)

好的,我要回答一下-希望这会有所帮助。

首先,ToolData不应包含任何References / Enums /列出其子类型的任何内容。因此,首先是斩波块:将对象强制转换为特定子类型的所有lambda属性。我可以理解它的吸引力-您知道 ToolType的一个实例恰好是FloobieTool,因此您调用instance.FloobieTool并神奇地获得了FloobieTool的演员表。但是...好吧,它伴随着一些问题,尤其是您违反了开放/封闭原则。如果知道他们正在使用FloobieTool的工作,则使调用该类的人员使用(FloobieTool)实例显式对其进行强制转换就没错。

下一步:工具类型。你为什么需要这个?如果您在ToolData的实例是FloobieTool的情况下,只需在IF条件下执行“ is”检查,就可以告诉

void SomeFunc(ToolData toolData)
{
    if (!(toolData is FloobieTool)) throw new Exception("Non-Floobie!");
    // more code
}

我的意思是,该枚举实际上能为您带来什么?因为它具有一定的成本,所以它必须与实现ToolData的类列表保持同步。

此外,每个这些Exercise类的ToolDataReceived()中的部分似乎都很奇怪。我的意思是,您已经进行了练习,并且正在传递ToolData。为什么要存储Dumbell练习的金额?与仅存储ToolData相反。我的意思是,您要进行大量测试/发布/等等,只是要将哑铃 weight 添加到集合中。有什么原因不能仅仅存储ToolData实例并每天调用它?如果您确实需要专门存储哑铃信息,则可以执行以下操作:

public class DumbbellExercise
{
    List<DumbbellDataValue> dumbbellData = new List<DumbbellDataValue>();
    public void AddToolData(ToolData toolData)
    {
        if (toolData is DumbbellDataValue)
            this.dumbbellData.Add((DumbbellDataValue)toolData);
    }
}

希望这会有所帮助-在我们为您的实际问题制作一个抽象示例时,很难涉及太多细节:-)

答案 1 :(得分:0)

看过您的编辑后,我更加坚信重构此代码的方法是使用模式匹配。模式匹配至少需要C#7.0,因此我将提供一种几乎一样好的方法来实现7.0之前的版本。

步骤1

使用ObsoleteAttribute将属性标记为过时,并为true参数传递error

[Obsolete("Use pattern matching instead.", true)]
public NullToolDataValue NullData => this as NullToolDataValue;

[Obsolete("Use pattern matching instead.", true)]
public DumbellDataValue DumbellData => this as DumbellDataValue;

[Obsolete("Use pattern matching instead.", true)]
public HeartRateDataValue HRData => this as HeartRateDataValue;

[Obsolete("Use pattern matching instead.", true)]
public SomeForceDataValue SomeForceData => this as SomeForceDataValue;

在编译器处理的任何代码中使用它们将导致编译器错误。如果您对它们进行了任何思考,则如果您也未更改该代码,则将获得运行时异常(在步骤3完成之后)。

步骤2

修改使用这些属性的每个呼叫站点,改为使用模式匹配。如果您所做的只是问题中显示的内容,那么应该像这样简单:

public class DumbellExcercise
{
    public void ToolDataReceived(ToolData data)
    {
        if (data is DumbellDataValue dumbell)
            Collection.Add(dumbell.WeightValue);

        // OR

        if (!(data is DumbellDataValue dumbell))
            return;

        Collection.Add(dumbell.WeightValue);
    }
}

第二个变体不是很漂亮,因为条件必须在条件可否定之前加上括号(嘿,至少VB具有IsNot关键字;请参见图),但是您会获得与现有条件相同的早期回报代码。

您似乎使用的是至少C#6.0,因为您使用的是空分号运算符(?.),但是如果您使用的不是至少7.0,则可以这样做:

public class DumbellExcercise
{
    public void ToolDataReceived(ToolData data)
    {
        DumbellDataValue dumbell = data as DumbellDataValue;
        if (dumbell != null)
            Collection.Add(dumbell.WeightValue);

        // OR

        DumbellDataValue dumbell = data as DumbellDataValue;
        if (dumbell == null)
            return;

        Collection.Add(dumbell.WeightValue);
    }
}

步骤3

删除属性。如果没有更多的编译器错误,那么不会使用这些属性,因此您可以自由地摆脱它们。


附加说明

IsValid属性具有奇怪的双重性。它可以由派生类分配,但它也是虚拟的,因此也可以被覆盖。你真的应该选一个。如果这是我的决定,则将其保留为虚拟状态并将其设置为只读。

public abstract class ToolData
{
    // Continue to assume it's true...
    public virtual bool IsValid => true;
}

public class NullToolDataValue : ToolData
{
    // ...and indicate otherwise as needed.
    public override bool IsValid => false;
}