Skip to content

Instantly share code, notes, and snippets.

@trikitrok
Created December 16, 2024 21:25
Show Gist options
  • Save trikitrok/dd13e2950207d102573ed2a5b4388a61 to your computer and use it in GitHub Desktop.
Save trikitrok/dd13e2950207d102573ed2a5b4388a61 to your computer and use it in GitHub Desktop.
Port of example from Re-Engineering Legacy Software book
// an abstract Rule class that each of our business rules will extend.
public abstract class Rule
{
private readonly Rule nextRule;
protected Rule(Rule nextRule)
{
this.nextRule = nextRule;
}
// Does this rule apply to the given player and page?
protected abstract bool CanApply(Player player, Page page);
// Apply the rule to choose a banner to show.
// Returns a banner, which may be null
protected abstract Banner Apply(Player player, Page page);
public Banner ChooseBanner(Player player, Page page)
{
if (CanApply(player, page))
{
// Apply this rule
return Apply(player, page);
}
else if (nextRule != null)
{
// Try the next rule
return nextRule.ChooseBanner(player, page);
}
else
{
// Ran out of rules to try!
return null;
}
}
}
//////////////////////////////////////////////////////////////////////////////
// There will be one concrete subclass of Rule for each of our business rules.
// These are a couple of examples.
public sealed class ExcludeCertainPages : Rule
{
// Pages on which banners should not be shown
private static readonly HashSet<string> PageIds = new HashSet<string>(new[] { "profile" });
public ExcludeCertainPages(Rule nextRule) : base(nextRule)
{
}
protected override bool CanApply(Player player, Page page)
{
return PageIds.Contains(page.Id);
}
protected override Banner Apply(Player player, Page page)
{
return null;
}
}
public sealed class ABTest : Rule
{
private readonly BannerDao dao;
public ABTest(BannerDao dao, Rule nextRule) : base(nextRule)
{
this.dao = dao;
}
protected override bool CanApply(Player player, Page page)
{
// Check if player is in A/B test segment
return player.Id % 5 == 0;
}
protected override Banner Apply(Player player, Page page)
{
// Show banner 123 to players in A/B test segment
return dao.FindById(123);
}
}
//////////////////////////////////////////////////////////////////////////////
// Once we have our Rule implementations, we can chain them together into a Chain of Responsibility.
// Only showing a few links of the chain, for brevity.
public Rule BuildChain(BannerDao dao)
{
return new ABTest(dao,
new ExcludeCertainPages(
new ChooseRandomBanner(dao)));
}
//////////////////////////////////////////////////////////////////////////////
// We rename the concrete class to LegacyBannerAdChooser and
// extract an interface called BannerAdChooser
public interface BannerAdChooser
{
Banner GetAd(Player player, Page page);
}
public sealed class LegacyBannerAdChooser : BannerAdChooser
{
public Banner GetAd(Player player, Page page)
{
// ...
}
}
//////////////////////////////////////////////////////////////////////////////
// Next we split the method into a base case and a couple of decorators.
// The base case is the main Chain of Responsibility-based implementation.
// Renamed LegacyBannerAdChooser to BaseBannerAdChooser.
public sealed class BaseBannerAdChooser : BannerAdChooser
{
private readonly BannerDao dao = new BannerDao();
private readonly Rule chain;
public BaseBannerAdChooser()
{
chain = CreateChain(dao);
}
public Banner GetAd(Player player, Page page)
{
return chain.ChooseBanner(player, page);
}
}
//////////////////////////////////////////////////////////////////////////////
// We also introduce a decorator and a proxy that transparently take care of logging and caching respectively.
// The proxy for caching
public sealed class CachingBannerAdChooser : BannerAdChooser
{
private readonly BannerCache cache = new BannerCache();
private readonly BannerAdChooser baseChooser;
public CachingBannerAdChooser(BannerAdChooser baseChooser)
{
this.baseChooser = baseChooser;
}
public Banner GetAd(Player player, Page page)
{
var cachedBanner = cache.Get(player, page);
if (cachedBanner != null)
{
return cachedBanner;
}
else
{
// Delegate to the next layer
var banner = baseChooser.GetAd(player, page);
// Store the result in the cache for 30 minutes
cache.Put(player, page, banner, 30 * 60);
return banner;
}
}
}
// The decorator for logging
public sealed class LoggingBannerAdChooser : BannerAdChooser
{
private readonly BannerAdChooser baseChooser;
public LoggingBannerAdChooser(BannerAdChooser baseChooser)
{
this.baseChooser = baseChooser;
}
public Banner GetAd(Player player, Page page)
{
// Delegate to the next layer
var banner = baseChooser.GetAd(player, page);
if (banner != null)
{
// Make a record of what banner we chose
LogImpression(player, page, banner);
}
return banner;
}
private void LogImpression(Player player, Page page, Banner banner)
{
// Log logic here...
}
}
//////////////////////////////////////////////////////////////////////////////
// Finally, we need a factory to take care of wiring up all our objects in the correct order.
public static class BannerAdChooserFactory
{
public static BannerAdChooser Create()
{
return new LoggingBannerAdChooser( // the decorator
new CachingBannerAdChooser( // the proxy
new BaseBannerAdChooser())); // the concrete class using the chain of responsibility inside.
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment