4
\$\begingroup\$

I work mainly at developing MSSQL databases but I've been trying to teach myself C# for a bit now and I wrote some simple code that prints a list of colleagues and their coffee preference.

I just went through it with my precarious knowledge and I can't find much more to add or remove from it, I thought maybe I can post it here and learn from it.

It really is a very innocent bit of code but I think that's exactly where fundamental mistakes are displayed the best.

using System;
using System.Collections.Generic;
namespace CoffeeDrinkers
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] coffeeDrinkers = new[] {"Anthony", "Chris", "Christian"};
            foreach (var person in coffeeDrinkers)
            {
                Console.WriteLine(person + ": " + MakeCoffee(person));
            }
            Console.ReadLine();
        }
        private static string NewCoffee(bool milk, int milkAmount, bool sugar, int sugarSpoons)
        {
            var coffee = new Coffee();
            coffee.Milk = milk;
            if (coffee.Milk) { coffee.MilkAmount = milkAmount; }
            coffee.Sugar = sugar;
            if (coffee.Sugar) { coffee.SugarSpoons = sugarSpoons; }
            return PrepareCoffee(coffee);
        }
        private static string PrepareCoffee(Coffee coffee)
        {
            string a = "", b = "", c = "";

            if (coffee.Milk)
            {
                a = "White";
                if (coffee.MilkAmount <= 20) { b = " but not too white"; }
            }
            else
            {
                a = "Black";
            }
            if (coffee.Sugar)
            {
                if (coffee.SugarSpoons > 1)
                {
                    c = ", " + coffee.SugarSpoons + " sugars.";
                }
                else
                {
                    c = ", one sugar.";
                }
            }
            else
            {
                c = " ,no sugar.";
            }
            return a + b + c;
        }
        private static string MakeCoffee(string drinker)
        {
            switch (drinker)
            {
                case "Anthony": return NewCoffee(true, 25, true, 2);
                case "Chris": return NewCoffee(true, 25, false, 0);
                case "Christian": return NewCoffee(true, 25, true, 2);
                default: return "";
            }
        }
        private class Coffee
        {
            public bool Milk { get; set; } 
            public int MilkAmount { get; set; } 
            public bool Sugar { get; set; } 
            public decimal SugarSpoons { get; set; }
        }
    }
}

First review:

using System;

namespace CoffeeDrinkers
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] coffeeDrinkers = new[] {"Anthony", "Chris", "Christian"};
            foreach (var person in coffeeDrinkers)
            {
                Console.WriteLine(person + ": " + NewCoffee(person));
            }
            Console.ReadLine();
        }

        private static string NewCoffee(string person)
        {
            switch (person)
            {
                case "Anthony": return PrepareCoffee(25, 2);
                case "Chris": return PrepareCoffee(25, 0);
                case "Christian": return PrepareCoffee(25, 2);
                default: return "";
            }
        }

        private static string PrepareCoffee(int milk, int sugar)
        {
            string a = "", b = "", c = "";

            if (milk > 0)
            {
                a = "White";
                if (milk <= 20)
                {
                    b = " but not too white";
                }
            }
            else
            {
                a = "Black";
            }
            if (sugar > 0)
            {
                if (sugar > 1)
                {
                    c = ", " + sugar + " sugars.";
                }
                else
                {
                    c = ", one sugar.";
                }
            }
            else
            {
                c = " ,no sugar.";
            }
            return a + b + c;
        }
    }
}
\$\endgroup\$
5
  • 1
    \$\begingroup\$ first look: All the conditions in NewCoffee don't seems right. I mean, why do you need a bool mik, isn't milkamount = 0, same as that? Same with bool sugar \$\endgroup\$
    – P.K
    Commented Nov 2, 2016 at 17:59
  • \$\begingroup\$ where are you using the class CoffeeDrinkers ? \$\endgroup\$
    – P.K
    Commented Nov 2, 2016 at 18:03
  • \$\begingroup\$ Why static NewCoffee? Why does MakeCoffee return string? Just create a New Coffee. Prepare coffee should be a method of Coffee. \$\endgroup\$
    – paparazzo
    Commented Nov 2, 2016 at 18:07
  • \$\begingroup\$ @P.K Class CoffeeDrinkers is not used, I forgot to remove that portion. \$\endgroup\$
    – Nelson
    Commented Nov 2, 2016 at 18:41
  • \$\begingroup\$ @Paparazzi just added another version \$\endgroup\$
    – Nelson
    Commented Nov 2, 2016 at 19:05

3 Answers 3

8
\$\begingroup\$

Issues

Your current model violates two principles:

  • OCP: you cannot easily add new coffee types without touching multiple functions.
  • SRP: the PrepareCoffee method not only creates a coffee but also contains logic for multiple recipies.

Let's fix that...


Fix

We start by creating a few abstractions that will allow us to create more concrete objects.

The first thing we need is an Ingredient:

abstract class Ingredient
{
    protected Ingredient(string name, int amount)
    {
        Name = name;
        Amount = Amount;
    }
    public string Name { get; }
    public int Amount { get; }
}

we now use this to derive concrete ingredients from it:

class Milk : Ingredient
{
    public Milk(int amount) : base("Milk", amount) { }
}

class Sugar : Ingredient
{
    public Sugar(int amount) : base("Sugar", amount) { }
}

we also use this to create another abstractions which is a Coffee:

abstract class Coffee : Ingredient
{
    public Coffee(string name, int amount) : base(name, amount) { }
}

and of course some concrete coffee types:

class Espresso : Coffee
{
    public Espresso(int amount) : base("Espresso", amount) { }
}

class Americano : Coffee
{
    public Americano(int amount) : base("Americano", amount) { }
}

But we need more. Ingredients need to be combined in to something even more concrete - a recipe.

class Recipe : IEnumerable<Ingredient>
{
    private readonly IEnumerable<Ingredient> _ingredients;
    public Recipe(int id, string name, params Ingredient[] ingredients)
    {
        Id = id;
        Name = name;
        _ingredients = ingredients;
    }
    public string Name { get; }
    public int Id { get; }
    public IEnumerator<Ingredient> GetEnumerator() => _ingredients.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

A recipe can be for various things, we are interested only in drinks, so let's create it:

private class Drink
{
    public Drink(int id, string name, Recipe recipe)
    {
        Id = id;
        Name = name;
        Recipe = recipe;
    }
    public string Name { get; }
    public int Id { get; }
    public Recipe Recipe { get; set; }
}

This will allow us to create a menu:

var drinks = new Drink[]
{
    new Drink(1, "Espresso1", new Recipe(new Espresso(3), new Milk(2), new Sugar(1))),
    new Drink(2, "Black with Sugar", new Recipe(new Americano(3), new Sugar(2))),
}

Lastly we need customers...

class Customer
{
    public Customer(string name, Drink drink)
    {
        Name = name;
        Drink = drink;

    public string Name { get; }
    public Drink Drink { get; }
}

If a customer wants to buy something we can create a new one and assign a drink to him:

var customers = new Customer[]
{
    new Customer("Anthony", drinks.Single(x => x.Id == 1)),
    new Customer("Chris", drinks.Single(x => x.Id == 2)),
}

You have a textual representation of the recipes. You can achieve this by for example creating a new class specialized only in this:

class RecipeRenderer
{
     public string Render(Recipe recipe)
     {
         // create the string...
     }
}

With all these modules you can now add ingredients, drinks etc. without modifying anything. You just create new types and add a new item to the menu.

So how SOLID is this now?

  • SRP: checked - each module does only one thing
  • OCP: checked - can be extended without modifying any components
  • LSP: checked - Derived types must be completely substitutable for their base types. - Coffee, Milk, Espresso all are Ingredient and can be used as such.
  • ISP: does not apply
  • DIP: checked - High Level Classes Recipe --> Abstraction Layer Ingredient --> Low Level Classes Milk, Sugar

You can of course add meals or flatware and create even more complex menus and services.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Excellent answer. Only things I would change would be to have a factory create the drinks, and change the way drinks are selected by customers from "id" to an enum which shows more obviously what drink they're selecting. \$\endgroup\$
    – 404
    Commented Nov 3, 2016 at 11:23
  • \$\begingroup\$ Great answer, clarifies a lot of key concepts I was unaware of as a beginner... Exactly what I was looking for. \$\endgroup\$
    – Nelson
    Commented Nov 3, 2016 at 13:33
  • 1
    \$\begingroup\$ Look! No interfaces of the keyword kind. Yes, dear reader, this adheres to code to interfaces, not implementation \$\endgroup\$
    – radarbob
    Commented Nov 3, 2016 at 18:05
5
\$\begingroup\$

I would probably create a separate objects for this project, such CoffeeDrinkers and Coffee and leave the responsibility for creating of coffee on Coffee (or CoffeeMachine) object. You can eliminate the "switch" based on person. The repetition of predefined persons is bad. If you would like to add more persons in future, you will have to do it on two places of your code.

Program.cs

class Program
{
    static void Main(string[] args)
    {
        CoffeeDrinker[] coffeeDrinkers = new[]
        { new CoffeeDrinker("Anthony", new Coffee(25, 2)),
            new CoffeeDrinker("Chris", new Coffee(10, 0)),
            new CoffeeDrinker("Christian", new Coffee(25, 2)) };

        foreach (var person in coffeeDrinkers)
        {
            Console.WriteLine(person.Name + ": " + person.Coffee.GetCoffee());
        }
        Console.ReadLine();
    }
}

CoffeeDrinker.cs

CoffeeDrinker has name and favourite coffee.

public class CoffeeDrinker
{
    public CoffeeDrinker(string name, Coffee coffee)
    {
        Name = name;
        Coffee = coffee;
    }
    public string Name { get; set; }
    public Coffee Coffee { get; set; }
}

Coffee.cs

You can replace replace string variables a, b, c by one StringBuilder if there is only strings concatenation.

public class Coffee
{
    public Coffee(int milkCount, int sugarCount)
    {
        MilkCount = milkCount;
        SugarCount = sugarCount;
    }
    public int MilkCount { get; set; }
    public int SugarCount { get; set; }

    public string GetCoffee()
    {
        StringBuilder sb = new StringBuilder();

        if (this.MilkCount > 0)
        {
            sb.Append("White");
            if (this.MilkCount <= 20)
            {
                sb.Append(" but not too white");
            }
        }
        else
        {
            sb.Append("Black");
        }
        if (this.SugarCount > 0)
        {
            if (this.SugarCount > 1)
            {
                sb.Append(", " + this.SugarCount + " sugars.");
            }
            else
            {
                sb.Append(", one sugar.");
            }
        }
        else
        {
            sb.Append(", no sugar.");
        }
        return sb.ToString();
    }
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ One suggestion, if you make the parameters of the coffee constructor, optional, with default values of 0, you have much more flexibility by allowing a constructor with no parameters. \$\endgroup\$
    – user33306
    Commented Nov 2, 2016 at 22:11
  • \$\begingroup\$ This is a good start but there is more to be optimized. \$\endgroup\$
    – t3chb0t
    Commented Nov 3, 2016 at 8:25
5
\$\begingroup\$

Tweaking @Tomaa Paul Answer

This is an exercise in compare and contrast.

Think about "encapsulation", "least knowledge principle", "single responsibility principle"

Think about how client code cannot screw things up because it does not have access to all those properties.

Think about implications now that the client - Main - does not have to know intimate details of two different classes.

And how Coffee and CoffeeMaker are more portable - re-usable - because they know for themselves what and how to output.

Think about how you instantly know what MakeCoffee does. Why do you know what CustomMilk does without reading all the code? Why can we guess that is the only thing it does? Think about why OO principles should be applied into the depths of a class.

And finally, how .net framework is smart enough to use the ToString override.

class Program
{
   static void Main(string[] args)
   {
       CoffeeDrinker[] coffeeDrinkers = new[]
       { new CoffeeDrinker("Anthony", new Coffee(25, 2)),
         new CoffeeDrinker("Chris", new Coffee(10, 0)),
         new CoffeeDrinker("Christian", new Coffee(25, 2)) };

        foreach (var person in coffeeDrinkers)
        {
           Console.WriteLine(person);
        }
        Console.ReadLine();
    }
}

public class CoffeeDrinker
{
    public CoffeeDrinker(string name, Coffee coffee) {
      Name = name;
      Coffee = coffee;
    }

    protected string Name     { get; set; }
    protected Coffee Favorite { get; set; }

    public override string ToString() {
       return string.Format("{0} likes {1}", Name, Favorite);
    } 
}


public class Coffee
{
   public Coffee(int milkCount, int sugarCount)
   {
       Milk = milkCount;
       Sugar = sugarCount;
       MakeCoffee();
    }

    ///<Summary>
    /// Teaspoons
    ///</Summary>
    protected int Milk { get; set; }

    ///<Summary>
    /// lumps
    ///</Summary>
    protected int Sugar { get; set; }
    protected string Customize = string.Empty;

    protected void MakeCoffee()
    {
       Customize = string.Format("{0} {1}", CustomMilk(), CustomSugar());
    }

    protected string CustomMilk() {
       StringBuilder creamy = new StringBuilder();

       if (this.Milk > 0)  {
           creamy.Append("White");

           if (this.Milk <= 20)  {
              creamy.Append(" but not too white");
           }
        }
        else {
           creamy.Append("Black");
         }

        return creamy.ToString();
     }

    protected string CustomSugar() {
        StringBuilder sweetness = new StringBuilder();

        if (this.SugarCount > 0) {
           if (this.Sugar > 1) {
              sweetness.Append(", " + this.Sugar + " sugars.");
           }
          else {
            sweetness.Append(", one sugar.");
           }
         }
         else {
           sweetness.Append(", no sugar.");
         }

         return sweetness.ToString();
    }

    public override string ToString(){
       return string.Format("Sugar:{0}, Milk:{1}, {2}", Sugar, Milk, Customize);
    }
}

P.S. I know you're gonna say, "Hay Bob. You shouldn't use StringBuilder now. Well, it made refactoring easier and quicker.

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Why shouldn't use use the StringBuilder? It's exactly what should be used when you create strings. \$\endgroup\$
    – t3chb0t
    Commented Nov 3, 2016 at 8:23
  • 1
    \$\begingroup\$ For someone using the Coffee class, the parameters are milkCount and sugarCount which doesn't mean anything to them. The comments of what the amounts actually are are hidden away in the protected properties inside the class. Name the parameters teaspoonsOfMilk and lumpsOfSugar instead. \$\endgroup\$
    – 404
    Commented Nov 3, 2016 at 11:57
  • \$\begingroup\$ Having refactored each (local) SB has fewer concats,. to do. I was expecting someone to mention the conventional wisdom that simple string concatenation would be more better in this case. \$\endgroup\$
    – radarbob
    Commented Nov 3, 2016 at 17:24
  • \$\begingroup\$ I commented these non-public properties so that "Milk", "Sugar" will otherwise suffice as names. When I comment class members I use XML comments (period)- and sometimes in-line. I have this delusion that someday our shop will generate code documentation as part of our Continuous Integration process. \$\endgroup\$
    – radarbob
    Commented Nov 3, 2016 at 17:35
  • \$\begingroup\$ ... but yeah, milkCount should be something like milkAmount \$\endgroup\$
    – radarbob
    Commented Nov 3, 2016 at 17:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.