Skip to content

Instantly share code, notes, and snippets.

@trikitrok
Last active March 22, 2025 10:44
Show Gist options
  • Save trikitrok/926637ab6b11d56e4791fce9205bf862 to your computer and use it in GitHub Desktop.
Save trikitrok/926637ab6b11d56e4791fce9205bf862 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.
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