Let's say you need to implement a feature that returns a different package based on the user-provided coupon code.
So you start with a model:
public record Package
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
}
And you write a function that returns a different package based on the coupon code:
private static Package GetPackageFromCoupon(string coupon)
{
if (coupon == "ABC")
{
return new Package { Id = 1, Name = "PS5 Controller", Price = 50.00 };
}
if (coupon == "EBC")
{
return new Package { Id = 2, Name = "Iphone X", Price = 200.00 };
}
if (coupon == "DDD")
{
return new Package { Id = 3, Name = "X7 Mouse", Price = 20.00 };
}
return new Package
{
Id = 1000,
Name = "Soda",
Price = 1.00
};
}
And invoke it from your main method:
internal class Program
{
static void Main(string[] args)
{
var package = GetPackageFromCoupon("ABC");
Console.WriteLine(package);
Console.ReadLine();
}
}
Quick test
Provide expected parameters and inspect the results.
"ABC" => Package { Id = 1, Name = PS5 Controller, Price = 50 }
"EBC" => Package { Id = 2, Name = Iphone X, Price = 200 }
"DDD" => Package { Id = 3, Name = X7 Mouse, Price = 20 }
Works as expected. Also, if you enter something that doesn't exist:
"a" => Package { Id = 1000, Name = Soda, Price = 1 }
The Problem
What if you need to add more coupon codes and return different variations of the Package object? Well, it's gonna get pretty messy very soon.
Quick solution - Dictionary
Rather than writing every possible variation in the if block, create a dictionary where the key is the coupon code and the value is the Package:
private static readonly Dictionary<string, Package> _packages = new()
{
["ABC"] = new Package { Id = 1, Name = "PS5 Controller", Price = 50.00 },
["EBC"] = new Package { Id = 2, Name = "Iphone X", Price = 200.00 },
["DDD"] = new Package { Id = 3, Name = "X7 Mouse", Price = 20.00 },
};
The next step is to enter the coupon code and either read the value from the dictionary or return a default value.
private static Package GetPackageFromCoupon(string coupon)
{
var isExistingPackage = _packages.TryGetValue(coupon, out var package);
if (isExistingPackage)
{
return package;
}
return new Package
{
Id = 1000,
Name = "Soda",
Price = 1.00
};
}
Run the program, and you'll get back the same result!
But why a dictionary? Why not an array or a list? The answer is Big O notation. Reading an item by key is immensely faster than querying a collection. Dictionary lookup is O(1), while list search is O(n).
Let's spice things up a bit.
What if the output varies?
Change request comes in. You need to introduce seasons. If it's the holiday season, the price should be lower.
You need to create a function that takes the season as a parameter and adjusts the price accordingly. To keep it simple, add static methods in the Package class:
public record Package
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public static Package PS5Controller(bool isHolidaySeason)
{
return new Package {
Id = 1,
Name = "PS5 Controller",
Price = isHolidaySeason ? 20.00 : 50.00
};
}
public static Package IphoneX(bool isHolidaySeason)
{
return new Package
{
Id = 2,
Name = "Iphone X",
Price = isHolidaySeason ? 50.00 : 200.00
};
}
public static Package X7Mouse(bool isHolidaySeason)
{
return new Package
{
Id = 3,
Name = "X7 Mouse",
Price = isHolidaySeason ? 10.00 : 20.00
};
}
public static Package DefaultPackage(bool isHolidaySeason)
{
return new Package
{
Id = 1000,
Name = "Soda",
Price = isHolidaySeason ? 0.00 : 1.00
};
}
}
But how do you call this function the dictionary? How do you pass that parameter?
private static readonly Dictionary<string, Package> _packages = new()
{
["ABC"] = Package.PS5Controller(), // ?
};
The Func Delegate
The Func<T, TResult> is a built-in generic delegate that encapsulates a method (that takes between 0 and 16 input parameters) and returns a value.
So instead of Dictionary<string, Package>, create a
Dictionary<string, Func<bool, Package>> where the bool is the input parameter (holiday season) and the Package is the output (method that returns a particular package).
private static readonly Dictionary<string, Func<bool, Package>> _packages = new()
{
["ABC"] = (isHoliday) => Package.PS5Controller(isHoliday),
["EBC"] = (isHoliday) => Package.IphoneX(isHoliday),
["DDD"] = (isHoliday) => Package.X7Mouse(isHoliday),
};
This can be further simplified:
private static readonly Dictionary<string, Func<bool, Package>> _packages = new()
{
["ABC"] = Package.PS5Controller,
["EBC"] = Package.IphoneX,
["DDD"] = Package.X7Mouse,
};
This works because C# allows method groups to be assigned to delegates when the signatures match.
The last piece of the puzzle is to adapt the GetPackageFromCoupon() method to also accept the holiday season:
private static Package GetPackageFromCoupon(string coupon, bool isHoliday)
{
var isExistingPackage = _packages.TryGetValue(coupon, out var packageFactory);
if (isExistingPackage)
{
return packageFactory(isHoliday);
}
return Package.DefaultPackage(isHoliday);
}
Clarifications
This time around, the Dictionary returns a function (using a coupon code):
var isExistingPackage = _packages.TryGetValue(coupon, out var packageFactory);
The function is invoked by passing the isHoliday parameter:
return packageFactory(isHoliday);
The packageFactory() is a pointer to the function returned from the Dictionary. It can be any of the three specified functions.
Lastly, if there is no match, return a default value:
return Package.DefaultPackage(isHoliday);
Final test
Invoke the function with different variants:
static void Main(string[] args)
{
var isHolidaySeason = true;
var package = GetPackageFromCoupon("ABC", isHolidaySeason);
Console.WriteLine(package);
Console.ReadLine();
}
- During the holiday season:
Package { Id = 1, Name = PS5 Controller, Price = 20 }
- When not:
Package { Id = 1, Name = PS5 Controller, Price = 50 }
This also works for the default scenario:
var isHolidaySeason = true;
var package = GetPackageFromCoupon("aaaa", isHolidaySeason);
// Package { Id = 1000, Name = Soda, Price = 0 }
Full Example (Program.cs)
using CSharpPractice.Coupons;
namespace CSharpPractice
{
internal class Program
{
static void Main(string[] args)
{
var isHolidaySeason = true;
var package = GetPackageFromCoupon("ABC", isHolidaySeason);
Console.WriteLine(package);
Console.ReadLine();
}
private static Package GetPackageFromCoupon(string coupon, bool isHoliday)
{
var isExistingPackage = _packages.TryGetValue(coupon, out var packageFactory);
if (isExistingPackage)
{
return packageFactory(isHoliday);
}
return Package.DefaultPackage(isHoliday);
}
private static readonly Dictionary<string, Func<bool, Package>> _packages = new()
{
["ABC"] = Package.PS5Controller,
["EBC"] = Package.IphoneX,
["DDD"] = Package.X7Mouse,
};
}
}
Summary
This approach also aligns with the Open–Closed Principle: the code is open for extension (add new coupon codes or pricing rules) but closed for modification (no need to touch existing logic).
Adding a new coupon doesn't require rewriting method conditions. Just one dictionary entry.
Until next time 👋
Top comments (0)