Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compositional delegates example #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

NTCoding
Copy link

@NTCoding NTCoding commented Apr 26, 2018

Hi @jchannon,

I love what you're doing here. This PR contains an example of the patterns I've used in the past - both in C# and Scala / Play Framework. It uses basic functional techniques: currying to simulate object constructors, and returning functions from functions. With C#s local functions, the syntax is quite neat, too.

Like you, I found I got the same benefits - loosely-coupled, testable code, yet with none of the class, interface, and mocking noisy syntax so it was much easier to read and maintain.

@jchannon
Copy link
Owner

jchannon commented May 1, 2018

Looks very similar to the RouteHandler class I have apart from it uses local functions rather than delegates?

Thanks!

@NTCoding
Copy link
Author

NTCoding commented May 1, 2018

Yeah, the RouteHandler class does the same job as my ApplicationServices class. It wires up dependencies into the Function factories to create the functions. This is equivalent to creating new objects and passing in constructor dependencies.

The main difference is below:

public static class CreateFilmRoute
    {
        public static void Handle(Film film, ValidUserDelegate validUserQuery)
        {
            if (!validUserQuery())
            {
                throw new InvalidOperationException();
            }

            //Do some special MEGA CORP business validation

            //Save to database by writing SQL here
        }
    }

This is your application logic, but the interface Handle contains the ValidUserDelegate which has the exact same value for every invocation of the function and also other implementations of Handle may not need that dependency - so it isn't really part of the interface.

In the example I provided, we use currying to make it explicit which are the dependencies and which are the parameters that will be different for each invocation:

// This is effectively an object constructor
        public static CreateFilm Create(ValidUserDelegate validUserQuery)
        {
            // C# local method syntax makes it easy to return a function from a function
            // This function is basically the wired up object
            void func(Film film)
            {
                if (!validUserQuery())
                {
                    throw new InvalidOperationException();
                }

                //Do some special MEGA CORP business validation

                //Save to database by writing SQL here
            }

            return func;
       }

I prefer this approach because:

  • The dependencies and public interface are separate. It's clear here that ValidUserDelegate is a constructor parameter.

You have moved this responsibility into a higher level of the application - anyone using the function has to know which parameters should always be the same value and which are dynamic:

CreateFilmHandler = film => CreateFilmRoute.Handle(film, () => ValidUserQuery.Execute());

The result is the same - a delegate that takes a film and returns nothing. But the knowledge of knowing that a ValidUserDelegate is a constructor parameter is now duplicated in two places.

Whereas in the pattern I showed, you can only use the function in the expected way. You have to pass in the constructor parameter to construct the function:

ValidUserDelegate vud = () => ValidUserQuery.Execute();
// Use currying to supply 'constructor' dependencies
createFilm = Films.Create(vud);

@jchannon
Copy link
Owner

jchannon commented May 1, 2018

I don't have an interface though 😄

@jchannon
Copy link
Owner

jchannon commented May 1, 2018

@NTCoding
Copy link
Author

NTCoding commented May 1, 2018

@jchannon Yeah, the delegate is the top-level interface called by your 'controller'.

By interface I meant the signature of your function which takes a validUserQuery and a Film. That interface (interface in the general term https://stackoverflow.com/questions/2866987/what-is-the-definition-of-interface-in-object-oriented-programming) implies that to create a film you pass in a film and you pass in a valid user query and those two arguments are different for each invocation of the function.

Really, the valid user query is a collaborator that the function depends on, so it doesn't need to be passed into each invocation. You've made that clear in your RouteHandler (where it passes in the same instance of valid user query), I'm showing an example where you can encapsulate that in a single class.

@NTCoding
Copy link
Author

NTCoding commented May 1, 2018

If we were to consider these two as objects, what I'm proposing is:

public class CreateFilm 
{
    public CreateFilm(ValidateUser validate) 
   {
     ...
   }

   public void Create(Film film)
   {
      ...
    }
}

^^^ The object itself owns the rules of deciding which dependencies are collaborators via the constructor and which parameters values can vary on each invocation of the function

Your base example is like this:

public class CreateFilm
{
    public Action<Film, ValidUserQuery> Create() 
    {
       ...
    }
}

In this example, the object itself is just the raw function, and it's upto consumers to decide how to curry the arguments. There's more flexibility here - you can pass a different ValidUserQuery into each invocation - that's what the signature is saying here - both of the these parameters must be supplied on each invocation.

But if your intention is for ValidUserQuery to be a collaborator that is used by every invocation of the object, anyone using your CreatFilm function has to remember this.

It's not right or wrong, it's just about making your intentions clear and making the compiler enforce them for you.

@jchannon
Copy link
Owner

jchannon commented May 1, 2018 via email

@NTCoding
Copy link
Author

NTCoding commented May 1, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants