diff --git a/examples/01-SRP/srp-bad.ts b/examples/01-SRP/srp-bad.ts new file mode 100644 index 0000000..7865ee7 --- /dev/null +++ b/examples/01-SRP/srp-bad.ts @@ -0,0 +1,19 @@ +/* tslint:disable:max-classes-per-file */ + +// Bad SRP + +class UserSettings { + constructor(user) { + this.user = user; + } + + public changeSettings(settings) { + if (this.verifyCredentials()) { + // ... + } + } + + public verifyCredentials() { + // ... + } +} diff --git a/examples/01-SRP/srp-good.ts b/examples/01-SRP/srp-good.ts new file mode 100644 index 0000000..c4971c7 --- /dev/null +++ b/examples/01-SRP/srp-good.ts @@ -0,0 +1,26 @@ +/* tslint:disable:max-classes-per-file */ + +// Good SRP + +class UserAuth { + constructor(user) { + this.user = user; + } + + public verifyCredentials() { + // ... + } +} + +class UserSettings { + constructor(user) { + this.user = user; + this.auth = new UserAuth(user); + } + + public changeSettings(settings) { + if (this.auth.verifyCredentials()) { + // ... + } + } +} diff --git a/examples/02-OCP/ocp-bad.ts b/examples/02-OCP/ocp-bad.ts new file mode 100644 index 0000000..818a5e4 --- /dev/null +++ b/examples/02-OCP/ocp-bad.ts @@ -0,0 +1,43 @@ +/* tslint:disable:max-classes-per-file */ + +// Bad OCP + +class AjaxAdapter extends Adapter { + constructor() { + super(); + this.name = "ajaxAdapter"; + } +} + +class NodeAdapter extends Adapter { + constructor() { + super(); + this.name = "nodeAdapter"; + } +} + +class HttpRequester { + constructor(adapter) { + this.adapter = adapter; + } + + public fetch(url) { + if (this.adapter.name === "ajaxAdapter") { + return makeAjaxCall(url).then((response) => { + // transform response and return + }); + } else if (this.adapter.name === "httpNodeAdapter") { + return makeHttpCall(url).then((response) => { + // transform response and return + }); + } + } +} + +function makeAjaxCall(url) { + // request and return promise +} + +function makeHttpCall(url) { + // request and return promise +} diff --git a/examples/02-OCP/ocp-good.ts b/examples/02-OCP/ocp-good.ts new file mode 100644 index 0000000..7291b41 --- /dev/null +++ b/examples/02-OCP/ocp-good.ts @@ -0,0 +1,38 @@ +/* tslint:disable:max-classes-per-file */ + +// Good OCP + +class AjaxAdapter extends Adapter { + constructor() { + super(); + this.name = "ajaxAdapter"; + } + + public request(url) { + // request and return promise + } +} + +class NodeAdapter extends Adapter { + constructor() { + super(); + this.name = "nodeAdapter"; + } + + public request(url) { + // request and return promise + } +} + +class HttpRequester { + constructor(adapter) { + this.adapter = adapter; + } + + public fetch(url) { + return this.adapter.request(url) + .then((response) => { + // transform response and return + }); + } +} diff --git a/examples/03-LSP/lsp-bad.ts b/examples/03-LSP/lsp-bad.ts new file mode 100644 index 0000000..734de99 --- /dev/null +++ b/examples/03-LSP/lsp-bad.ts @@ -0,0 +1,54 @@ +/* tslint:disable:max-classes-per-file */ + +// Bad LSP + +class Rectangle { + constructor() { + this.width = 0; + this.height = 0; + } + + public setColor(color) { + // ... + } + + public render(area) { + // ... + } + + public setWidth(width) { + this.width = width; + } + + public setHeight(height) { + this.height = height; + } + + public getArea() { + return this.width * this.height; + } +} + +class Square extends Rectangle { + public setWidth(width) { + this.width = width; + this.height = width; + } + + public setHeight(height) { + this.width = height; + this.height = height; + } +} + +function renderLargeRectangles(rectangles) { + rectangles.forEach((rectangle) => { + rectangle.setWidth(4); + rectangle.setHeight(5); + const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20. + rectangle.render(area); + }); +} + +const rectangles = [new Rectangle(), new Rectangle(), new Square()]; +renderLargeRectangles(rectangles); diff --git a/examples/03-LSP/lsp-good.ts b/examples/03-LSP/lsp-good.ts new file mode 100644 index 0000000..bc93b40 --- /dev/null +++ b/examples/03-LSP/lsp-good.ts @@ -0,0 +1,46 @@ +/* tslint:disable:max-classes-per-file */ + +// Good LSP + +class Shape { + public setColor(color) { + // ... + } + + public render(area) { + // ... + } +} + +class Rectangle extends Shape { + constructor(width, height) { + super(); + this.width = width; + this.height = height; + } + + public getArea() { + return this.width * this.height; + } +} + +class Square extends Shape { + constructor(length) { + super(); + this.length = length; + } + + public getArea() { + return this.length * this.length; + } +} + +function renderLargeShapes(shapes) { + shapes.forEach((shape) => { + const area = shape.getArea(); + shape.render(area); + }); +} + +const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; +renderLargeShapes(shapes); diff --git a/examples/04-ISP/isp-bad.ts b/examples/04-ISP/isp-bad.ts new file mode 100644 index 0000000..5b896cd --- /dev/null +++ b/examples/04-ISP/isp-bad.ts @@ -0,0 +1,25 @@ +// Bad ISP + +class DOMTraverser { + constructor(settings) { + this.settings = settings; + this.setup(); + } + + public setup() { + this.rootNode = this.settings.rootNode; + this.animationModule.setup(); + } + + public traverse() { + // ... + } +} + +const $ = new DOMTraverser({ + rootNode: document.getElementsByTagName("body"), + animationModule() { + // Most of the time, we won't need to animate when traversing. + }, + // ... +}); diff --git a/examples/04-ISP/isp-good.ts b/examples/04-ISP/isp-good.ts new file mode 100644 index 0000000..ae93578 --- /dev/null +++ b/examples/04-ISP/isp-good.ts @@ -0,0 +1,33 @@ +// Good ISP + +class DOMTraverser { + constructor(settings) { + this.settings = settings; + this.options = settings.options; + this.setup(); + } + + public setup() { + this.rootNode = this.settings.rootNode; + this.setupOptions(); + } + + public setupOptions() { + if (this.options.animationModule) { + // ... + } + } + + public traverse() { + // ... + } +} + +const $ = new DOMTraverser({ + options: { + animationModule() { + // ... + }, + }, + rootNode: document.getElementsByTagName("body"), +}); diff --git a/examples/05-DIP/dip-bad.ts b/examples/05-DIP/dip-bad.ts new file mode 100644 index 0000000..04cbf5b --- /dev/null +++ b/examples/05-DIP/dip-bad.ts @@ -0,0 +1,32 @@ +/* tslint:disable:max-classes-per-file */ + +// Bad DIP + +class InventoryRequester { + constructor() { + this.REQ_METHODS = ["HTTP"]; + } + + public requestItem(item) { + // ... + } +} + +class InventoryTracker { + constructor(items) { + this.items = items; + + // BAD: We have created a dependency on a specific request implementation. + // We should just have requestItems depend on a request method: `request` + this.requester = new InventoryRequester(); + } + + public requestItems() { + this.items.forEach((item) => { + this.requester.requestItem(item); + }); + } +} + +const inventoryTracker = new InventoryTracker(["apples", "bananas"]); +inventoryTracker.requestItems(); diff --git a/examples/05-DIP/dip-good.ts b/examples/05-DIP/dip-good.ts new file mode 100644 index 0000000..0791f52 --- /dev/null +++ b/examples/05-DIP/dip-good.ts @@ -0,0 +1,41 @@ +/* tslint:disable:max-classes-per-file */ + +// Good DIP + +class InventoryTracker { + constructor(items, requester) { + this.items = items; + this.requester = requester; + } + + public requestItems() { + this.items.forEach((item) => { + this.requester.requestItem(item); + }); + } +} + +class InventoryRequesterV1 { + constructor() { + this.REQ_METHODS = ["HTTP"]; + } + + public requestItem(item) { + // ... + } +} + +class InventoryRequesterV2 { + constructor() { + this.REQ_METHODS = ["WS"]; + } + + public requestItem(item) { + // ... + } +} + +// By constructing our dependencies externally and injecting them, we can easily +// substitute our request module for a fancy new one that uses WebSockets. +const inventoryTracker = new InventoryTracker(["apples", "bananas"], new InventoryRequesterV2()); +inventoryTracker.requestItems();