2
\$\begingroup\$

My company wants me to build a serverless component (AWS Lambda) that checks every minute for our system components health.

The data model (classes structure) I have devised so far is as pasted below. The main problem I am trying to address is I wanted to have a collection of all our system metrics. At first I thought of having clsMetric<T> where T would determine the type of Value and Threshold properties. But I can't have a collection of a generic class (at least not a generic collection of generic classes).

So I ended up creating subclasses of clsMetric for each metric value type I have, and left in the parent class a string representation of those properties for accessing them while looping over my collection of clsMetric.

Is this the way to go ? Current C# capabilities offer something else I do not know? Any design pattern suitable to apply here I am not aware of ?

public abstract class clsMetric
{
    public string Name { get; }
    public abstract string ValueAsStr { get; }
    public abstract eLogicalOperator LogicalOperator { get; }
    public abstract string ThresholdAsStr { get; }
    public abstract bool Faulty();
    public clsMetric(string name)
    {
         Name = name ?? string.Empty;
    }

    public override string ToString()
    {
        return Name;
    }
}


public class IntegerMetric : clsMetric
{
    eLogicalOperator _logicalOperator;
    #region Properties
    public int Value { get; private set; }

    public override string ValueAsStr => Value.ToString();

    public override eLogicalOperator LogicalOperator => _logicalOperator;

    public int Threshold { get; private set; }
    public override string ThresholdAsStr => Threshold.ToString();

    #endregion

    #region Constructors
    public IntegerMetric(string name, int value, eLogicalOperator logicalOperator, int threshold) : base(name)
    {
        Value = value;
        _logicalOperator = logicalOperator;
        Threshold = threshold;
    }

    public override bool Faulty()
    {
        if (LogicalOperator == eLogicalOperator.Equal)
            return Value == Threshold;
        else if (LogicalOperator == eLogicalOperator.GreaterOrEqualThan)
            return Value >= Threshold;
        else if (LogicalOperator == eLogicalOperator.GreaterThan)
            return Value > Threshold;
        else if (LogicalOperator == eLogicalOperator.LowerOrEqualThan)
            return Value <= Threshold;
        else if (LogicalOperator == eLogicalOperator.LowerThan)
            return Value < Threshold;

        return false;
    }

    #endregion
}


public class StringMetric : clsMetric
{
    eLogicalOperator _logicalOperator;
    #region Properties
    public string Value { get; private set; }

    public override string ValueAsStr => Value.ToString();
    public override eLogicalOperator LogicalOperator => _logicalOperator;

    public string Threshold { get; private set; }

    public override string ThresholdAsStr => Threshold.ToString();
    #endregion

    #region Constructors
    public StringMetric(string name, string value, eLogicalOperator logicalOperator, string threshold) : base(name)
    {
        Value = value;
        _logicalOperator = logicalOperator;
        Threshold = threshold;
    }

    public override bool Faulty()
    {
        if (LogicalOperator == eLogicalOperator.Equal)
            return string.Equals(Value, Threshold);
        else if (LogicalOperator == eLogicalOperator.Contains)
            return ValueAsStr.Contains(ThresholdAsStr, StringComparison.InvariantCultureIgnoreCase);

        return false;
    }
    #endregion
}
\$\endgroup\$
  • 2
    \$\begingroup\$ Where did you get this naming convention from? This is so old school. \$\endgroup\$ – t3chb0t Sep 5 at 16:25
  • 1
    \$\begingroup\$ I'm voting-to-close this question for the lack of context. We need to see how you use this code. \$\endgroup\$ – t3chb0t Sep 5 at 16:28
  • \$\begingroup\$ @t3chb0t There are still companies using conventions from 25 years ago and even books sold teaching them, unfortunately. Considering this question has been answered now, closing isn't helpful. OP should however take your advice for next time. \$\endgroup\$ – Mast Sep 9 at 11:45
5
\$\begingroup\$

A design pattern often used, is to have a non generic base type and a generic type derived from it or a type implementing a non generic as well as a generic interface. An example is

public abstract class Comparer<T> :
    System.Collections.Generic.IComparer<T>, System.Collections.IComparer

You can see the full implementation here public abstract class Comparer<T> : IComparer, IComparer<T>.

You can see that it does an explicit implementaion of IComparer members and an implicit implementation of IComparer<T>. This hides the IComparer members when not accessed through this very interface. Original code:

public abstract int Compare(T x, T y);

int IComparer.Compare(object x, object y) {
    if (x == null) return y == null ? 0 : -1;
    if (y == null) return 1;
    if (x is T && y is T) return Compare((T)x, (T)y);
    ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidArgumentForComparison);
    return 0;
}

A possible approach for your metric class, would be to implement two interfaces IMetric and IMetric<T>. The collection would then have an element type of IMetric.

public interface IMetric
{
    string Name { get; }
    object Value { get; }
    eLogicalOperator LogicalOperator { get; }
    object Threshold { get; }
    bool Faulty();
}

public interface IMetric<out T> : IMetric
{
    new T Value { get; }
    new T Threshold { get; }
}

Implementation:

public class Metric<T> : IMetric<T>
{
    public Metric(string name, eLogicalOperator logicalOperator, T value, T threshold)
    {
        Name = name ?? String.Empty;
        LogicalOperator = logicalOperator;
        Value = value;
        Threshold = threshold;
    }

    public T Value  { get; }
    public T Threshold  { get; }

    object IMetric.Value => Value;
    object IMetric.Threshold => Threshold;

    public string Name  { get; }
    public eLogicalOperator LogicalOperator { get; }

    public bool Faulty()
    {
        //TODO: implement;
    }
}

I don't know, however, how you would implement the Faulty method in a generic way. Maybe you could inject the logic and use a factory method to create metric objects.

Probably a better approach is to create an abstract Metric<T> class having only the Faulty method abstract to be able to implement specialized variants.

public class DecimalMetric : IMetric<decimal>

Yet another, elegant possibility is to pass a delegate doing the comparison, instead of the operator enum.

public Metric(string name, T value, T threshold, Func<T,T,bool> isValid)

This allows you to provide a type specific comparison and saves you a complex processing for different operators. I prefer to ask the positive question "is valid". It feels better than specifying what is wrong. You can call it like this

new Metric<int>("int test", 5, 10, (v, threshold) => v < threshold)

Usage (still with you original constructor):

var list = new List<IMetric>();
list.Add(new Metric<int>("int", eLogicalOperator.LowerThan, 5, 10));
list.Add(new Metric<double>("double", eLogicalOperator.LowerThan, 2.45, 10.0));

object value = list[0].Value;

switch (list[0]) {
    case Metric<int> intMetric:
        int i = intMetric.Value;
        break;
    case Metric<double> doubleMetric:
        double d = doubleMetric.Value;
        break;
    default:
        break;
}

// or

switch (list[0].Value) { // of type object
    case int i:
        int myInt = i;
        break;
    case double d:
        double myDouble = d;
        break;
    default:
        break;
}
\$\endgroup\$
  • \$\begingroup\$ Thanks. My initial design indeed had a Precidate<T> Property as a member of the Metric class, but I need to explicitly know both the operator and the "threshold" value for reporting purposes, so I gave up the Predicate. \$\endgroup\$ – Veverke Sep 8 at 7:53
  • \$\begingroup\$ Does this design scenario constitute a formal known design pattern I can search and learn more ? \$\endgroup\$ – Veverke Sep 8 at 11:46
  • \$\begingroup\$ It's not a named (and known) design pattern, but it is used at different places in the .NET Framework library. public interface IEnumerable<out T> : System.Collections.IEnumerable is a very prominent use case. \$\endgroup\$ – Olivier Jacot-Descombes Sep 8 at 12:36
  • 1
    \$\begingroup\$ Thanks a lot for sharing your time and knowledge here. \$\endgroup\$ – Veverke Sep 8 at 12:39
  • \$\begingroup\$ You are welcome. \$\endgroup\$ – Olivier Jacot-Descombes Sep 8 at 12:44
3
\$\begingroup\$

I just have a couple of minor points to add to Olivier Jacot-Descombes' excellent answer.

    public override bool Faulty()
    {
        if (LogicalOperator == eLogicalOperator.Equal)
            return Value == Threshold;
        else if (LogicalOperator == eLogicalOperator.GreaterOrEqualThan)
            return Value >= Threshold;
        else if (LogicalOperator == eLogicalOperator.GreaterThan)
            return Value > Threshold;
        else if (LogicalOperator == eLogicalOperator.LowerOrEqualThan)
            return Value <= Threshold;
        else if (LogicalOperator == eLogicalOperator.LowerThan)
            return Value < Threshold;

        return false;
    }

Enums support switch, and I don't think you'll find many people who claim that a chain of if/else is better style than a switch.

Also, most of the comparisons are fairly generic, in the sense that if you choose to follow Olivier's suggestion about a base non-generic class/interface and a generic implementation but not the suggestion about passing a delegate, it could have a base implementation using Comparer<T>.Default():

public class Metric<T> : IMetric<T>
{
    private static readonly IComparer<T> Comparer = Comparer<T>.Default;

    ...

    public T Value  { get; }
    public T Threshold  { get; }
    public eLogicalOperator LogicalOperator { get; }

    public virtual bool Faulty()
    {
        switch (LogicalOperator)
        {
            case eLogicalOperator.Equal:
                return Comparer.Compare(Value, Threshold) == 0;
            case eLogicalOperator.GreaterOrEqualThan:
                return Compare.Compare(Value, Threshold) >= 0;
            case eLogicalOperator.GreaterThan:
                return Comparer.Compare(Value, Threshold) > 0;
            case eLogicalOperator.LowerOrEqualThan:
                return Comparer.Compare(Value, Threshold) <= 0;
            case eLogicalOperator.LowerThan:
                return Comparer.Compare(Value, Threshold) < 0;
            default:
                throw new NotSupportedException($"Operator {LogicalOperator} is not supported for {typeof(T)}");
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Now we can take this a step further and make this an extension method public static bool Evaluate<T>(this IComparer<T> comparer, LogicalOperator operator, T operand1, T operand2) to me, this feels even more appropriate than an instance method of Metric \$\endgroup\$ – dfhwze Sep 6 at 18:33
  • \$\begingroup\$ @dfhwze, no, although I goofed by missing virtual. E.g. StringMetric will want to override to add support for eLogicalOperator.Contains. \$\endgroup\$ – Peter Taylor Sep 6 at 21:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.