user219396
user219396

Reputation: 57

General design guidance c#; finding im unnecessarily passing objects between methods

Sorry its a bit vague perhaps but its been bugging me for weeks. I find each project I tackle I end up making what I think is a design mistake and am pretty sure theres a bettwe way.

When defining a class thats serialized from an event source like a sinple json doc definition. Lets call it keys class with various defined integers, bools and strings. i have multiple methods that make use of this and i find that i constantly need to paas this class as an object by means of an overload. So method a calls methods b, method b doesnt need these objects but it calls method c which does... In doing this bad practice im passing these 'keys' objects to method b for the sole purpose of method c accessibility.

Im probably missing one major OOP fundamental :) any guidance or reading would be appreciated as im googled out!!

public class Keys
{
    public child Detail { get; set; }
}

public class child
{
    public string instance { get; set; }
}

//my main entry point
public void FunctionHandler(Keys input, ILambdaContext context)
{
    methodA(input)
}

static void methodA(Keys input) 
{
   //some-other logic or test that doesn't need Keys object/class if (foo==bar) {proceed=true;}
   string foo = methodB(input)
}

static string methodB(Keys input) 
{
    //here i need Keys do do stuff and I return a string in this example
}

Upvotes: 1

Views: 92

Answers (1)

Peter - Reinstate Monica
Peter - Reinstate Monica

Reputation: 16016

What you do is not necessarily bad or wrong. Remember that in C# what you actually pass are references, not objects proper, so the overhead of parameter passing is really small.

The main downside of long call chains is that the program logic is perhaps more complicated than it needs to be, with the usual maintainability issues.

Sometimes you can use the C# type system to let the compiler or the run time choose the proper function.

The compiler is employed when you overload method() for two different types instead of defining methodA() and methodB(). But they are distinguished by the parameter type, so you need different Key types which may be (but don't have to be) related:

public class KeyA {/*...*/}
public class KeyB {/*...*/}

void method(KeyA kA) { /* do something with kA */ }
void method(KeyB kB) { /* do something with kB */ }

This is of limited benefit; that the functions have the same name is just syntactic sugar which makes it clear that they serve the same purpose.

The other, perhaps more elegant and versatile technique is to create an inheritance hierarchy of Keys which each "know" what a method should do.

You'll need a base class with a virtual method which will be overridden by the inheriting classes. Often the base is an interface just declaring that there is some method(), and the various implementing types implement a method() which suits them. Here is a somewhat lengthy example which uses a virtual Output() method so that we see something on the Console.

It's noteworthy that each Key calls a method of an OutputterI, passing itself to it as a parameter; the outputter class then in turn calls back a method of the calling object. That's called "Double Dispatch" and combines run-time polymorphism with compile-time function overloading. At compile time the object and it's concrete type are not known; in fact, they can be implemented later (e.g. by inventing another Key). But each object knows what to do when its callback function (here: GetData()) is called.

using System;
using System.Collections.Generic;

namespace DoubleDispatch
{
    interface KeyI
    {   // They actually delegate that to an outputter
        void Output(); 
    }

    interface OutputterI
    {
        void Output(KeyA kA);
        void Output(KeyExtra kE);
        void Output(KeyI k); // whatever this does.
    }

    class KeyBase: KeyI
    {
        protected OutputterI o;

        public KeyBase(OutputterI oArg) { o = oArg; }

        // This will call Output(KeyI))
        public virtual void Output() { o.Output(this); }
    }

    class KeyA : KeyBase
    {
        public KeyA(OutputterI oArg) : base(oArg) { }

        public string GetAData() { return "KeyA Data"; }

        // This will compile to call Output(KeyA kA) because
        // we pass this which is known here to be of type KeyA
        public override void Output() { o.Output(this); }
    }

    class KeyExtra : KeyBase
    {
        public string GetEData() { return "KeyB Data"; }
        public KeyExtra(OutputterI oArg) : base(oArg) { }

        /** Some extra data which needs to be handled during output. */
        public string GetExtraInfo() { return "KeyB Extra Data"; }

        // This will, as is desired,
        // compile to call o.Output(KeyExtra) 
        public override void Output() { o.Output(this); }
    }

    class KeyConsolePrinter : OutputterI
    {
        // Note: No way to print KeyBase.
        public void Output(KeyA kA) { Console.WriteLine(kA.GetAData());  }
        public void Output(KeyExtra kE)
        {
            Console.Write(kE.GetEData() + ", ");
            Console.WriteLine(kE.GetExtraInfo());
        }
        // default method for other KeyI
        public void Output(KeyI otherKey) { Console.WriteLine("Got an unknown key type"); }

    }
    // similar for class KeyScreenDisplayer{...} etc.

    class DoubleDispatch
    {
        static void Main(string[] args)
        {

            KeyConsolePrinter kp = new KeyConsolePrinter();

            KeyBase b = new KeyBase(kp);
            KeyBase a = new KeyA(kp);
            KeyBase e = new KeyExtra(kp);

            // Uninteresting, direkt case: We know at compile time
            // what each object is and could simply call kp.Output(a) etc.
            Console.Write("base:\t\t");
            b.Output();

            Console.Write("KeyA:\t\t");
            a.Output();

            Console.Write("KeyExtra:\t");
            e.Output();

            List<KeyI> list = new List<KeyI>() { b, a, e };
            Console.WriteLine("\nb,a,e through KeyI:");

            // Interesting case: We would normally not know which
            // type each element in the vector has. But each type's specific
            // Output() method is called -- and we know it must have
            // one because that's part of the interface signature.
            // Inside each type's Output() method in turn, the correct
            // OutputterI::Output() for the given real type was 
            // chosen at compile time dpending on the type of the respective
            // "this"" argument.
            foreach (var k in list) { k.Output(); }
        }
    }
}

Sample output:

base:           Got an unknown key type
KeyA:           KeyA Data
KeyExtra:       KeyB Data, KeyB Extra Data

b,a,e through KeyI:
Got an unknown key type
KeyA Data
KeyB Data, KeyB Extra Data

Upvotes: 3

Related Questions