6

I'm taking a crack at writing my first DSL for a simple tool at work. I'm using the builder pattern to setup the complex parent object but am running into brick walls for building out the child collections of the parent object. Here's a sample:

Use:

var myMorningCoffee = Coffee.Make.WithCream().WithOuncesToServe(16);

Sample with closure (I think that's what they're called):

var myMorningCoffee = Coffee.Make.WithCream().PourIn( 
                        x => {
                                x.ShotOfExpresso.AtTemperature(100);
                                x.ShotOfExpresso.AtTemperature(100).OfPremiumType();
                             }
                        ).WithOuncesToServe(16);

Sample class (without the child PourIn() method as this is what I'm trying to figure out.)

 public class Coffee
 {
   private bool _cream;

   public Coffee Make { get new Coffee(); }
   public Coffee WithCream()
   {
     _cream = true;
     return this;
   }
   public Coffee WithOuncesToServe(int ounces)
   {
     _ounces = ounces;
     return this;
   }
 }

So in my app for work I have the complex object building just fine, but I can't for the life of me figure out how to get the lambda coded for the sub collection on the parent object. (in this example it's the shots (child collection) of expresso).

Perhaps I'm confusing concepts here and I don't mind being set straight; however, I really like how this reads and would like to figure out how to get this working.

Thanks, Sam

sam
  • 753
  • 1
  • 6
  • 15
  • I've got to be honest; that is truly a terrible use of a DSL, IMHO. It reads horribly to me. But to each his own, I suppose. – Noon Silk Nov 25 '09 at 05:33
  • So what is your problem? All of this code looks proprietary, so we can have no way of knowing what any of it means. For instance, what is the parameter type of IncludeApps? – tster Nov 25 '09 at 05:35
  • Can you post the signature for the IncludeApps method? – Kirk Broadhurst Nov 25 '09 at 05:38
  • This looks more like a fluent interface than a DSL. – James Black Nov 25 '09 at 05:39
  • I agree, this sample sucks, I was hoping it would convey my intent but maybe not. I would agree with "James Black" that this may be more of a fluent interface than a DSL. Regarding the signature for the "IncludeApps" method, that's my point, I'm not really sure what this should be. It's a child collection of a different object so I guess you're free to come up with any sample. – sam Nov 25 '09 at 05:45
  • Why not explain in words what you are trying to convey, as a starting point. Is this for an internal DSL or something that a person would be using? – James Black Nov 25 '09 at 06:02

3 Answers3

3

Ok, so I figured out how to write my DSL using an additional expression builder. This is how I wanted my DSL to read:

var myPreferredCoffeeFromStarbucks =
            Coffee.Make.WithCream().PourIn(
                x =>
                    {
                        x.ShotOfExpresso().AtTemperature(100);
                        x.ShotOfExpresso().AtTemperature(100).OfPremiumType();
                    }
                ).ACupSizeInOunces(16);

Here's my passing test:

[TestFixture]
public class CoffeeTests
{
    [Test]
    public void Can_Create_A_Caramel_Macchiato()
    {
        var myPreferredCoffeeFromStarbucks =
            Coffee.Make.WithCream().PourIn(
                x =>
                    {
                        x.ShotOfExpresso().AtTemperature(100);
                        x.ShotOfExpresso().AtTemperature(100).OfPremiumType();
                    }
                ).ACupSizeInOunces(16);

        Assert.IsTrue(myPreferredCoffeeFromStarbucks.expressoExpressions[0].ExpressoShots.Count == 2);
        Assert.IsTrue(myPreferredCoffeeFromStarbucks.expressoExpressions[0].ExpressoShots.Dequeue().IsOfPremiumType == true);
        Assert.IsTrue(myPreferredCoffeeFromStarbucks.expressoExpressions[0].ExpressoShots.Dequeue().IsOfPremiumType == false);
        Assert.IsTrue(myPreferredCoffeeFromStarbucks.CupSizeInOunces.Equals(16));
    }
}

And here's my CoffeeExpressionBuilder DSL class(s):

public class Coffee
{
    public List<ExpressoExpressionBuilder> expressoExpressions { get; private set; }

    public bool HasCream { get; private set; }
    public int CupSizeInOunces { get; private set; }

    public static Coffee Make
    {
        get
        {
            var coffee = new Coffee
                             {
                                 expressoExpressions = new List<ExpressoExpressionBuilder>()
                             };

            return coffee;
        }
    }

    public Coffee WithCream()
    {
        HasCream = true;
        return this;
    }

    public Coffee ACupSizeInOunces(int ounces)
    {
        CupSizeInOunces = ounces;

        return this;
    }

    public Coffee PourIn(Action<ExpressoExpressionBuilder> action)
    {
        var expression = new ExpressoExpressionBuilder();
        action.Invoke(expression);
        expressoExpressions.Add(expression);

        return this;
    }

    }

public class ExpressoExpressionBuilder
{
    public readonly Queue<ExpressoExpression> ExpressoShots = 
        new Queue<ExpressoExpression>();

    public ExpressoExpressionBuilder ShotOfExpresso()
    {
        var shot = new ExpressoExpression();
        ExpressoShots.Enqueue(shot);

        return this;
    }

    public ExpressoExpressionBuilder AtTemperature(int temp)
    {
        var recentlyAddedShot = ExpressoShots.Peek();
        recentlyAddedShot.Temperature = temp;

        return this;
    }

    public ExpressoExpressionBuilder OfPremiumType()
    {
        var recentlyAddedShot = ExpressoShots.Peek();
        recentlyAddedShot.IsOfPremiumType = true;

        return this;
    }
}

public class ExpressoExpression
{
    public int Temperature { get; set; }
    public bool IsOfPremiumType { get; set; }

    public ExpressoExpression()
    {
        Temperature = 0;
        IsOfPremiumType = false;
    }
}

Any and all suggestions are welcome.

sam
  • 753
  • 1
  • 6
  • 15
2

What if .IncludeApps accepted an array of AppRegistrations

IncludeApps(params IAppRegistration[] apps)

then

public static class App
{
  public static IAppRegistration IncludeAppFor(AppType type)
  {
    return new AppRegistration(type);
  }
}

public class AppRegistration
{
  private AppType _type;
  private bool _cost;

  public AppRegistration(AppType type)
  {
    _type = type;
  }

  public AppRegistration AtNoCost()
  { 
    _cost = 0;
    return this;
  }
}

so eventually it would look like this...

.IncludeApps
(
  App.IncludeAppFor(AppType.Any), 
  App.IncludeAppFor(AppType.Any).AtNoCost()
)

Inside your IncludeApps method you would inspect the registrations and create the objects as required.

Rohan West
  • 9,262
  • 3
  • 37
  • 64
  • I definitely think you're going in the right direction, but I'd like to solve this problem using c#'s System.Action or System.Func objects in a closure. This is the concept that I'm trying to grasp my head around. Thanks. – sam Nov 25 '09 at 06:03
  • 2
    This is not a useful place to use closure. Lambda expressions are useful when you want to define an `action` which will occur according to some unknown parameter. Here, everything is known, and you aren't really defining an action. You just want to register an app on the phone. – tster Nov 25 '09 at 06:08
  • Good call on the params. I was thinking the same thing. You beat me to it. – David Silva Smith Nov 25 '09 at 06:11
  • you're definitely not dealing with closures. see this post for info on how closures behave in c# http://bit.ly/81BfpW – Derick Bailey Nov 25 '09 at 12:53
1

To go the delegate route maybe something like this would work?

var aPhone = MyPhone.Create;
  MyPhone.Create.IncludeApps
  (
    x =>
      {
        x.IncludeAppFor(new object());
      }
  );

class MyPhone
  {
    public MyPhone IncludeApps(Action<MyPhone> includeCommand)
    {
        includeCommand.Invoke(this);
        return this;
    }
  }

If you aren't set on the delegate route maybe params would work?

var anotherPhone = MyPhone.Create.IncludeApps(
    new IncludeAppClass(AppType.Math),
    new IncludeAppClass(AppType.Entertainment).AtNoCost());


class MyPhone
{
    internal MyPhone IncludeApps(params IncludeAppClass[] includeThese)
    {
        if (includeThese == null)
        {
            return this;
        }
        foreach (var item in includeThese)
        {
            this.Apps.Add(Item);
        }
        return this;
    }
}
David Silva Smith
  • 11,498
  • 11
  • 67
  • 91