1

I'm trying to design a pattern to orchest several operations. Each operation would take a parameter and deliver a result. That result might or might not be used by the following operation. This is a simplified version of the design, but if you copy/paste this on a console projecto it will "work" (there's a compiling error I can't get fixed).

Error

The type 'ConsoleApplication1.InternalDebit' cannot be used as type parameter 'T1' in the generic type or method 'ConsoleApplication1.Orchestrator.Add(T1)'. There is no implicit reference conversion from 'ConsoleApplication1.InternalDebit' to 'ConsoleApplication1.Operation'. c:\projects\BCP\BaseMvc\ConsoleApplication1\ConsoleApplication1\Program.cs 17 13 ConsoleApplication1

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var internalDebit = new InternalDebit<InternalDebitParameter, InterbankCreditParameter>(new InternalDebitParameter() { Id = 1 });

            var orchestrator = new Orchestrator();

            // error here!
            orchestrator.Add(internalDebit);
        }
    }

    public interface IParameter
    {
    }

    public interface IResult
    {
    }

    public interface IReversible
    {
        void Reverse();
    }


    public interface IOperation<T, R>
        where T : class, IParameter
        where R : class, IResult
    {
        Type ParameterType { get; }

        Type ResultType { get; }

        T Parameter { get; set; }

        R Execute(T parameter);
    }

    public abstract class Operation<T, R> : IOperation<T, R>
        where T : class, IParameter
        where R : class, IResult
    {
        public virtual R Execute(T parameter)
        {
            this.Parameter = parameter;
            return default(R);
        }

        public Type ParameterType
        {
            get { return typeof(T); }
        }

        public Type ResultType
        {
            get { return typeof(R); }
        }

        public T Parameter { get; set; }

        public Operation(T parameter)
        {
            this.Parameter = parameter;
        }
    }

    public class InternalDebitParameter : IParameter
    {
        public int Id { get; set; }
    }

    public class InterbankCreditParameter : IParameter, IResult
    {
        public int Id { get; set; }
    }



    public class InternalDebit<T, R> : Operation<T, R>
        where T : class, IParameter
        where R : class, IResult
    {
        public InternalDebit(T parameter)
            : base(parameter)
        {
        }

        public override R Execute(T parameter)
        {
            return new InterbankCreditParameter() { Id = 2 } as R;
        }
    }

    public class Orchestrator
    {
        public List<Operation<IParameter, IResult>> Operations { get; private set; }

        public List<IParameter> Parameters { get; private set; }

        public void Add<T1>(T1 t) where T1 : Operation<IParameter, IResult>
        {
            this.Operations.Add(t);
        }

        public void SetUpParameters(params IParameter[] parameters)
        {
            this.Parameters = new List<IParameter>();

            parameters.ToList().ForEach(s => this.Parameters.Add(s));
        }

        public void Play()
        {
            IParameter generalResult = null;

            foreach (var instrument in this.Operations)
            {
                var parameter = this.Parameters.FirstOrDefault(s => s.GetType() == instrument.ParameterType);

                if (parameter == null)
                {
                    IResult actualResult = null;

                    if (generalResult != null)
                    {
                        try
                        {
                            actualResult = instrument.Execute(generalResult);
                        }
                        catch (Exception ex)
                        {
                            if (instrument is IReversible)
                                ((IReversible)instrument).Reverse();
                            else
                                throw;
                            break;
                        }
                        finally
                        {
                            if (actualResult is IParameter)
                                generalResult = (IParameter)actualResult;
                        }
                    }
                    else
                    {
                        throw new Exception("Orchetrator missconfiguration.");
                    }
                }
            }
        }
    }
}
danielQ
  • 2,016
  • 1
  • 15
  • 19
  • 1
    This is happening because `Operation` cannot be cast to `Operation`. Your example is a bit too complex for me to wrap my head around what its purpose is, but would it be possible to change your Orchestrator class to use generic class parameters (i.e. make it `Orchestrator`)? – JLRishe Apr 23 '13 at 20:46
  • I'm guessing this falls under the umbrella of [variance](http://msdn.microsoft.com/en-ca/library/dd799517.aspx). That your `Add` method is looking _specifically_ for an `Operation` but you're supplying an `Operation`. Since the `Operation` interface is defining both `in` and `out` parameters (see the method/properties) it might not be an easy fix. EDIT: That is, as far as the compiler is concerned, you could assign a `SomeOtherDebitParameter` to `Operation.Parameter` in `Orcestrator` which would be a bad thing. – Chris Sinclair Apr 23 '13 at 20:47
  • @JLRishe no, I can't do that, since I don't know how many generics I would need. – danielQ Apr 23 '13 at 20:49
  • 1
    Id say the other two have it pegged. generics can be strange like that. Can you un-cast `Operation<>` and make a true base class for it that doesn't implement any generic types, just plain old `Operation()`, perhaps as an abstract class, and then have your other classes implement that, giving your Orchestrator engine some concrete base object to run against? – Nevyn Apr 23 '13 at 20:51
  • 1
    Is it possible to modify your `IOperation` to have a base, non-generic interface where `Parameter` and `Execute` work with simply an `IParameter` and `IResult`? There doesn't seem to be any _strict_ need in the current usage or `Orchestrator` to restrict them to the generic types. (you can always have your `Operation` abstract class perform type-checking or be generic as well) – Chris Sinclair Apr 23 '13 at 20:53
  • 1
    One thing to note here is that your orchestrator is not actually making use of the generic nature of the other classes. If you're not going to take advantage of the generics, you should consider removing them entirely, or alternatively, you can define a non-generic `Operation` and have `Operation` inherit from that. – JLRishe Apr 23 '13 at 20:54
  • 1
    @JLRishe GET OUT OF MY HEAD! :) – Chris Sinclair Apr 23 '13 at 20:54
  • @ChrisSinclair Thanks, it's discutible, the desing is still on progress, but what would fix the issue, given that downside? – danielQ Apr 23 '13 at 20:56
  • Oh! I understand what you guys are saying, I'll try removing Generics from IOperation. Thanks ChrisSinclair, JLRishe, Nevyn. – danielQ Apr 23 '13 at 20:59

3 Answers3

1

You're taking generics too far into C++ templating power. On the line that gives the error you're implicitly creating the function:

 public void Add(InternalDebit<InternalDebitParameter, InterbankCreditParameter>);

As declared, this class inherits from:

 Operation<InternalDebitParameter, InterbankCreditParameter>

The generic requirement howeveer states that T1 should be of type Operation<IParameter, IResult>, which it isn't, even though both parameters do inherit from the correct types, since there is no polymorphism allowed.

What you're trying to achieve here is inherently impossible with generics (or templates in C++ actually) because you are specifying way too much, and specifying inheritance requirements that can never be satisfied. You need to remember that generics are in a way just a luxury shorthand of writing many classes with only a little bit of code, they do not introduce recursive polymorphism all of a sudden.

Long story short, rewrite the code to use inheritance and base classes rather than depending on generics. I suspect your entire pattern is possible without a single generic and just as type safe.

Niels Keurentjes
  • 41,402
  • 9
  • 98
  • 136
  • Thanks, both @Paolo Falabella and yours answers pointed me in the right direction. Unfortunatelly I have to choose one, but points are for you too. – danielQ Apr 24 '13 at 15:58
1

If you play a little with covariance/contravariance you may be able to do something similar to what you're after. Or anyway, the compiler will tell you more precisely where what you're trying to do is not type-safe.

First step: the error you're getting states that There is no implicit reference conversion from 'InternalDebit<InternalDebitParameter,InterbankCreditParameter>' to 'Operation<IParameter,IResult>'.

So, since InternalDebit implements IOperation, the first thing you can do is make IOperation covariant, trying to define it as:

public interface IOperation<out T, out R>

This would mean that a variable of type IOperation<IParameter,IResult> would happily accept a value of type Operation<InternalDebitParameter,InterbankCreditParameter>, which is one step closer to what you want. You would then have your Add's method signature constrained in terms of IOperation instead of Operation

public void Add<T1>(T1 t) where T1 : IOperation<IParameter, IResult>

The compiler tells us something's wrong:

Invalid variance: The type parameter 'T' must be invariantly valid on 'IOperation<T,R>.Parameter'. 'T' is covariant.

Invalid variance: The type parameter 'T' must be contravariantly valid on 'IOperation<T,R>.Execute(T)'. 'T' is covariant.

That's the first indication of why this code is unsound. Covariant parameters can only be used "on the way out" of function (i.e. as a return type), not as "in" parameters.

Second step making IOperation covariant. This may be painful and change your code, as it means changing Execute not to accept parameters of type T.

public interface IOperation<out T, out R>
    where T : class, IParameter
    where R : class, IResult
{
    Type ParameterType { get; }

    Type ResultType { get; }

    T Parameter { get; /*set;*/ } //can't allow the interface to set T 

    // R Execute(T parameter); // can't have an Execute with T as a parameter
    R Execute(); // you can however inject T in the constructor of the
                 // inherited class and call Execute without parameters    
}

Third step you now get a new error:

The best overloaded method match for 'System.Collections.Generic.List<Operation<IParameter,IResult>>.Add(Operation<IParameter,IResult>)' has some invalid arguments

This is again a covariance issue. List is not covariant and you can't Add t to a List. I don't really know what to suggest,since I don't want to change completely the intent of your code (especially since I can't say I fully understand it...) You may find something useful in this answer, for instance:

Covariance and IList

Community
  • 1
  • 1
Paolo Falabella
  • 24,914
  • 3
  • 72
  • 86
  • Thanks! Great covariant explanation, I've learned How to use it now. I'll post final code. It can be better, but I can't spend more time with this for now. – danielQ Apr 24 '13 at 15:51
  • @Daniel glad this helped. If you post your final code as an answer to yourself, I can take a look and comment. – Paolo Falabella Apr 24 '13 at 15:56
1

Ok, for the sake of completeness of this post, I'll show you how I finally get this working. It can be better, I'm still open to suggestions. Unfortunatelly I got to move on from this task, it's already delayed.

I'll post and edit to this answer in order to follow up it on Code Review site.

Copy/Paste in a console application, it's a fully functional code example.

class Program
    {
        static void Main(string[] args)
        {
            var transferenceInfo = new InterbankTranferenceInfo();

            var orchestrator = new Orchestrator(new InternalDebitOperation(transferenceInfo),
                                                new InterbankCreditOperation(),
                                                new CommissionOperation());

            orchestrator.Run();
        }
    }

    public class InterbankTranferenceInfo : IParameter
    {
        public bool InternalDebitDone { get; set; }

        public bool InterbankCreditDone { get; set; }

        public bool CommissionDone { get; set; }
    }

    public class InternalDebitOperation : Operation<InterbankTranferenceInfo>, IOperation<InterbankTranferenceInfo>
    {
        public InternalDebitOperation(InterbankTranferenceInfo parameter)
            : base(parameter)
        {
        }

        public override InterbankTranferenceInfo Execute()
        {
            return new InterbankTranferenceInfo() { InternalDebitDone = true };
        }
    }

    public class InterbankCreditOperation : Operation<InterbankTranferenceInfo>, IOperation<InterbankTranferenceInfo>
    {
        public override InterbankTranferenceInfo Execute()
        {
            Parameter.InterbankCreditDone = true;

            return Parameter;
        }
    }

    public class CommissionOperation : Operation<InterbankTranferenceInfo>, IReversible, IOperation<InterbankTranferenceInfo>
    {
        public override InterbankTranferenceInfo Execute()
        {
            Parameter.CommissionDone = true;

            // Uncomment this code to test Reverse operation.
            // throw new Exception("Test exception, it should trigger Reverse() method.");

            return Parameter;
        }

        public void Reverse()
        {
            Parameter.CommissionDone = false;
        }
    }

    public enum OperationStatus
    {
        Done,
        Pending,
        Reversed
    }

    public interface IParameter
    {
    }

    public interface IReversible
    {
        void Reverse();
    }

    public interface IOperation<out T> : IInternalOperation<T>  where T : IParameter
    {
    }

    public interface IInternalOperation<out T> : IExecutableOperation<T>
    {
        bool GetParameterFromParentOperation { get; }

        OperationStatus Status { get; set; }

        IParameter Execute(IParameter parameter);      
    }

    public interface IExecutableOperation<out T>
    {
        T Execute();
    }

    //[System.Diagnostics.DebuggerStepThroughAttribute()]
    public abstract class Operation<T> : IInternalOperation<T> where T : IParameter
    {
        public T Parameter { get; private set; }

        public bool GetParameterFromParentOperation { get { return this.Parameter == null; } }

        public OperationStatus Status { get; set; }

        public Operation()
        {
            Status = OperationStatus.Pending;
        }

        public Operation(IParameter parameter)
        {
            Status = OperationStatus.Pending;
            this.Parameter = (T)parameter;
        }

        public abstract T Execute();

        public virtual IParameter Execute(IParameter parameter)
        {
            this.Parameter = (T)parameter;
            return this.Execute();
        }
    }

    public class Orchestrator
    {
        public List<IOperation<IParameter>> Operations { get; private set; }

        public Orchestrator(params IOperation<IParameter>[] operations) 
        {
            this.Operations = new List<IOperation<IParameter>>();

            foreach (var item in operations)
            {
                this.Operations.Add((IOperation<IParameter>)item);
            }
        }

        public IParameter Run()
        {
            IParameter previousOperationResult = null;

            foreach (var operation in this.Operations)
            {
                try
                {
                    if (operation.GetParameterFromParentOperation)
                        previousOperationResult = operation.Execute(previousOperationResult);
                    else
                        previousOperationResult = operation.Execute();

                    operation.Status = OperationStatus.Done;
                }
                catch (Exception)
                {
                    foreach (var o in this.Operations)
                    {
                        if (o is IReversible)
                        {
                            ((IReversible)o).Reverse();
                            o.Status = OperationStatus.Reversed;
                        }
                        else
                            throw;
                    }
                    break;
                }
            }

            return previousOperationResult;
        }
    }

EDIT

Code Review Post

Community
  • 1
  • 1
danielQ
  • 2,016
  • 1
  • 15
  • 19