Different way to use interfaces

Hi guys,

I’m trying to understand ways to use interfaces in a context like the one below.
I find this implementation quite different compared to the explanation given on Unity Learn.
What is the purpose of having an implementation like this and how to use it properly? Can you give me an example?

    public interface Numbers
    {

        IEnumerable<int> numbers{ get; set;}

        int total{ get; set;}               

    }



    public interface Calculate
    {
      
    
        Numbers GetNumbs(int[] numbCollection);

      

    }



    class Calculator
    {

      public static Calculate ProcessCalculation()
      {

      }
   
   }

Say you have different ways to ‘Calculate’, but they all effectively take in an int array, and the return a set of numbers and a total (like say one is a sum of them all, but another is a sum with tax added, and another is the product of them all, and another is an average of them all… so on, so forth).

OK, so you have multiple 'Calculate’s that implement ‘Calculate’.

Think of this as being sort of similar to inheritance… and in earlier OOP languages you would have used inheritance of an abstract class to achieve this. The primary thing being you’d be inheriting from a class that defined the method ‘GetNumbers(int[ ] numbCollection)’, but no implementation what so ever.

Boom, that’s what an interface is. It’s a class with no implementation, only methods/properties defined on them, and you can’t directly instantiate it (rather you must instantiate a class that inherits/implements that interface).

Now why does GetNumbs return an interface called Numbers? Well this one is getting really simplistic in its design IMO. I personally wouldn’t create a Numbers interface if only because all its purpose is, is to store the numbers enumeration and retrieve the total. This could have easily been done with as just a data container. By having it an interface it allows us to create our own Numbers class on a per Calcualte basis if we choose. Not exactly useful…

EXCEPT if Calculate is a factory.

If Calculate is a factory that returns a Numbers object whose total is calculated by it… rather than the Calculate. This could be useful.

Since ‘numbers’ and ‘total’ are getter/setters, you could in theory change the numbers and total could get updated.

Thing is…

Those would be HORRIBLE names for interfaces since they don’t convey anything about them being factories. Nevermind the fact that the names are already pretty bad in and of them self. A ‘Calculate’… really? Class an Interface names are usually nouns, and methods are usually verbs… Nevermind the fact it’s almost industry standard to start interface names with I, like ICalculate or INumbers.

Last the static class with static member. I think the intent there is to just be a factory method for Calculate. Not sure why it’s included… not sure why it’d need to be an interface. It doesn’t do a good job at conveying why these interfaces exist, so for example purposes it sort of just adds confusion to the whole thing.

Furthermore it implies something to me… the fact only 1 method exists that returns a Calculate, but Calculate can be 1 of many types, it says to me that ‘ProcessCalculations’ can return many of possible things based on some internal state we’re unaware of. This sort of design is considered bad.

  1. a function should return a predictable result. It’s sort of like algebra, a function should for any given input always return the same output. f(x) = 3 + x will return 9 for f(6) no matter how many times you call f(6). This function above doesn’t take in any parameters, so it should be returning the same kind of object every time its called. If not, then the parameters that control the state of the function should either a) be passed in as parameters, or b) be properties of the class the function is a member of.

  2. With (1) stated, the implication that the function has ‘state’ means it shouldn’t be a static. Statics should be stateless and only take in state information as parameters (and yes, Unity has stateful statics like Random, those are BAD design… and I hope they know that).

But yeah, that’s my best guess at the intent.

Without proper documentation, or an example of their uses, I could say what precisely the design is for. But only guess at the design based on my experience.

In the end, if that came across my desk at work for a review. I’d fail that in a heart beat.

1 Like

If you want an example of interfaces in action… I think this one might be pretty good.

ITimeSupplier:

    /// <summary>
    /// Represents an object that supplies the current game time. See com.spacepuppy.SPTime.
    /// </summary>
    public interface ITimeSupplier
    {

        float Total { get; }
        float Delta { get; }
        float Scale { get; }

        double TotalPrecise { get; }

    }

    /// <summary>
    /// A ITimeSupplier that has a scale property (this gives things like Time.timeScale an object identity).  See com.spacepuppy.SPTime.
    /// </summary>
    public interface IScalableTimeSupplier : ITimeSupplier
    {

        event System.EventHandler TimeScaleChanged;

        bool Paused { get; set; }


        IEnumerable<string> ScaleIds { get; }

        void SetScale(string id, float scale);
        float GetScale(string id);
        bool RemoveScale(string id);
        bool HasScale(string id);

    }

This defines a TimeSupplier object that can have time retrieved from (just like Time.deltaTime, Time.time, Time.realTime, Time.unscaledTime, etc). Breaking them up into their useful relationships rather than just being rando properties of a static class (back to that whole static state nonsense).

And here are my implementations of ITimeSupplier:

    /// <summary>
    /// A static entry point to the various ITimeSuppliers in existance. An ITimeSupplier gives object identity 
    /// to the varous kinds of time out there: normal (UnityEngine.Time.time), real (UnityEngine.Time.unscaledTime), 
    /// smooth (Time.smoothDeltaTime), custom (no unity parrallel). 
    /// 
    /// With the objects in hand you can than swap out what time is used when updating objects.
    /// 
    /// For example the com.spacepuppy.Tween namespace containers tweeners that can update on any of the existing 
    /// times in unity. These objects can be passed in to allow updating at any one of those times. Lets say for 
    /// instance you've set the time scale of Normal time to 0 (to pause the game), but you still need the menu to 
    /// tween and update appropriately. Well, you'd use the 'RealTime' or a 'Custom' time object to update the 
    /// tween with instead of 'Normal'.
    /// 
    /// An added feature includes stacking TimeScales. The Normal and Custom TimeSuppliers allow naming your time 
    /// scales so that you can compound them together. This way time scales don't overlap. Lets say you have an 
    /// effect where when swinging the sword that halves the time speed to look cool, but you also have a slomo effect 
    /// when an item is picked up for a duration of time. You expect the swords half speed to be half of the slomo 
    /// effect, not half of normal time, so that the sword looks right either way. This allows you to combine those 
    /// 2 with out having to test if one or the other is currently playing (or the scale of it playing). Which is 
    /// SUPER extra handy when tweening the time scale.
    /// 
    /// 
    /// Instances of the class can be used as an inspector configurable reference to a ITimeSupplier. These configurable 
    /// instances are intended to be immutable once deserialized at runtime. Else the ITimeSupplier interface pass through 
    /// could add event callbacks to one time supplier, but later remove it from another.
    /// </summary>
    [System.Serializable()]
    public struct SPTime : ITimeSupplier
    {

        #region Fields

        [SerializeField()]
        private DeltaTimeType _timeSupplierType;
        [SerializeField()]
        private string _timeSupplierName;

        #endregion

        #region CONSTRUCTOR

        //public SPTime()
        //{
        //    //exists only for unity serialization
        //}

        public SPTime(DeltaTimeType etp)
        {
            if (etp == DeltaTimeType.Custom) throw new System.ArgumentException("For custom time suppliers, you must specify it directly.");
            _timeSupplierType = etp;
            _timeSupplierName = null;
        }

        public SPTime(ITimeSupplier ts)
        {
            if (ts == null) throw new System.ArgumentNullException("ts");
            _timeSupplierType = SPTime.GetDeltaType(ts);
            if (ts is CustomTimeSupplier) _timeSupplierName = (ts as CustomTimeSupplier).Id;
            else _timeSupplierName = null;
        }

        public SPTime(DeltaTimeType etp, string timeSupplierName)
        {
            if(etp == DeltaTimeType.Custom)
            {
                _timeSupplierType = etp;
                _timeSupplierName = timeSupplierName;
            }
            else
            {
                _timeSupplierType = etp;
                _timeSupplierName = null;
            }
        }

        #endregion

        #region Properties

        public DeltaTimeType TimeSupplierType
        {
            get { return _timeSupplierType; }
        }

        public string CustomTimeSupplierName
        {
            get { return _timeSupplierName; }
        }

        public ITimeSupplier TimeSupplier
        {
            get
            {
                return SPTime.GetTime(_timeSupplierType, _timeSupplierName);
            }
            set
            {
                _timeSupplierType = GetDeltaType(value);
                if (_timeSupplierType == DeltaTimeType.Custom)
                {
                    var cts = value as CustomTimeSupplier;
                    _timeSupplierName = (cts != null) ? cts.Id : null;
                }
                else
                {
                    _timeSupplierName = null;
                }
            }
        }

        public bool IsCustom
        {
            get { return _timeSupplierType == DeltaTimeType.Custom; }
        }

        public float Total
        {
            get
            {
                var ts = this.TimeSupplier;
                if (ts != null)
                    return ts.Total;
                else
                    return 0f;
            }
        }

        public float Delta
        {
            get
            {
                var ts = this.TimeSupplier;
                if (ts != null)
                    return ts.Delta;
                else
                    return 0f;
            }
        }

        public float Scale
        {
            get
            {
                var ts = this.TimeSupplier;
                if (ts != null)
                    return ts.Scale;
                else
                    return 0f;
            }
        }

        public double TotalPrecise
        {
            get
            {
                var ts = this.TimeSupplier;
                if (ts != null)
                    return ts.TotalPrecise;
                else
                    return 0d;
            }
        }

        #endregion




        #region Static Interface

        #region Fields

        private static Dictionary<string, CustomTimeSupplier> _customTimes;
        private static NormalTimeSupplier _normalTime = new NormalTimeSupplier();
        private static RealTimeSupplier _realTime = new RealTimeSupplier();
        private static SmoothTimeSupplier _smoothTime = new SmoothTimeSupplier();

        #endregion

        #region Properties

        /// <summary>
        /// Represents the normal update time, ala Time.time & Time.deltaTime.
        /// </summary>
        public static IScalableTimeSupplier Normal { get { return _normalTime; } }

        /// <summary>
        /// Represents the real update time, ala Time.unscaledTime.
        /// </summary>
        public static ITimeSupplier Real { get { return _realTime; } }

        /// <summary>
        /// Represents smooth delta time, ala Time.smoothDeltaTime.
        /// </summary>
        public static ITimeSupplier Smooth { get { return _smoothTime; } }

        #endregion

        #region Methods

        /// <summary>
        /// Retrieve the ITimeSupplier for a specific DeltaTimeType, see com.spacepuppy.DeltaTimeType.
        /// </summary>
        /// <param name="etp"></param>
        /// <returns></returns>
        public static ITimeSupplier GetTime(DeltaTimeType etp)
        {
            switch(etp)
            {
                case DeltaTimeType.Normal:
                    return _normalTime;
                case DeltaTimeType.Real:
                    return _realTime;
                case DeltaTimeType.Smooth:
                    return _smoothTime;
                default:
                    return _customTimes.Values.FirstOrDefault();
            }
        }

        public static ITimeSupplier GetTime(DeltaTimeType etp, string id)
        {
            switch (etp)
            {
                case DeltaTimeType.Normal:
                    return _normalTime;
                case DeltaTimeType.Real:
                    return _realTime;
                case DeltaTimeType.Smooth:
                    return _smoothTime;
                default:
                    {
                        if (id == null) return null;
                        CustomTimeSupplier ct;
                        if (_customTimes.TryGetValue(id, out ct))
                        {
                            return ct;
                        }
                        return null;
                    }
            }
        }

        /// <summary>
        /// Reverse lookup for DeltaTimeType from an object.
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
        public static DeltaTimeType GetDeltaType(ITimeSupplier time)
        {
            if (time == _normalTime || time == null)
                return DeltaTimeType.Normal;
            else if (time == _realTime)
                return DeltaTimeType.Real;
            else if (time == _smoothTime)
                return DeltaTimeType.Smooth;
            else
                return DeltaTimeType.Custom;
        }

        /// <summary>
        /// Retrieve a CustomTimeSupplier by name.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="createIfNotExists"></param>
        /// <returns></returns>
        public static CustomTimeSupplier Custom(string id, bool createIfNotExists = false)
        {
            if (id == null) return null;
            if (_customTimes == null)
            {
                if (createIfNotExists)
                {
                    _customTimes = new Dictionary<string, CustomTimeSupplier>();
                    GameLoop.RegisterInternalEarlyUpdate(SPTime.Update);
                    var ct = new CustomTimeSupplier(id);
                    _customTimes[ct.Id] = ct;
                    return ct;
                }
                else
                {
                    return null;
                }
            }
            else
            {
                CustomTimeSupplier ct;
                if (_customTimes.TryGetValue(id, out ct))
                {
                    return ct;
                }
                else if (createIfNotExists)
                {
                    ct = new CustomTimeSupplier(id);
                    _customTimes[ct.Id] = ct;
                    return ct;
                }
                else
                {
                    return null;
                }
            }
        }

        public static CustomTimeSupplier[] GetAllCustom()
        {
            if (_customTimes == null) return ArrayUtil.Empty<CustomTimeSupplier>();
            return _customTimes.Values.ToArray();
        }

        public static string GetCustomName(ITimeSupplier supplier)
        {
            if (_customTimes == null || supplier == null) return null;

            var e = _customTimes.GetEnumerator();
            while(e.MoveNext())
            {
                if (e.Current.Value == supplier) return e.Current.Key;
            }
            return null;
        }

        public static bool HasCustom(string id)
        {
            if (id == null) return false;
            if (_customTimes == null) return false;
            return _customTimes.ContainsKey(id);
        }

        /// <summary>
        /// Removes a CustomTimeSupplier from the update pool by name.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static bool RemoveCustomTime(string id)
        {
            if (id == null) return false;
            if (_customTimes != null) return _customTimes.Remove(id);
            else return false;
        }

        /// <summary>
        /// Removes a CustomTimeSupplier from the update pool by reference.
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
        public static bool RemoveCustomTime(CustomTimeSupplier time)
        {
            if (_customTimes != null)
            {
                if (_customTimes.ContainsKey(time.Id) && _customTimes[time.Id] == time)
                {
                    return _customTimes.Remove(time.Id);
                }
                else
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Returns the scale relative to NormalTime that would cause something updating by normal time appear at the scale of 'supplier'.
        /// Basically if you have an Animation/Animator, which animates relative to Time.timeScale, and you want to set the 'speed' property of it 
        /// to a value so that it appeared at the speed that is defined in 'supplier', you'd set it to this value.
        /// </summary>
        /// <param name="supplier"></param>
        /// <returns></returns>
        public static float GetInverseScale(ITimeSupplier supplier)
        {
            if (supplier == null) return 1f;
            if (supplier is NormalTimeSupplier) return 1f;

            return supplier.Scale / Time.timeScale;
        }

        private static void Update(bool isFixed)
        {
            if(_customTimes.Count > 0)
            {
                var e = _customTimes.GetEnumerator();
                while(e.MoveNext())
                {
                    e.Current.Value.Update(isFixed);
                }
            }
        }

        #endregion

        #endregion

        #region Special Types

        private class NormalTimeSupplier : IScalableTimeSupplier
        {

            private bool _paused;
            private Dictionary<string, float> _scales = new Dictionary<string, float>();

            public event System.EventHandler TimeScaleChanged;

            public float Total
            {
                get { return UnityEngine.Time.time; }
            }

            public double TotalPrecise
            {
                get { return (double)UnityEngine.Time.time; }
            }

            public float Delta
            {
                get { return UnityEngine.Time.deltaTime; }
            }

            public bool Paused
            {
                get { return _paused; }
                set
                {
                    if (_paused == value) return;
                    _paused = value;
                    if (_paused)
                    {
                        UnityEngine.Time.timeScale = 0f;
                    }
                    else
                    {
                        this.SyncTimeScale();
                    }
                }
            }

            public float Scale
            {
                get
                {
                    return (_paused) ? this.GetTimeScale() : UnityEngine.Time.timeScale;
                }
            }

            public IEnumerable<string> ScaleIds
            {
                get { return _scales.Keys; }
            }

            public void SetScale(string id, float scale)
            {
                _scales[id] = scale;
                if(!_paused)
                {
                    this.SyncTimeScale();
                }
            }

            public float GetScale(string id)
            {
                float result;
                if(_scales.TryGetValue(id, out result))
                {
                    return result;
                }
                else
                {
                    return float.NaN;
                }
            }

            public bool RemoveScale(string id)
            {
                if (_scales.Remove(id))
                {
                    if (!_paused)
                    {
                        this.SyncTimeScale();
                    }
                    return true;
                }
                else
                {
                    return false;
                }
            }

            public bool HasScale(string id)
            {
                return _scales.ContainsKey(id);
            }

            private void SyncTimeScale()
            {
                float result = this.GetTimeScale();
               
                if (MathUtil.FuzzyEqual(result, UnityEngine.Time.timeScale))
                {
                    UnityEngine.Time.timeScale = result;
                }
                else
                {
                    UnityEngine.Time.timeScale = result;
                    if (this.TimeScaleChanged != null) this.TimeScaleChanged(this, System.EventArgs.Empty);
                }

                if(Mathf.Approximately(result, UnityEngine.Time.timeScale))
                {
                    UnityEngine.Time.timeScale = result;
                }
                else
                {
                    UnityEngine.Time.timeScale = result;
                    if (this.TimeScaleChanged != null) this.TimeScaleChanged(this, System.EventArgs.Empty);
                }
            }

            private float GetTimeScale()
            {
                float result = 1f;
                if (_scales.Count > 0)
                {
                    var e = _scales.GetEnumerator();
                    while (e.MoveNext())
                    {
                        result *= e.Current.Value;
                    }
                }
                return result;
            }

        }

        private class RealTimeSupplier : ITimeSupplier
        {

            public float Total
            {
                get { return UnityEngine.Time.unscaledTime; }
            }

            public double TotalPrecise
            {
                get { return (double)UnityEngine.Time.unscaledTime; }
            }

            public float Delta
            {
                get { return UnityEngine.Time.unscaledDeltaTime; }
            }

            public float Scale
            {
                get
                {
                    return 1f;
                }
            }

        }

        private class SmoothTimeSupplier : IScalableTimeSupplier
        {

            public float Total
            {
                get { return UnityEngine.Time.time; }
            }

            public double TotalPrecise
            {
                get { return (double)UnityEngine.Time.time; }
            }

            public float Delta
            {
                get { return UnityEngine.Time.smoothDeltaTime; }
            }

            public float Scale
            {
                get
                {
                    return SPTime.Normal.Scale;
                }
            }

            bool IScalableTimeSupplier.Paused
            {
                get
                {
                    return SPTime.Normal.Paused;
                }
                set
                {
                    throw new System.NotSupportedException();
                }
            }

            IEnumerable<string> IScalableTimeSupplier.ScaleIds
            {
                get
                {
                    return SPTime.Normal.ScaleIds;
                }
            }

            event System.EventHandler IScalableTimeSupplier.TimeScaleChanged
            {
                add
                {
                    SPTime.Normal.TimeScaleChanged += value;
                }

                remove
                {
                    SPTime.Normal.TimeScaleChanged -= value;
                }
            }

            void IScalableTimeSupplier.SetScale(string id, float scale)
            {
                throw new System.NotSupportedException();
            }

            float IScalableTimeSupplier.GetScale(string id)
            {
                return SPTime.Normal.GetScale(id);
            }

            bool IScalableTimeSupplier.RemoveScale(string id)
            {
                throw new System.NotSupportedException();
            }

            bool IScalableTimeSupplier.HasScale(string id)
            {
                return SPTime.Normal.HasScale(id);
            }
        }

        #endregion

        #region Special Config Types

        [System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
        public class Config : System.Attribute
        {

#region Fields

            public string[] AvailableCustomTimeNames;

#endregion

            public Config(params string[] availableCustomTimeNames)
            {
                this.AvailableCustomTimeNames = availableCustomTimeNames;
            }

        }

#endregion

    }

Note how we have versions of ITimeSupplier like:
NormalTimeSupplier
RealTimeSuppliler
SmoothTimeSupplier
and CustomTimeSupplier found here:

Now that they’re objects with object-identity we can pass references to different ITimeSuppliers now. And since they implement the interface ITimeSupplier, they can be independent class types as well.

As a result you can have a function like this that takes in an ITimeSupplier and therefore is time agnostic:

Vector3 MoveAtSpeed(Vector3 start, Vector3 dir, float speed, ITimeSupplier time)
{
    return start + dir.normalized * speed * time.DeltaTime;
}

Or you can build more complex systems around it. Like I wrote my entire SPTween library around the idea of a ITimeSupplier so that you can tween objects at different times by just passing in the ITimeSupplier of your choice:

You may also notice that SPTime is defined as a struct, this is because I’ve created an editor property drawer for SPTime so that it can allow you to have configurable time information on your MonoBehaviours:

So you can do:

public class SomeScript : MonoBehaviour
{
   
    public SPTime Time;
    public Vector3 Dir;
    public float Speed;
   
    void Update()
    {
        this.transform.position = MoveAtSpeed(this.transform.position, Dir, Speed, Time.TimeSupplier);
    }
   
}

Thanks for the answer. I think I understand a bit more thanks to your example. Indeed the static function ProcessCalculation is quite confusing. I’m going to try to play around with it to get a better understanding.

It’s pretty much the same concept as having objects as parameters and/or return types, the only difference being that interfaces are more abstract and can be applied to any object that implements them.

For instance, imagine you wanted to create a method that compares two arrays and returns a true or false value if both arrays have the same amount of elements inside of them:

public bool ContainsSameLength(object[] arr1, object[] arr2) {
   return arr1.Length == arr2.Length;
}

Now imagine if, later down the line, you have some Lists, Queues, SortedSets, or other kinds of collections, and you want to compare the lengths of these various different collections as well. The method written above only accepts arrays, so we cannot pass in any of these other collection types. Likewise, if we change the method to something like this…

public bool ContainsSameLength(List<object> col1, List<object> col2) {
   return col1.Count == col2.Count;
}

…We could now compare Lists, but no longer arrays or any other collection types.

This is where the abstraction of interfaces comes in handy. Arrays, Lists, Queues, and SortedSets all implement an interface called ICollection. If we take a look at the docs, we can see that an ICollection has a Count property. This means any class that implements ICollection is guaranteed to have this Count property as well. This is a rule that applies to implementing interfaces in general.

If we change the method to accept ICollection parameters like so…

public bool ContainsSameLength(ICollection col1, ICollection col2) {
   return col1.Count == col2.Count;
}

…Then we can now pass any of these types of collections into the method. It effectively now does not care what type of collection the parameters are, only that they are indeed collections.

That calculator example probably does nothing. It’s like using “if(false) {}” to show how if’s work. The one in UnityLearn (kill/damage) could be something, but doesn’t have enough yet to show to real reason you’d need them.

And in practice, you’ll probably never need to create one yourself. Some built-in C# functions require one – a version of Sort does – and you can mostly follow the steps to add the interface to whatever class you want to sort.