Instantly share code, notes, and snippets.
Created
December 16, 2024 21:25
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save trikitrok/dd13e2950207d102573ed2a5b4388a61 to your computer and use it in GitHub Desktop.
Port of example from Re-Engineering Legacy Software book
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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