Instantly share code, notes, and snippets.
Last active
March 22, 2025 10:44
-
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/926637ab6b11d56e4791fce9205bf862 to your computer and use it in GitHub Desktop.
Port of example from Re-Engineering Legacy Software book
This file contains hidden or 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. | |
export abstract class Rule { | |
private readonly nextRule: Rule | null; | |
protected constructor(nextRule: Rule | null = null) { | |
this.nextRule = nextRule; | |
} | |
// Does this rule apply to the given player and page? | |
protected abstract canApply(player: Player, page: Page): boolean; | |
// Apply the rule to choose a banner to show. | |
// Returns a banner, which may be null | |
protected abstract apply(player: Player, page: Page): Banner | null; | |
public chooseBanner(player: Player, page: Page): Banner | null { | |
if (this.canApply(player, page)) { | |
// Apply this rule | |
return this.apply(player, page); | |
} else if (this.nextRule != null) { | |
// Try the next rule | |
return this.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. | |
export class ExcludeCertainPages extends Rule { | |
// Pages on which banners should not be shown | |
private static readonly pageIds: Set<string> = new Set(["profile"]); | |
public constructor(nextRule: Rule | null = null) { | |
super(nextRule); | |
} | |
protected canApply(player: Player, page: Page): boolean { | |
return ExcludeCertainPages.pageIds.has(page.id); | |
} | |
protected apply(player: Player, page: Page): Banner | null { | |
return null; | |
} | |
} | |
export class ABTest extends Rule { | |
private readonly dao: BannerDao; | |
public constructor(dao: BannerDao, nextRule: Rule | null = null) { | |
super(nextRule); | |
this.dao = dao; | |
} | |
protected canApply(player: Player, page: Page): boolean { | |
// Check if player is in A/B test segment | |
return player.id % 5 === 0; | |
} | |
protected apply(player: Player, page: Page): Banner | null { | |
// Show banner 123 to players in A/B test segment | |
return this.dao.findById(123); | |
} | |
} | |
// more rules... | |
////////////////////////////////////////////////////////////////////////////// | |
// 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. | |
export function buildChain(dao: BannerDao): Rule { | |
return new ABTest(dao, | |
new ExcludeCertainPages( | |
new ChooseRandomBanner(dao) | |
) | |
); | |
} | |
////////////////////////////////////////////////////////////////////////////// | |
// We rename the concrete class to LegacyBannerAdChooser and | |
// extract an interface called BannerAdChooser | |
export interface BannerAdChooser { | |
getAd(player: Player, page: Page): Banner | null; | |
} | |
export class LegacyBannerAdChooser implements BannerAdChooser { | |
//... | |
public getAd(player: Player, page: Page): Banner { | |
//... | |
} | |
} | |
////////////////////////////////////////////////////////////////////////////// | |
// 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. | |
export class BaseBannerAdChooser implements BannerAdChooser { | |
private readonly dao: BannerDao = new BannerDao(); | |
private readonly chain: Rule; | |
public constructor() { | |
this.chain = buildChain(this.dao); | |
} | |
public getAd(player: Player, page: Page): Banner | null { | |
return this.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 | |
export class CachingBannerAdChooser implements BannerAdChooser { | |
private readonly cache: BannerCache = new BannerCache(); | |
private readonly baseChooser: BannerAdChooser; | |
public constructor(baseChooser: BannerAdChooser) { | |
this.baseChooser = baseChooser; | |
} | |
public getAd(player: Player, page: Page): Banner | null { | |
const cachedBanner = this.cache.get(player, page); | |
if (cachedBanner) { | |
return cachedBanner; | |
} else { | |
// Delegate to the next layer | |
const banner = this.baseChooser.getAd(player, page); | |
// Store the result in the cache for 30 minutes | |
if (banner) { | |
this.cache.put(player, page, banner, 30 * 60); | |
} | |
return banner; | |
} | |
} | |
} | |
// The decorator for logging | |
export class LoggingBannerAdChooser implements BannerAdChooser { | |
private readonly baseChooser: BannerAdChooser; | |
public constructor(baseChooser: BannerAdChooser) { | |
this.baseChooser = baseChooser; | |
} | |
public getAd(player: Player, page: Page): Banner | null { | |
// Delegate to the next layer | |
const banner = this.baseChooser.getAd(player, page); | |
if (banner) { | |
// Record the impression of the chosen banner | |
this.logImpression(player, page, banner); | |
} | |
return banner; | |
} | |
private logImpression(player: Player, page: Page, banner: Banner): void { | |
// Logging logic here... | |
} | |
} | |
////////////////////////////////////////////////////////////////////////////// | |
// Finally, we need a factory to take care of wiring up all our objects in the correct order. | |
export class BannerAdChooserFactory { | |
public static create(): BannerAdChooser { | |
return new LoggingBannerAdChooser( // the decorator for logging | |
new CachingBannerAdChooser( // the proxy for caching | |
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