├── .gitignore ├── README.md ├── conclusion ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── FileStore.ts │ ├── IFileLocator.ts │ ├── IStoreReader.ts │ ├── IStoreWriter.ts │ ├── MessageStore.ts │ ├── SqlStore.ts │ ├── StoreCache.ts │ ├── StoreLogger.ts │ └── TestExamples.ts ├── tests │ ├── src │ │ ├── FileStore.test.ts │ │ ├── MessageStore.test.ts │ │ ├── StoreCache.test.ts │ │ ├── StoreLogger.test.ts │ │ └── mockstore.ts │ └── tsconfig.json └── tsconfig.json ├── dip ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── FileStore.ts │ ├── IFileLocator.ts │ ├── IStoreReader.ts │ ├── IStoreWriter.ts │ ├── MessageStore.ts │ ├── SqlStore.ts │ ├── StoreCache.ts │ ├── StoreLogger.ts │ └── TestExamples.ts └── tsconfig.json ├── isp ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── FileStore.ts │ ├── IFileLocator.ts │ ├── IStore.ts │ ├── IStoreCache.ts │ ├── IStoreLogger.ts │ ├── IStoreWriter.ts │ ├── LogSavedStoreWriter.ts │ ├── LogSavingStoreWriter.ts │ ├── MessageStore.ts │ ├── SqlStore.ts │ ├── StoreCache.ts │ ├── StoreLogger.ts │ └── TestExamples.ts └── tsconfig.json ├── lsp ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── FileStore.ts │ ├── IStore.ts │ ├── MessageStore.ts │ ├── SqlStore.ts │ ├── StoreCache.ts │ ├── StoreLogger.ts │ └── TestExamples.ts └── tsconfig.json ├── ocp ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── CustomMessageStore.ts │ ├── FileStore.ts │ ├── MessageStore.ts │ ├── StoreCache.ts │ ├── StoreLogger.ts │ ├── StoreLoggerSplunk.ts │ └── TestExamples.ts └── tsconfig.json ├── package-lock.json ├── srp ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── FileStore.ts │ ├── MessageStore.ts │ ├── StoreCache.ts │ ├── StoreLogger.ts │ └── TestExamples.ts └── tsconfig.json └── start ├── package-lock.json ├── package.json ├── src └── FileStore.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | */dist 3 | */src/testfiles 4 | 5 | TODO.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOLID Principles Examples using Typescript 2 | 3 | This tutorial was insprired from the [Encapsulation SOLID](https://app.pluralsight.com/library/courses/encapsulation-solid/table-of-contents) course on Pluralsight. 4 | 5 | ## What is SOLID 6 | 7 | SOLID is an acronym for 5 important design principles when doing OOP (Object Oriented Programming). These 5 principles were introduced by Robert C. Martin (Uncle Bob), in his 2000 paper Design Principles and Design Patterns. 8 | 9 | These principles, when combined together, make it easy for a programmer to develop software that are easy to maintain and extend. They also make it easy for developers to avoid code smells, easily refactor code. 10 | 11 | * **S** - Single Responsibility Principle ([SRP](./srp)) 12 | * **O** - Open Closed Principle ([OCP](./ocp)) 13 | * **L** - Liskov Substitution Principle ([LSP](./lsp)) 14 | * **I** - Interface Segregation Principle ([ISP](./isp)) 15 | * **D** - Dependency Inversion Principle ([DIP](./dip)) 16 | 17 | ## How to use this tutorial 18 | 19 | Start with the original file here the [start](./start) folder and we then take that and apply refectorings step by step in the order of SOLID. 20 | 21 | So prehaps take a look at the original file as a reference. As you can see all the functionality is included in this file and none of the SOLID priniples have yet to be applied. Then move to each priniple one at a time folowing the same order of SOLID. 22 | 23 | If you want to jump directly to the final solution, that is once all the SOLID priniples have been applied to this original file, then take a look at the code in the [conclusion](./conclusion)) folder. In that code **all** the SOLID principles have been applied. If you want a sort of journey on how we got there, I suggest following each step at a time. 24 | 25 | So, what are you waiting for? Navigate to the separate folder linked above in this repo for further explainations and an example application written in [Typescript](https://www.typescriptlang.org/). 26 | 27 | ### Issue Reporting 28 | 29 | If you experience with bugs or need further improvement, please create a new issue under [Issues](https://github.com/devbootstrap/SOLID-Principles-Examples-using-Typescript/issues). 30 | 31 | ### Contributing to this SOLID journey! 32 | 33 | Pull requests are very welcome. Before submitting a pull request, please make sure that your changes are well tested. 34 | 35 | ### License 36 | 37 | This application is released under [AGPL](http://www.gnu.org/licenses/agpl-3.0-standalone.html) 38 | 39 | ### Disclaimer 40 | 41 | This application is part of a _research and learning project_ and is most definitely __not__ suitable for Production use! :) -------------------------------------------------------------------------------- /conclusion/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devbootstrap/SOLID-Principles-Examples-using-Typescript/a46022f7905c8364bbaebbe5cbe3e27a23cf6883/conclusion/README.md -------------------------------------------------------------------------------- /conclusion/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /conclusion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-conclusion-example", 3 | "version": "1.0.0", 4 | "description": "The concluding example application to demo the SOLID Priniples", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "testwatch": "jest --watch", 9 | "build": "tsc" 10 | }, 11 | "keywords": [], 12 | "author": "Darren Jensen", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/jest": "^25.2.1", 16 | "@types/node": "^13.13.4", 17 | "jest": "^25.4.0", 18 | "ts-jest": "^25.4.0", 19 | "ts-node": "^8.9.0", 20 | "typescript": "^3.8.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /conclusion/src/FileStore.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import IFileLocator from './IFileLocator'; 4 | import IStoreWriter from './IStoreWriter'; 5 | import IStoreReader from './IStoreReader'; 6 | 7 | /** 8 | * A class that allows for messages to be stored in 9 | * a local file system 10 | */ 11 | export default class FileStore implements IStoreReader, IStoreWriter, IFileLocator { 12 | directory: string 13 | 14 | constructor(_directory: string) { 15 | this.directory = _directory; 16 | } 17 | 18 | public save(id: number, message: string): void { 19 | var fileFullName = this.getFileInfo(id); 20 | fs.writeFileSync(fileFullName, message) 21 | } 22 | 23 | public read(id: number): string { 24 | var fileFullName = this.getFileInfo(id); 25 | var exists = fs.existsSync(fileFullName); 26 | if(!exists) { 27 | return undefined 28 | } 29 | return fs.readFileSync(fileFullName, {encoding: 'ASCII'}); 30 | } 31 | 32 | public getFileInfo(id: number): string { 33 | return path.join(__dirname, this.directory, `${id}.txt`) 34 | } 35 | } -------------------------------------------------------------------------------- /conclusion/src/IFileLocator.ts: -------------------------------------------------------------------------------- 1 | export default interface IFileLocator { 2 | getFileInfo(id: number): string 3 | } -------------------------------------------------------------------------------- /conclusion/src/IStoreReader.ts: -------------------------------------------------------------------------------- 1 | export default interface IStoreReader { 2 | read(id: number): string 3 | } -------------------------------------------------------------------------------- /conclusion/src/IStoreWriter.ts: -------------------------------------------------------------------------------- 1 | export default interface IStoreWriter { 2 | save(id: number, message: string): void 3 | } -------------------------------------------------------------------------------- /conclusion/src/MessageStore.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from "./IStoreWriter"; 2 | import IStoreReader from "./IStoreReader"; 3 | 4 | export default class MessageStore { 5 | writer: IStoreWriter; 6 | reader: IStoreReader; 7 | 8 | constructor(writer: IStoreWriter,reader: IStoreReader) { 9 | if(writer === null) { 10 | throw new Error("writer argument cannot be null") 11 | } 12 | if(reader === null) { 13 | throw new Error("reader argument cannot be null") 14 | } 15 | this.writer = writer; 16 | this.reader = reader; 17 | } 18 | 19 | /** 20 | * 21 | * @param id the id of the message to save 22 | * @param message the text message to write to storage 23 | * 24 | */ 25 | public save (id: number, message: string) { 26 | this.writer.save(id, message); 27 | } 28 | 29 | /** 30 | * 31 | * @param id the id of the message to read 32 | * @returns message string 33 | * 34 | */ 35 | public read(id: number): string { 36 | return this.reader.read(id); 37 | } 38 | } -------------------------------------------------------------------------------- /conclusion/src/SqlStore.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from './IStoreWriter' 2 | import IStoreReader from './IStoreReader' 3 | /** 4 | * A class that allows for messages to be stored in 5 | * a relational database such as Postgres or MySql 6 | * 7 | * Included here as an example and its not implemented or used. 8 | */ 9 | export default class SqlStore implements IStoreReader, IStoreWriter { 10 | save(id: number, message: string): void { 11 | // Write to database code would go here 12 | } 13 | read(id: number): string { 14 | // Read from database here 15 | return '' 16 | } 17 | } -------------------------------------------------------------------------------- /conclusion/src/StoreCache.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from './IStoreWriter'; 2 | import IStoreReader from './IStoreReader'; 3 | 4 | export default class StoreCache implements IStoreWriter, IStoreReader { 5 | cache: { [key: number]: string } 6 | writer: IStoreWriter; 7 | reader: IStoreReader; 8 | 9 | constructor(_writer: IStoreWriter, _reader: IStoreReader) { 10 | this.cache = {} 11 | this.writer = _writer 12 | this.reader = _reader 13 | } 14 | 15 | public save(id: number, message: string): void { 16 | this.writer.save(id, message) 17 | this.addOrUpdate(id, message); 18 | } 19 | 20 | public read(id: number) { 21 | if(this.exists(id)) { 22 | return this.cache[id]; 23 | } 24 | var retValue = this.reader.read(id); 25 | if(retValue !== undefined) { 26 | this.addOrUpdate(id, retValue); 27 | } 28 | return retValue; 29 | } 30 | 31 | private exists?(id: number): boolean { 32 | return this.cache.hasOwnProperty(id); 33 | } 34 | 35 | private addOrUpdate(id: number, message: string) { 36 | this.cache[id] = message; 37 | } 38 | } -------------------------------------------------------------------------------- /conclusion/src/StoreLogger.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from "./IStoreWriter" 2 | import IStoreReader from "./IStoreReader"; 3 | 4 | export default class StoreLogger implements IStoreWriter, IStoreReader { 5 | writer: IStoreWriter 6 | reader: IStoreReader 7 | 8 | constructor(_writer: IStoreWriter, _reader: IStoreReader){ 9 | this.writer = _writer; 10 | this.reader = _reader; 11 | } 12 | 13 | public read(id: number): string { 14 | this.reading(id) 15 | var retValue = this.reader.read(id); 16 | if(retValue === undefined){ 17 | this.didNotFind(id) 18 | } else { 19 | this.returning(id) 20 | } 21 | return retValue; 22 | } 23 | 24 | public save(id: number, message: string): void { 25 | this.saving(id); 26 | try { 27 | this.writer.save(id, message); 28 | } catch (err) { 29 | console.log(err) 30 | this.errorSaving(id) 31 | } 32 | this.saved(id); 33 | } 34 | 35 | public saving(id: number): void { 36 | console.log(`Saving message ${id}.`) 37 | } 38 | 39 | public saved(id: number): void { 40 | console.info(`Saved message ${id}.`) 41 | } 42 | 43 | public reading(id: number): void { 44 | console.debug(`Reading message ${id}.`) 45 | } 46 | 47 | public didNotFind(id: number): void { 48 | console.debug(`No message ${id} found.`) 49 | } 50 | 51 | public missingFromCache(id: number): void { 52 | console.debug(`Message ${id} missing from cache.`) 53 | } 54 | 55 | public returning(id: number): void { 56 | console.debug(`Returning message ${id}.`) 57 | } 58 | 59 | public errorSaving(id: number): void { 60 | console.error(`Error saving message ${id}.`) 61 | } 62 | } -------------------------------------------------------------------------------- /conclusion/src/TestExamples.ts: -------------------------------------------------------------------------------- 1 | import MessageStore from './MessageStore'; 2 | import FileStore from "./FileStore"; 3 | import StoreCache from "./StoreCache"; 4 | import StoreLogger from "./StoreLogger"; 5 | 6 | import fs from 'fs'; 7 | import path from 'path' 8 | 9 | var dirtest = "./testfiles"; 10 | var dirpath = path.join(__dirname, dirtest) 11 | if(!fs.existsSync(dirpath)){ 12 | fs.mkdirSync(dirpath) 13 | } 14 | 15 | // 'Compose' our objects .... 16 | var fileStore = new FileStore(dirtest); 17 | var cache = new StoreCache(fileStore, fileStore); 18 | var logger = new StoreLogger(cache, cache); 19 | var messagestore = new MessageStore(logger, logger) 20 | 21 | // Test the CustomMessageStore class 22 | console.log("** Test the CustomMessageStore class **") 23 | console.log() 24 | 25 | messagestore.save(99, 'Message 99 saved via MessageStore class') 26 | var fileMessage99 = messagestore.read(99) 27 | console.log(fileMessage99) 28 | var fileMessage33 = messagestore.read(33) 29 | console.log(fileMessage33) 30 | messagestore.save(33, 'Message 33 saved via MessageStore class') 31 | console.log() -------------------------------------------------------------------------------- /conclusion/tests/src/FileStore.test.ts: -------------------------------------------------------------------------------- 1 | import FileStore from '../../src/FileStore'; 2 | import { readFileSync, writeFileSync } from 'fs' 3 | import { tmpdir } from 'os' 4 | import IFileLocator from '../../src/IFileLocator'; 5 | 6 | describe('FileStore', () => { 7 | var filestore: FileStore 8 | var directory: string 9 | var msg: string 10 | var id: number 11 | beforeEach(() => { 12 | msg = 'a message to save to disk' 13 | id = 1 14 | directory = 'teststore' 15 | filestore = new FileStore(directory) 16 | }) 17 | describe('IFileLocator', () => { 18 | describe('getFileInfo', () => { 19 | it('should return full path of the message file by id', () => { 20 | var expectedPath = (__dirname + '/' + directory + `/${id}.txt`).replace('tests/','') 21 | expect(filestore.getFileInfo(1)).toBe(expectedPath) 22 | }) 23 | }) 24 | }) 25 | describe('reading and writing files', () => { 26 | var fullFileName: Function 27 | var getFileInfoOrig: any 28 | beforeEach(() => { 29 | getFileInfoOrig = filestore.getFileInfo 30 | fullFileName = (): string => { return tmpdir() + `/${id}.txt` } 31 | filestore.getFileInfo = jest.fn((id: number): string => { return fullFileName() }) 32 | }) 33 | describe('IStoreWriter', () => { 34 | describe('save', () => { 35 | it('should save the message to a file called id.txt', () => { 36 | filestore.save(id, msg) 37 | expect(readFileSync(fullFileName(), {encoding: 'ASCII'})).toBe(msg) 38 | }) 39 | }) 40 | }) 41 | describe('IStoreReader', () => { 42 | describe('read', () => { 43 | describe('when the file does not exist', () => { 44 | it('returns undefined', () => { 45 | filestore.getFileInfo = getFileInfoOrig 46 | expect(filestore.read(99)).toBeUndefined() 47 | }) 48 | }) 49 | describe('when the file exists', () => { 50 | it('should read the message from file given an id', () => { 51 | msg = 'a new file to read' 52 | writeFileSync(fullFileName(), msg) 53 | expect(filestore.read(id)).toBe(msg) 54 | }) 55 | }) 56 | }) 57 | }) 58 | }) 59 | }) -------------------------------------------------------------------------------- /conclusion/tests/src/MessageStore.test.ts: -------------------------------------------------------------------------------- 1 | import MessageStore from '../../src/MessageStore' 2 | import mockstore from './mockstore' 3 | 4 | describe('MessageStore', () => { 5 | var messagestore: MessageStore 6 | beforeEach(() => { 7 | messagestore = new MessageStore(mockstore, mockstore) 8 | }) 9 | describe('constructor', () => { 10 | it('should throw and error if called with null params', () => { 11 | expect(() => {new MessageStore(null, mockstore)}).toThrowError("writer argument cannot be null"); 12 | expect(() => {new MessageStore(mockstore, null)}).toThrowError("reader argument cannot be null"); 13 | }) 14 | }) 15 | describe('IStoreReader', () => { 16 | describe('read', () => { 17 | it('should call the underlying reader', () => { 18 | messagestore.read(1) 19 | expect(mockstore.read).toHaveBeenCalledWith(1) 20 | }) 21 | }) 22 | }) 23 | describe('IStoreWriter', () => { 24 | describe('save', () => { 25 | it('should call the underlying writer', () => { 26 | messagestore.save(1, 'message') 27 | expect(mockstore.save).toHaveBeenCalledWith(1, 'message') 28 | }) 29 | }) 30 | }) 31 | }) -------------------------------------------------------------------------------- /conclusion/tests/src/StoreCache.test.ts: -------------------------------------------------------------------------------- 1 | import StoreCache from '../../src/StoreCache' 2 | import mockstore from './mockstore' 3 | 4 | describe('StoreCache', () => { 5 | var cache: StoreCache; 6 | var id: number 7 | var msg: string 8 | 9 | beforeEach(() => { 10 | cache = new StoreCache(mockstore, mockstore) 11 | id = 1 12 | msg = 'a message in the cache' 13 | cache.cache[id] = msg; 14 | }) 15 | 16 | describe('exists?', () => { 17 | it('returns true when the message id exists', () => { 18 | expect(cache.cache[id]).toBe(msg) // sanity check 19 | // note exists? is a private method so the call 20 | // to the method is made via array access 21 | expect(cache["exists"](id)).toBeTruthy() 22 | }) 23 | }) 24 | describe('addOrUpdate', () => { 25 | describe('adding a new message', () => { 26 | it('should add a new message to the cache', () => { 27 | var newmsg = 'a new message' 28 | expect(cache["exists"](2)).toBeFalsy() 29 | cache["addOrUpdate"](2, newmsg) 30 | expect(cache["exists"](2)).toBeTruthy() 31 | expect(cache.cache[2]).toBe(newmsg) 32 | }) 33 | }) 34 | describe('updating an existing message', () => { 35 | it('should update the message with the id to new message', () => { 36 | var newmsg = 'a new message' 37 | expect(cache.cache[1]).toBe(msg) // the old message 38 | cache["addOrUpdate"](1, newmsg) 39 | expect(cache.cache[1]).toBe(newmsg) 40 | }) 41 | }) 42 | }) 43 | describe('IStoreReader', () => { 44 | describe('when existing message is read', () => { 45 | it('returns the message string', () => { 46 | expect(cache.read(id)).toBe(msg) 47 | }) 48 | }) 49 | describe('when message is read that does not exist in the cache', () => { 50 | it('reads the message from the underlying store, updates the cache and returns that value', () => { 51 | var newmsg = 'a new message from store' 52 | // confirm that message id 2 does not yet exist in the store 53 | expect(cache["exists"](2)).toBeFalsy() 54 | // mock the underlying store read method to return a set message string 55 | mockstore.read = jest.fn((id: number) => newmsg) 56 | // expect the cache read to return the new message string 57 | expect(cache.read(2)).toBe(newmsg) 58 | // expect the cache to now contain a message for id 2 59 | expect(cache['exists'](2)).toBeTruthy() 60 | }) 61 | describe('and when the message also does not exist in the underlying store', () => { 62 | it('will return undefined and not update the cache', () => { 63 | mockstore.read = jest.fn((id: number) => undefined) 64 | expect(cache.read(2)).toBeUndefined() 65 | expect(cache["exists"](2)).toBeFalsy() 66 | expect(cache.cache[2]).toBeUndefined() 67 | }) 68 | }) 69 | }) 70 | }) 71 | describe('IStoreWriter', () => { 72 | beforeEach(()=> { 73 | cache["addOrUpdate"] = jest.fn() 74 | }) 75 | describe('when message is saved', () => { 76 | it('saves to the cache and the underlying store', () => { 77 | cache.save(id, msg) 78 | expect(mockstore.save).toHaveBeenCalledWith(id, msg) 79 | expect(cache["addOrUpdate"]).toHaveBeenCalledWith(1, msg) 80 | }) 81 | }) 82 | }) 83 | }) -------------------------------------------------------------------------------- /conclusion/tests/src/StoreLogger.test.ts: -------------------------------------------------------------------------------- 1 | import StoreLogger from '../../src/StoreLogger' 2 | import mockstore from './mockstore' 3 | 4 | describe('StoreLogger', () => { 5 | var logger: StoreLogger; 6 | var consolelog = console.log; 7 | var consoleinfo = console.info; 8 | var consoledebug = console.debug; 9 | var consoleerror = console.error; 10 | beforeEach(()=> { 11 | // mock console.log calls since we are testing this 12 | console.log = jest.fn(); 13 | console.info = jest.fn(); 14 | console.debug = jest.fn(); 15 | console.error = jest.fn(); 16 | logger = new StoreLogger(mockstore, mockstore); 17 | }) 18 | afterEach(() => { 19 | console.log = consolelog; 20 | console.info = consoleinfo; 21 | console.debug = consoledebug; 22 | console.error = consoleerror; 23 | }) 24 | describe('IStoreReader', () => { 25 | beforeEach(()=> { 26 | logger.reading = jest.fn() 27 | logger.returning = jest.fn() 28 | logger.didNotFind = jest.fn() 29 | }) 30 | describe('when message is found', () => { 31 | it('reads, logs and returns the message', () => { 32 | expect(logger.read(1)).toBe('Message') 33 | expect(logger.reading).toHaveBeenCalledWith(1) 34 | expect(logger.returning).toHaveBeenCalledWith(1) 35 | expect(mockstore.read).toHaveBeenCalledWith(1) 36 | }) 37 | }) 38 | describe('when message is NOT found', () => { 39 | it('logs did not find on the logger', () => { 40 | mockstore.read = jest.fn((id: number) => undefined) 41 | expect(logger.read(1)).toBe(undefined) 42 | expect(mockstore.read).toHaveBeenCalledWith(1) 43 | expect(logger.didNotFind).toHaveBeenCalledWith(1) 44 | }) 45 | }) 46 | }) 47 | describe('IStoreWriter', () => { 48 | beforeEach(()=> { 49 | logger.saving = jest.fn() 50 | logger.saved = jest.fn() 51 | logger.errorSaving = jest.fn() 52 | }) 53 | describe('when message is written sucessfully', () => { 54 | it('persists the message via the store and logs success', () => { 55 | var msg = 'A new message to save' 56 | logger.save(1, msg) 57 | expect(mockstore.save).toHaveBeenCalledWith(1, msg) 58 | expect(logger.saving).toHaveBeenCalledWith(1) 59 | expect(logger.saved).toHaveBeenCalledWith(1) 60 | }) 61 | }) 62 | describe('when call to writer save throws a new Error', () => { 63 | it('logs an error saving', () => { 64 | mockstore.save = jest.fn((id: number, message: string) => {throw new Error('test')}) 65 | logger.save(1, 'setup to fail!') 66 | expect(mockstore.save).toThrowError('test') 67 | expect(logger.errorSaving).toHaveBeenCalledWith(1) 68 | }) 69 | }) 70 | }) 71 | describe('saving', () => { 72 | it('logs a saving message via console.log', () => { 73 | logger.saving(1); 74 | expect(console.log).toHaveBeenCalledWith('Saving message 1.'); 75 | }) 76 | }) 77 | describe('saved', () => { 78 | it('logs a saved message via console.info', () => { 79 | logger.saved(1) 80 | expect(console.info).toHaveBeenCalledWith('Saved message 1.') 81 | }) 82 | }) 83 | describe('reading', () => { 84 | it('logs a reading message via console.debug', () => { 85 | logger.reading(1) 86 | expect(console.debug).toHaveBeenCalledWith('Reading message 1.') 87 | }) 88 | }) 89 | describe('didNotFind', () => { 90 | it('logs a didNotFind message via console.debug', () => { 91 | logger.didNotFind(1) 92 | expect(console.debug).toHaveBeenCalledWith('No message 1 found.') 93 | }) 94 | }) 95 | describe('missingFromCache', () => { 96 | it('logs a missingFromCache message via console.debug', () => { 97 | logger.missingFromCache(1) 98 | expect(console.debug).toHaveBeenCalledWith('Message 1 missing from cache.') 99 | }) 100 | }) 101 | describe('returning', () => { 102 | it('logs a returning message via console.debug', () => { 103 | logger.returning(1) 104 | expect(console.debug).toHaveBeenCalledWith('Returning message 1.') 105 | }) 106 | }) 107 | describe('errorSaving', () => { 108 | it('logs a errorSaving message via console.debug', () => { 109 | logger.errorSaving(1) 110 | expect(console.error).toHaveBeenCalledWith('Error saving message 1.') 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /conclusion/tests/src/mockstore.ts: -------------------------------------------------------------------------------- 1 | var mockstore; 2 | 3 | // mockstore needs to be the same shape as 4 | // the IStoreWriter and IStoreReader 5 | export default mockstore = { 6 | read: jest.fn((id: number) => 'Message'), 7 | save: jest.fn((id: number, message: string) => {}) 8 | }; -------------------------------------------------------------------------------- /conclusion/tests/tsconfig.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devbootstrap/SOLID-Principles-Examples-using-Typescript/a46022f7905c8364bbaebbe5cbe3e27a23cf6883/conclusion/tests/tsconfig.json -------------------------------------------------------------------------------- /conclusion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | // "types": ["@types/jest"], 13 | "paths": { 14 | "*": ["node_modules/*", "src/types/*"] 15 | } 16 | }, 17 | "include": ["src/**/*"] 18 | } -------------------------------------------------------------------------------- /dip/README.md: -------------------------------------------------------------------------------- 1 | # Dependency Inversion Principle 2 | 3 | This is a basic TypeScript application to demo the Dependency InvesionPrinciple (ISP). 4 | 5 | We are progressing on from the LSP excercise so we are essentially refactoring the code that already exists in [../isp/src](../isp/src) folder. Please refer to that code as a starting reference. 6 | 7 | ## What is the Dependency Inversion Principle? 8 | 9 | This states that high level modules should not depend on low level modules. Both should depend on abstractions. 10 | 11 | Moreover, abstractions should not depend on details. The details should depend on abstractions. 12 | 13 | So in a way this is closely related to the Interface Segregation Principle in that clients own the interface. So a client can talk to an abstraction because it owns the interface and whatever is on the other side of that abstraction is an implementation detail. 14 | 15 | ### Refactoring the application 16 | 17 | In this application we mostly applied a combination of the **composite** and **decorator** patterns. 18 | 19 | The main approach is to include the [IStoreReader](./src/IStoreReader.ts) and [IStoreWriter](./src/IStoreWriter.ts) interfaces in the [FileStore](./src/FileStore.ts), [StoreCache](./src/StoreCache.ts) and [StoreLogger](./src/StoreLogger.ts) classes and then to compoose everything within the StoreLogger so that it acts as the definitive _reader_ and _writer_ implementation. 20 | 21 | What we end up with is the [MessageStore](./src/MessageStore.ts) effectivly becomes redundant in this example. However, if there was originally some business logic then this is where it would live. 22 | 23 | In order to use this implemetnation the client needs to compose the objects in such away so that they adhear to the requirements of the applications interface. Such a composition to use the application might look like the following example: 24 | 25 | ```ts 26 | var directory = "./testfiles"; 27 | var filestore = new FileStore(directory); 28 | var cache = new StoreCache(filestore, filestore); 29 | var logger = new StoreLogger(cache, cache); 30 | var messagestore = new MessageStore(logger, logger) 31 | 32 | // Now we can use our messagestore instance 33 | messagestore.save(99, 'Message 99 is a very important message!') 34 | var msg99 = messagestore.read(99) 35 | ``` 36 | 37 | ## Running the application 38 | 39 | Install the dependences. Note this includes `node-ts` which allows to run TypeScript files without having to compile first. 40 | 41 | **NOTE:** Make sure you change your directory into the [lsp](./lsp) directory first! 42 | 43 | ``` 44 | npm install 45 | ``` 46 | 47 | Now, it should be possible to run the application using the following commands. 48 | 49 | ``` 50 | npx ts-node src/TestExamples.ts 51 | ``` 52 | 53 | Alternatively, you can compile the TypeScript files and then run the output JavaScript files form the resuling `dist` folder: 54 | 55 | ``` 56 | npm run build 57 | node dist/TestExamples.js 58 | ``` -------------------------------------------------------------------------------- /dip/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dip-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "13.13.2", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz", 10 | "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==", 11 | "dev": true 12 | }, 13 | "arg": { 14 | "version": "4.1.3", 15 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 16 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 17 | "dev": true 18 | }, 19 | "buffer-from": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 22 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 23 | "dev": true 24 | }, 25 | "diff": { 26 | "version": "4.0.2", 27 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 28 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 29 | "dev": true 30 | }, 31 | "make-error": { 32 | "version": "1.3.6", 33 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 34 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 35 | "dev": true 36 | }, 37 | "source-map": { 38 | "version": "0.6.1", 39 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 40 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 41 | "dev": true 42 | }, 43 | "source-map-support": { 44 | "version": "0.5.19", 45 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 46 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 47 | "dev": true, 48 | "requires": { 49 | "buffer-from": "^1.0.0", 50 | "source-map": "^0.6.0" 51 | } 52 | }, 53 | "ts-node": { 54 | "version": "8.9.1", 55 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.1.tgz", 56 | "integrity": "sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==", 57 | "dev": true, 58 | "requires": { 59 | "arg": "^4.1.0", 60 | "diff": "^4.0.1", 61 | "make-error": "^1.1.1", 62 | "source-map-support": "^0.5.17", 63 | "yn": "3.1.1" 64 | } 65 | }, 66 | "typescript": { 67 | "version": "3.8.3", 68 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 69 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 70 | "dev": true 71 | }, 72 | "yn": { 73 | "version": "3.1.1", 74 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 75 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 76 | "dev": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /dip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dip-example", 3 | "version": "1.0.0", 4 | "description": "An example application to demo the Dependency Invesion Principal", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "keywords": [], 11 | "author": "Darren Jensen", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/node": "^13.13.2", 15 | "ts-node": "^8.9.0", 16 | "typescript": "^3.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dip/src/FileStore.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import IFileLocator from './IFileLocator'; 4 | import IStoreWriter from './IStoreWriter'; 5 | import IStoreReader from './IStoreReader'; 6 | 7 | /** 8 | * A class that allows for messages to be stored in 9 | * a local file system 10 | */ 11 | export default class FileStore implements IStoreReader, IStoreWriter, IFileLocator { 12 | directory: string 13 | 14 | constructor(_directory: string) { 15 | this.directory = _directory; 16 | } 17 | 18 | public save(id: number, message: string): void { 19 | var fileFullName = this.getFileInfo(id); 20 | fs.writeFileSync(fileFullName, message) 21 | } 22 | 23 | public read(id: number): string { 24 | var fileFullName = this.getFileInfo(id); 25 | var exists = fs.existsSync(fileFullName); 26 | if(!exists) { 27 | return undefined 28 | } 29 | return fs.readFileSync(fileFullName, {encoding: 'ASCII'}); 30 | } 31 | 32 | public getFileInfo(id: number): string { 33 | return path.join(__dirname, this.directory, `${id}.txt`) 34 | } 35 | } -------------------------------------------------------------------------------- /dip/src/IFileLocator.ts: -------------------------------------------------------------------------------- 1 | export default interface IFileLocator { 2 | getFileInfo(id: number): string 3 | } -------------------------------------------------------------------------------- /dip/src/IStoreReader.ts: -------------------------------------------------------------------------------- 1 | export default interface IStoreReader { 2 | read(id: number): string 3 | } -------------------------------------------------------------------------------- /dip/src/IStoreWriter.ts: -------------------------------------------------------------------------------- 1 | export default interface IStoreWriter { 2 | save(id: number, message: string): void 3 | } -------------------------------------------------------------------------------- /dip/src/MessageStore.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from "./IStoreWriter"; 2 | import IStoreReader from "./IStoreReader"; 3 | 4 | export default class MessageStore { 5 | writer: IStoreWriter; 6 | reader: IStoreReader; 7 | 8 | constructor(writer: IStoreWriter,reader: IStoreReader) { 9 | if(writer === null) { 10 | throw new Error("writer argument cannot be null") 11 | } 12 | if(reader === null) { 13 | throw new Error("reader argument cannot be null") 14 | } 15 | this.writer = writer; 16 | this.reader = reader; 17 | } 18 | 19 | /** 20 | * 21 | * @param id the id of the message to save 22 | * @param message the text message to write to storage 23 | * 24 | */ 25 | public save(id: number, message: string) { 26 | this.writer.save(id, message); 27 | } 28 | 29 | /** 30 | * 31 | * @param id the id of the message to read 32 | * @returns message string 33 | * 34 | */ 35 | public read(id: number): string { 36 | return this.reader.read(id); 37 | } 38 | } -------------------------------------------------------------------------------- /dip/src/SqlStore.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from './IStoreWriter' 2 | import IStoreReader from './IStoreReader' 3 | /** 4 | * A class that allows for messages to be stored in 5 | * a relational database such as Postgres or MySql 6 | * 7 | * Included here as an example and its not implemented or used. 8 | */ 9 | export default class SqlStore implements IStoreReader, IStoreWriter { 10 | save(id: number, message: string): void { 11 | // Write to database code would go here 12 | } 13 | read(id: number): string { 14 | // Read from database here 15 | return '' 16 | } 17 | } -------------------------------------------------------------------------------- /dip/src/StoreCache.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from './IStoreWriter'; 2 | import IStoreReader from './IStoreReader'; 3 | 4 | export default class StoreCache implements IStoreWriter, IStoreReader { 5 | cache: { [key: number]: string } 6 | writer: IStoreWriter; 7 | reader: IStoreReader; 8 | 9 | constructor(_writer: IStoreWriter, _reader: IStoreReader) { 10 | this.cache = {} 11 | this.writer = _writer 12 | this.reader = _reader 13 | } 14 | 15 | public save(id: number, message: string): void { 16 | this.writer.save(id, message) 17 | this.addOrUpdate(id, message); 18 | } 19 | 20 | public read(id: number) { 21 | if(this.exists(id)) { 22 | return this.cache[id]; 23 | } 24 | var retValue = this.reader.read(id); 25 | if(retValue !== undefined) { 26 | this.addOrUpdate(id, retValue); 27 | } 28 | return retValue; 29 | } 30 | 31 | private exists?(id: number): boolean { 32 | return this.cache.hasOwnProperty(id); 33 | } 34 | 35 | private addOrUpdate(id: number, message: string) { 36 | this.cache[id] = message; 37 | } 38 | } -------------------------------------------------------------------------------- /dip/src/StoreLogger.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from "./IStoreWriter" 2 | import IStoreReader from "./IStoreReader"; 3 | 4 | export default class StoreLogger implements IStoreWriter, IStoreReader { 5 | writer: IStoreWriter 6 | reader: IStoreReader 7 | 8 | constructor(_writer: IStoreWriter, _reader: IStoreReader){ 9 | this.writer = _writer; 10 | this.reader = _reader; 11 | } 12 | 13 | public read(id: number): string { 14 | this.reading(id) 15 | var retValue = this.reader.read(id); 16 | if(retValue === undefined){ 17 | this.didNotFind(id) 18 | } else { 19 | this.returning(id) 20 | } 21 | return retValue; 22 | } 23 | 24 | public save(id: number, message: string): void { 25 | this.saving(id); 26 | try { 27 | this.writer.save(id, message); 28 | } catch (err) { 29 | this.errorSaving(id) 30 | } 31 | this.saved(id); 32 | } 33 | 34 | public saving(id: number): void { 35 | console.log(`Saving message ${id}.`) 36 | } 37 | 38 | public saved(id: number): void { 39 | console.info(`Saved message ${id}.`) 40 | } 41 | 42 | public reading(id: number): void { 43 | console.debug(`Reading message ${id}`) 44 | } 45 | 46 | public didNotFind(id: number): void { 47 | console.debug(`No message ${id} found.`) 48 | } 49 | 50 | public missingFromCache(id: number): void { 51 | console.debug(`Message ${id} missing from cache.`) 52 | } 53 | 54 | public returning(id: number): void { 55 | console.debug(`Returning message ${id}.`) 56 | } 57 | 58 | public errorSaving(id: number): void { 59 | console.error(`Error saving message ${id}.`) 60 | } 61 | } -------------------------------------------------------------------------------- /dip/src/TestExamples.ts: -------------------------------------------------------------------------------- 1 | import MessageStore from './MessageStore'; 2 | import FileStore from "./FileStore"; 3 | import StoreCache from "./StoreCache"; 4 | import StoreLogger from "./StoreLogger"; 5 | 6 | import fs from 'fs'; 7 | import path from 'path' 8 | 9 | var dirtest = "./testfiles"; 10 | var dirpath = path.join(__dirname, dirtest) 11 | if(!fs.existsSync(dirpath)){ 12 | fs.mkdirSync(dirpath) 13 | } 14 | 15 | // 'Compose' our objects .... 16 | var fileStore = new FileStore(dirtest); 17 | var cache = new StoreCache(fileStore, fileStore); 18 | var logger = new StoreLogger(cache, cache); 19 | var messagestore = new MessageStore(logger, logger) 20 | 21 | // Test the CustomMessageStore class 22 | console.log("** Test the CustomMessageStore class **") 23 | console.log() 24 | 25 | messagestore.save(99, 'Message 99 saved via MessageStore class') 26 | var fileMessage99 = messagestore.read(99) 27 | console.log(fileMessage99) 28 | var fileMessage33 = messagestore.read(33) 29 | console.log(fileMessage33) 30 | messagestore.save(33, 'Message 33 saved via MessageStore class') 31 | console.log() -------------------------------------------------------------------------------- /dip/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"] 17 | } -------------------------------------------------------------------------------- /isp/README.md: -------------------------------------------------------------------------------- 1 | # Interface Substitution Principle 2 | 3 | This is a basic TypeScript application to demo the Interface Substitution Principle (ISP). 4 | 5 | We are progressing on from the LSP excercise so we are essentially refactoring the code that already exists in [../lsp/src](../lsp/src) folder. Please refer to that code as a starting reference. 6 | 7 | ## What is the Interface Substitution Principle? 8 | 9 | The Interface Segregation Principle (ISP) states that Clients should not be forced to depend on methods they do not use. 10 | 11 | So it's important, at this point, to understand **who owns the Interface**? It's _not_ defined by the concrete class that uses it, instead it is the **client that owns the interface**. Remember that interfaces are used to help introduce loose coupling. So it's not the concrete class that needs the interface - its the client that needs the interface. 12 | 13 | So it's the client that owns the interface and the client defines what it needs. Therefore there is no need for the client to define a method on that interface if it does not need that method! 14 | 15 | This should lead us to producing simple, focused interfaces and, futher, we should favour _Role Interfaces_ over _Header Interfaces_. 16 | 17 | ### Header Interfaces 18 | 19 | These are basically the fully extracted interfaces that we have already seen. It is an interface that is extracted from a concrete class and generally includes lots of (i.e. too many) members. Recall, also, that these potentially break the Liskov Substitution Principle and are another reason to avoid these types of interfaces. They are called header interfaces since they are similar to the way C or C++ works in that a header file (.h) is required to include all the definitions of the members of the concrete implementation file (.c). These are essentially redundant files that duplicate what is already available in the implementation file. 20 | 21 | So a header interface is similar to a header file in that it just states that these are all the methods available in a certain concrete class. Therefore, if you have a header interface with a large number of members then it becomes unlikely that you will ever have a need for a different concrete class that would need exactly the same interface - without having to violate the LSP (by needing to throw lots of “NotSupportedExceptions” everywhere). 22 | 23 | ### Role Interfaces 24 | 25 | A role interface is an interface that defines very few members. So in being client driven and client owned then it will only define the set of members that it actually needs to talk to. So an extreme role interface is one with just one member. It turns out that **defining role interfaces with just one member makes it easier to solve any violations of the LSP**. With only one member defined there is no interaction with other members because there aren't any! 26 | 27 | ### Refactorings made in this project 28 | 29 | To fix the violation of the LSP from the previous exercies, where we have the SqlStore having to implement a method that it cannot via the IStore interface, we simply create a new interface and remove that method from IStore. The result, of course is that we no longer need to define a method that we done need `getFileInfo()` in the [SqlStore](./src/SqlStore.ts) class, like so: 30 | 31 | ```ts 32 | export default interface IFileLocator { 33 | getFileInfo(id: number): string 34 | } 35 | ``` 36 | 37 | Note that in the in the [SqlStore](./src/SqlStore.ts) class the getFileInfo method has been removed. 38 | 39 | We also exctracted some commonality. One thing is that there are many cases of different classes using a message call `xyz(id: number, message: string): void`. Note that I call this xyz becuase when looking for a common interface the method name can be ignored for the time being and we only need to focus on the input parameters and the response type. So here the input was alwayws `id:number` and `message:string` with a return of `void`. So we can define a new interface that defines that method format as an [IStoreWriter](.src/IStoreWriter.ts) interface. As you can see its a good example of a _role interface_. 40 | 41 | ```ts 42 | export default interface IStoreWriter { 43 | save(id: number, message: string): void 44 | } 45 | ``` 46 | 47 | This can now be implemented in the [FileStore](./src/FileStore.ts) and [SqlStore](./src/SqlStore.ts) classes. 48 | 49 | ### Further refactoring to create log classes that can implement IStoreWriter 50 | 51 | As you can see we also added two new classes [LogSavedStoreWriter](./src/LogSaveedStoreWriter.ts) and [LogSavingStoreWriter](./src/LogSavingStoreWriter.ts). However, these are just examples to show how we found other uses of methods with the same signature but with differnt names and in order to get the same name we needed these two new classes. However, this does indeed break the Open Closed Priniple. So we will solve this in the next module by using composition instead of inheritance. 52 | 53 | ## Running the application 54 | 55 | Install the dependences. Note this includes `node-ts` which allows to run TypeScript files without having to compile first. 56 | 57 | **NOTE:** Make sure you change your directory into the [lsp](./lsp) directory first! 58 | 59 | ``` 60 | npm install 61 | ``` 62 | 63 | Now, it should be possible to run the application using the following commands. 64 | 65 | ``` 66 | npx ts-node src/TestExamples.ts 67 | ``` 68 | 69 | Alternatively, you can compile the TypeScript files and then run the output JavaScript files form the resuling `dist` folder: 70 | 71 | ``` 72 | npm run build 73 | node dist/TestExamples.js 74 | ``` -------------------------------------------------------------------------------- /isp/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isp-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "13.13.2", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz", 10 | "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==", 11 | "dev": true 12 | }, 13 | "arg": { 14 | "version": "4.1.3", 15 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 16 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 17 | "dev": true 18 | }, 19 | "buffer-from": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 22 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 23 | "dev": true 24 | }, 25 | "diff": { 26 | "version": "4.0.2", 27 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 28 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 29 | "dev": true 30 | }, 31 | "make-error": { 32 | "version": "1.3.6", 33 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 34 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 35 | "dev": true 36 | }, 37 | "source-map": { 38 | "version": "0.6.1", 39 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 40 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 41 | "dev": true 42 | }, 43 | "source-map-support": { 44 | "version": "0.5.19", 45 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 46 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 47 | "dev": true, 48 | "requires": { 49 | "buffer-from": "^1.0.0", 50 | "source-map": "^0.6.0" 51 | } 52 | }, 53 | "ts-node": { 54 | "version": "8.9.0", 55 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.0.tgz", 56 | "integrity": "sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w==", 57 | "dev": true, 58 | "requires": { 59 | "arg": "^4.1.0", 60 | "diff": "^4.0.1", 61 | "make-error": "^1.1.1", 62 | "source-map-support": "^0.5.17", 63 | "yn": "3.1.1" 64 | } 65 | }, 66 | "typescript": { 67 | "version": "3.8.3", 68 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 69 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 70 | "dev": true 71 | }, 72 | "yn": { 73 | "version": "3.1.1", 74 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 75 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 76 | "dev": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /isp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isp-example", 3 | "version": "1.0.0", 4 | "description": "An example application to demo the Interface Segregation Principal", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "keywords": [], 11 | "author": "Darren Jensen", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/node": "^13.13.2", 15 | "ts-node": "^8.9.0", 16 | "typescript": "^3.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /isp/src/FileStore.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path' 3 | import IStore from './IStore' 4 | import IFileLocator from './IFileLocator'; 5 | import IStoreLogger from './IStoreLogger'; 6 | import IStoreWriter from './IStoreWriter'; 7 | 8 | /** 9 | * A class that allows for messages to be stored in 10 | * a local file system 11 | * 12 | * Note this class implements the IStore interface 13 | * and now also the IFileLocator interface 14 | */ 15 | export default class FileStore implements IStore, IStoreWriter, IFileLocator { 16 | directory: string 17 | logger: IStoreLogger 18 | 19 | constructor(_directory: string, _logger: IStoreLogger) { 20 | this.directory = _directory; 21 | this.logger = _logger; 22 | } 23 | 24 | public save(id: number, message: string): void { 25 | this.logger.saving(id, message); 26 | 27 | // Below is how we might use LogSavedStoreWriter 28 | // But we will not since it breaks OCP !! Because 29 | // the client cannot change the implentation of 30 | // the logger class if we use this approach. 31 | // NOTEL: A solution is in the next exercise (which is 32 | // to use composition instead of inheritance) 33 | 34 | // new LogSavingStoreWriter().save(id, message); 35 | 36 | var fileFullName = this.getFileInfo(id); 37 | try { 38 | fs.writeFileSync(fileFullName, message) 39 | } catch (err) { 40 | this.logger.errorSaving(id); 41 | } 42 | this.logger.saved(id, message); 43 | 44 | // Below is how we might use LogSavedStoreWriter 45 | // We don't use this for the same reasons as mentioned above 46 | 47 | // new LogSavedStoreWriter().save(id, message); 48 | } 49 | 50 | public read(id: number): string { 51 | this.logger.readingFilestore(id) 52 | var fileFullName = this.getFileInfo(id); 53 | var exists = fs.existsSync(fileFullName); 54 | if(!exists) { 55 | this.logger.didNotFind(id); 56 | return undefined 57 | } 58 | return fs.readFileSync(fileFullName, {encoding: 'ASCII'}); 59 | } 60 | 61 | public getFileInfo(id: number): string { 62 | return path.join(__dirname, this.directory, `${id}.txt`) 63 | } 64 | } -------------------------------------------------------------------------------- /isp/src/IFileLocator.ts: -------------------------------------------------------------------------------- 1 | export default interface IFileLocator { 2 | getFileInfo(id: number): string 3 | } -------------------------------------------------------------------------------- /isp/src/IStore.ts: -------------------------------------------------------------------------------- 1 | export default interface IStore { 2 | save(id: number, message: string): void 3 | read(id: number): string 4 | } -------------------------------------------------------------------------------- /isp/src/IStoreCache.ts: -------------------------------------------------------------------------------- 1 | export default interface IStoreCache { 2 | save(id: number, message: string): void 3 | getOrAdd(id: number, fnStoreRead: Function): string 4 | } -------------------------------------------------------------------------------- /isp/src/IStoreLogger.ts: -------------------------------------------------------------------------------- 1 | export default interface IStoreLogger { 2 | // Note we add the message to the saving and saved methods 3 | saving(id: number, message: string): void 4 | saved(id: number, message: string): void 5 | readingFilestore(id: number): void 6 | readingCache(id: number): void 7 | didNotFind(id: number): void 8 | missingFromCache(id: number): void 9 | returning(id: number): void 10 | errorSaving(id: number): void 11 | } -------------------------------------------------------------------------------- /isp/src/IStoreWriter.ts: -------------------------------------------------------------------------------- 1 | export default interface IStoreWriter { 2 | save(id: number, message: string): void 3 | } -------------------------------------------------------------------------------- /isp/src/LogSavedStoreWriter.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from "./IStoreWriter" 2 | 3 | /** 4 | * Example of how we can extract the logging method 5 | * to a new class so that we can implement a common 6 | * interface for IStoreWriter. 7 | * 8 | * NOTE: this is NOT a good solution in the long term 9 | * since it breaks the OCP. So we will not use this 10 | * its just here to show you that we can break the class 11 | * into these smaller classes in order to implement 12 | * a common interface. 13 | */ 14 | export default class LogSavedStoreWriter implements IStoreWriter { 15 | public save(id: number): void { 16 | console.info(`Saved message ${id}.`) 17 | } 18 | } -------------------------------------------------------------------------------- /isp/src/LogSavingStoreWriter.ts: -------------------------------------------------------------------------------- 1 | import IStoreWriter from "./IStoreWriter" 2 | 3 | /** 4 | * Example of how we can extract the logging method 5 | * to a new class so that we can implement a common 6 | * interface for IStoreWriter. 7 | * 8 | * NOTE: this is not a good solution in the long term 9 | * since it breaks the OCP. So we will not use this 10 | * its just here to show you that we can break the class 11 | * into these smaller classes in order to implement 12 | * a common interface. 13 | */ 14 | export default class LogSavingStoreWriter implements IStoreWriter { 15 | public save(id: number): void { 16 | console.log(`Saving message ${id}.`) 17 | } 18 | } -------------------------------------------------------------------------------- /isp/src/MessageStore.ts: -------------------------------------------------------------------------------- 1 | import FileStore from "./FileStore"; 2 | import StoreCache from "./StoreCache"; 3 | import StoreLogger from "./StoreLogger"; 4 | import IStore from "./IStore"; 5 | import IStoreCache from "./IStoreCache"; 6 | import IStoreLogger from "./IStoreLogger"; 7 | 8 | export default class MessageStore { 9 | store : IStore; 10 | cache: IStoreCache; 11 | logger: IStoreLogger; 12 | 13 | constructor(directory: string) { 14 | this.logger = new StoreLogger(); 15 | this.store = new FileStore(directory, this.Logger); 16 | this.cache = new StoreCache(this.Logger); 17 | } 18 | 19 | /** 20 | * A getter that returns an instance of logger 21 | * Purpose of this is to be able to extend this class 22 | * and use a different type of logger (that inherits from StoreLogger) 23 | */ 24 | get Logger(): IStoreLogger { 25 | return this.logger; 26 | } 27 | 28 | /** 29 | * A getter that returns an instance of store 30 | * Purpose of this is to be able to use a different type of store 31 | * that would implement the IStore interface 32 | */ 33 | get Store(): IStore { 34 | return this.store; 35 | } 36 | 37 | /** 38 | * A getter that returns an instance of cache 39 | * Purpose of this is to be able to use a different type of cache 40 | * that would implement the IStore interface 41 | */ 42 | get Cache(): IStoreCache { 43 | return this.cache; 44 | } 45 | 46 | /** 47 | * 48 | * @param id the id of the file to save 49 | * @param message the text message to write to the file 50 | * 51 | * Function writes the file to disk using the id as part 52 | * of the filename. The id is a number and the file name is 53 | * formed as a .txt file using the pattern id.txt. Its saved 54 | * in the relative directory as set in the constructor. 55 | */ 56 | public save (id: number, message: string) { 57 | this.Store.save(id, message); 58 | this.Cache.save(id, message); 59 | } 60 | 61 | /** 62 | * 63 | * @param id the id of the message to read 64 | * 65 | * Function asks the cache to fetch the message 66 | * by id and passes an anonymous function that 67 | * fetches the message from the stoere 68 | * if the message is not in the cache. 69 | * 70 | * @returns message string 71 | */ 72 | public read(id: number): string { 73 | var message = this.Cache.getOrAdd( 74 | id, () => this.Store.read(id)) 75 | return message 76 | } 77 | } -------------------------------------------------------------------------------- /isp/src/SqlStore.ts: -------------------------------------------------------------------------------- 1 | import IStore from './IStore' 2 | import IStoreWriter from './IStoreWriter' 3 | /** 4 | * A class that allows for messages to be stored in 5 | * a relational database such as Postgres or MySql 6 | */ 7 | export default class SqlStore implements IStore, IStoreWriter { 8 | save(id: number, message: string): void { 9 | // Write to database code would go here 10 | } 11 | read(id: number): string { 12 | // Read from database here 13 | return '' 14 | } 15 | /** 16 | * Note: We have removed the getFileInfo method definition 17 | * since its no longer part of the IStore interfave 18 | **/ 19 | } -------------------------------------------------------------------------------- /isp/src/StoreCache.ts: -------------------------------------------------------------------------------- 1 | import StoreLogger from './StoreLogger' 2 | import IStoreCache from './IStoreCache' 3 | import IStoreLogger from './IStoreLogger'; 4 | 5 | export default class StoreCache implements IStoreCache { 6 | cache: any; 7 | logger: IStoreLogger; 8 | 9 | constructor(_logger: IStoreLogger) { 10 | this.cache = {} 11 | this.logger = _logger 12 | } 13 | 14 | public save(id: number, message: string): void { 15 | this.cache[id] = message; 16 | } 17 | 18 | public getOrAdd(id: number, fnStoreRead: Function) { 19 | this.logger.readingCache(id) 20 | if(!this.exists(id)) { 21 | // The message does not exist in the cache 22 | this.logger.missingFromCache(id); 23 | // Load the contents from the store using the passed in function 24 | var message = fnStoreRead() 25 | // Save the message to the cache 26 | this.save(id, message); 27 | } 28 | return this.cache[id]; 29 | } 30 | 31 | public exists?(id: number): boolean { 32 | return this.cache.hasOwnProperty(id); 33 | } 34 | } -------------------------------------------------------------------------------- /isp/src/StoreLogger.ts: -------------------------------------------------------------------------------- 1 | import IStoreLogger from "./IStoreLogger" 2 | 3 | /** 4 | * Note we now implement IStoreLogger 5 | */ 6 | export default class StoreLogger implements IStoreLogger { 7 | public saving(id: number): void { 8 | console.log(`Saving message ${id}.`) 9 | } 10 | public saved(id: number): void { 11 | console.info(`Saved message ${id}.`) 12 | } 13 | public readingFilestore(id: number): void { 14 | console.debug(`Reading message ${id} from FileStore.`) 15 | } 16 | public readingCache(id: number): void { 17 | console.debug(`Reading message ${id} from StoreCache.`) 18 | } 19 | public didNotFind(id: number): void { 20 | console.debug(`No message ${id} found.`) 21 | } 22 | public missingFromCache(id: number): void { 23 | console.debug(`Message ${id} missing from cache.`) 24 | } 25 | public returning(id: number): void { 26 | console.debug(`Returning message ${id}.`) 27 | } 28 | public errorSaving(id: number): void { 29 | console.error(`Error saving message ${id}.`) 30 | } 31 | } -------------------------------------------------------------------------------- /isp/src/TestExamples.ts: -------------------------------------------------------------------------------- 1 | import MessageStore from './MessageStore'; 2 | import fs from 'fs'; 3 | import path from 'path' 4 | 5 | var dirtest = "./testfiles"; 6 | var dirpath = path.join(__dirname, dirtest) 7 | if(!fs.existsSync(dirpath)){ 8 | fs.mkdirSync(dirpath) 9 | } 10 | 11 | 12 | // Test the CustomMessageStore class 13 | console.log("** Test the CustomMessageStore class **") 14 | console.log() 15 | 16 | var messagestore = new MessageStore(dirtest); 17 | 18 | messagestore.save(99, 'Message 99 saved via MessageStore class') 19 | var fileMessage99 = messagestore.read(99) 20 | console.log(fileMessage99) 21 | var fileMessage33 = messagestore.read(33) 22 | console.log(fileMessage33) 23 | messagestore.save(33, 'Message 33 saved via MessageStore class') 24 | console.log() -------------------------------------------------------------------------------- /isp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"] 17 | } -------------------------------------------------------------------------------- /lsp/README.md: -------------------------------------------------------------------------------- 1 | # Liskov Substitution Principle 2 | 3 | This is a basic TypeScript application to demo the Liskov Substitution Principle (LSP). 4 | 5 | We are progressing on from the OCP excercise so we are essentially refactoring the code that already exists in [../ocp/src](../ocp/src) folder. Please refer to that code as a starting reference. 6 | 7 | ## What is the Liskov Substitution Principle? 8 | 9 | The LSP can be defined as the following: 10 | 11 | * Subtypes must be substitutable for their base types OR (another way....) 12 | * Given any client, it should be able to apply any implementation of an interface without changing the correctness of the system 13 | 14 | So what does the **correctness of the system** mean? Well for starters, it's not about changing the behaviour of the system because polymorphism is about changing the behaviour. Ultimately the correctness of the system is application specific but one high level idea is that any software system should not crash so if a client uses implementation A of an interface and the system does not crash, but then uses implementation B of an interface and the system DOES crash then you can say that you have changed the correctness of the system. That's a high level generic explanation as to the correctness of the system. 15 | 16 | So you can think of the correctness of the system as the superset of all the correct behaviour. If you stay within that boundary then you have not changed the correctness of the system. Go outside and you have changed it (like if it causes the system to crash). 17 | 18 | ## When is the LSP violated? 19 | 20 | ### Throwing _NotSupportedException_ (or similar). 21 | 22 | So for example if you implement an interface that does not require or its not possible to implement a method based on that interface then a typical thing to do is to throw an exception stating that the method is not implemented or not supported. Here in this [SqlStore](./src/SqlStore.ts) example we throw a new Error and pass in the appropriate message `throw new Error("Method not implemented.");`. Doing this violates the Liskov Substitution Principle. 23 | 24 | ```ts 25 | class SqlStore implements IStore { 26 | save(id: number, message: string): void { 27 | // Write to database code would go here 28 | } 29 | read(id: number): string { 30 | // Read from database here 31 | return '' 32 | } 33 | /** 34 | * Note that we need to throw 'Method not implemented' here 35 | * because in the context of the SqlStore the 'getFileInfo' 36 | * method is not required. 37 | * 38 | * Note: THIS BREAKS LSP!! We will discuss a solution to this later. 39 | */ 40 | getFileInfo(id: number): string { 41 | throw new Error("Method not implemented."); 42 | } 43 | } 44 | ``` 45 | 46 | As noted in the code comments we will discuss how to fix this in later lessons. 47 | 48 | ### Downcasts 49 | 50 | Another reason is if you are using downcasts a lot in your code to check which is the concrete implementation of an Interface. You might have a method that accepts a specific interface and you can use downcasting to check what is the actual concrete class that is being passed into the function that implements it. This shows a risk that you might be breaking the LSP. 51 | 52 | ### Extracted Interfaces 53 | 54 | This is when you take a concrete class and extract an interface from it. If you end up creating many interfaces (perhaps via the IDE that you use) and you just select to implement all methods of the concrete class into that interface then you can be at risk of violating the LSP. 55 | 56 | The below extracted [IStore](./src/IStore.ts) interface from the [FileStore](./src/FileStore.ts) concrete class does, in fact, violate ISP (in the implemtation of it in SqlStore example above). 57 | 58 | ```ts 59 | interface IStore { 60 | save(id: number, message: string): void 61 | read(id: number): string 62 | getFileInfo(id: number): string 63 | } 64 | ``` 65 | 66 | ## Running the application 67 | 68 | Install the dependences. Note this includes `node-ts` which allows to run TypeScript files without having to compile first. 69 | 70 | **NOTE:** Make sure you change your directory into the [lsp](./lsp) directory first! 71 | 72 | ``` 73 | npm install 74 | ``` 75 | 76 | Now, it should be possible to run the application using the following commands. 77 | 78 | ``` 79 | npx ts-node src/TestExamples.ts 80 | ``` 81 | 82 | Alternatively, you can compile the TypeScript files and then run the output JavaScript files form the resuling `dist` folder: 83 | 84 | ``` 85 | npm run build 86 | node dist/TestExamples.js 87 | ``` -------------------------------------------------------------------------------- /lsp/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "13.13.2", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz", 10 | "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==", 11 | "dev": true 12 | }, 13 | "arg": { 14 | "version": "4.1.3", 15 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 16 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 17 | "dev": true 18 | }, 19 | "buffer-from": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 22 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 23 | "dev": true 24 | }, 25 | "diff": { 26 | "version": "4.0.2", 27 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 28 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 29 | "dev": true 30 | }, 31 | "make-error": { 32 | "version": "1.3.6", 33 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 34 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 35 | "dev": true 36 | }, 37 | "source-map": { 38 | "version": "0.6.1", 39 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 40 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 41 | "dev": true 42 | }, 43 | "source-map-support": { 44 | "version": "0.5.19", 45 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 46 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 47 | "dev": true, 48 | "requires": { 49 | "buffer-from": "^1.0.0", 50 | "source-map": "^0.6.0" 51 | } 52 | }, 53 | "ts-node": { 54 | "version": "8.9.0", 55 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.0.tgz", 56 | "integrity": "sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w==", 57 | "dev": true, 58 | "requires": { 59 | "arg": "^4.1.0", 60 | "diff": "^4.0.1", 61 | "make-error": "^1.1.1", 62 | "source-map-support": "^0.5.17", 63 | "yn": "3.1.1" 64 | } 65 | }, 66 | "typescript": { 67 | "version": "3.8.3", 68 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 69 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 70 | "dev": true 71 | }, 72 | "yn": { 73 | "version": "3.1.1", 74 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 75 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 76 | "dev": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lsp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-example", 3 | "version": "1.0.0", 4 | "description": "An example application to demo the Liskov Subsitiution Principal", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "keywords": [], 11 | "author": "Darren", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/node": "^13.13.2", 15 | "ts-node": "^8.9.0", 16 | "typescript": "^3.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lsp/src/FileStore.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path' 3 | import StoreLogger from './StoreLogger' 4 | import IStore from './IStore' 5 | 6 | /** 7 | * A class that allows for messages to be stored in 8 | * a local file system 9 | * 10 | * Note this class implements the IStore interface 11 | */ 12 | export default class FileStore implements IStore { 13 | directory: string 14 | logger: StoreLogger 15 | 16 | constructor(_directory: string, _logger: StoreLogger) { 17 | this.directory = _directory; 18 | this.logger = _logger; 19 | } 20 | 21 | public save(id: number, message: string): void { 22 | this.logger.saving(id); 23 | var fileFullName = this.getFileInfo(id); 24 | try { 25 | fs.writeFileSync(fileFullName, message) 26 | } catch (err) { 27 | this.logger.errorSaving(id); 28 | } 29 | this.logger.saved(id) 30 | } 31 | 32 | public read(id: number): string { 33 | this.logger.readingFilestore(id) 34 | var fileFullName = this.getFileInfo(id); 35 | var exists = fs.existsSync(fileFullName); 36 | if(!exists) { 37 | this.logger.didNotFind(id); 38 | return undefined 39 | } 40 | return fs.readFileSync(fileFullName, {encoding: 'ASCII'}); 41 | } 42 | 43 | public getFileInfo(id: number): string { 44 | return path.join(__dirname, this.directory, `${id}.txt`) 45 | } 46 | } -------------------------------------------------------------------------------- /lsp/src/IStore.ts: -------------------------------------------------------------------------------- 1 | export default interface IStore { 2 | save(id: number, message: string): void 3 | read(id: number): string 4 | getFileInfo(id: number): string 5 | } -------------------------------------------------------------------------------- /lsp/src/MessageStore.ts: -------------------------------------------------------------------------------- 1 | import FileStore from "./FileStore"; 2 | import StoreCache from "./StoreCache"; 3 | import StoreLogger from "./StoreLogger"; 4 | import IStore from "./IStore"; 5 | 6 | export default class MessageStore { 7 | store : IStore; 8 | cache: StoreCache; 9 | logger: StoreLogger; 10 | 11 | constructor(directory: string) { 12 | this.logger = new StoreLogger(); 13 | this.store = new FileStore(directory, this.Logger); 14 | this.cache = new StoreCache(this.Logger); 15 | } 16 | 17 | /** 18 | * A getter that returns an instance of logger 19 | * Purpose of this is to be able to extend this class 20 | * and use a different type of logger (that inherits from StoreLogger) 21 | */ 22 | get Logger() { 23 | return this.logger; 24 | } 25 | 26 | /** 27 | * A getter that returns an instance of store 28 | * Purpose of this is to be able to use a different type of store 29 | * that would implement the IStore interface 30 | */ 31 | get Store() { 32 | return this.store; 33 | } 34 | /** 35 | * 36 | * @param id the id of the file to save 37 | * @param message the text message to write to the file 38 | * 39 | * Function writes the file to disk using the id as part 40 | * of the filename. The id is a number and the file name is 41 | * formed as a .txt file using the pattern id.txt. Its saved 42 | * in the relative directory as set in the constructor. 43 | */ 44 | public save (id: number, message: string) { 45 | this.Store.save(id, message); 46 | this.cache.addOrUpdate(id, message); 47 | } 48 | 49 | /** 50 | * 51 | * @param id the id of the message to read 52 | * 53 | * Function asks the cache to fetch the message 54 | * by id and passes an anonymous function that 55 | * fetches the message from the stoere 56 | * if the message is not in the cache. 57 | * 58 | * @returns message string 59 | */ 60 | public read(id: number): string { 61 | var message = this.cache.getOrAdd( 62 | id, () => this.Store.read(id)) 63 | return message 64 | } 65 | } -------------------------------------------------------------------------------- /lsp/src/SqlStore.ts: -------------------------------------------------------------------------------- 1 | import IStore from './IStore' 2 | // Note since deriving the SqlStore as simply an 3 | // extenstion of FileStore does not make sense 4 | // we instead create an interface IStore and use that 5 | // class SqlStore extends FileStore <- this does not make sense 6 | 7 | /** 8 | * A class that allows for messages to be stored in 9 | * a relational database such as Postgres or MySql 10 | * 11 | * Note this class implements the IStore interface 12 | * Note also this is just for demo purpose only! 13 | */ 14 | export default class SqlStore implements IStore { 15 | save(id: number, message: string): void { 16 | // Write to database code would go here 17 | } 18 | read(id: number): string { 19 | // Read from database here 20 | return '' 21 | } 22 | 23 | /** 24 | * Note that we need to throw 'Method not implemented' here 25 | * because in the context of the SqlStore the 'getFileInfo' 26 | * method is not required. 27 | * 28 | * Note: THIS BREAKS LSP!! We will discuss a solution to this later. 29 | */ 30 | getFileInfo(id: number): string { 31 | throw new Error("Method not implemented."); 32 | } 33 | } -------------------------------------------------------------------------------- /lsp/src/StoreCache.ts: -------------------------------------------------------------------------------- 1 | import StoreLogger from './StoreLogger' 2 | 3 | export default class StoreCache { 4 | cache: any; 5 | logger: StoreLogger; 6 | 7 | constructor(_logger: StoreLogger) { 8 | this.cache = {} 9 | this.logger = _logger 10 | } 11 | 12 | public addOrUpdate(id: number, message: string): void { 13 | this.cache[id] = message; 14 | } 15 | 16 | public getOrAdd(id: number, fnStoreRead: any) { 17 | this.logger.readingCache(id) 18 | if(!this.exists(id)) { 19 | // The message does not exist in the cache 20 | this.logger.missingFromCache(id); 21 | // Load the contents from the store using the passed in function 22 | var message = fnStoreRead() 23 | // Save the message to the cache 24 | this.addOrUpdate(id, message); 25 | } 26 | return this.cache[id]; 27 | } 28 | 29 | public exists?(id: number): boolean { 30 | return this.cache.hasOwnProperty(id); 31 | } 32 | } -------------------------------------------------------------------------------- /lsp/src/StoreLogger.ts: -------------------------------------------------------------------------------- 1 | export default class StoreLogger { 2 | public saving(id: number): void { 3 | console.log(`Saving message ${id}.`) 4 | } 5 | public saved(id: number): void { 6 | console.info(`Saved message ${id}.`) 7 | } 8 | public readingFilestore(id: number): void { 9 | console.debug(`Reading message ${id} from FileStore.`) 10 | } 11 | public readingCache(id: number): void { 12 | console.debug(`Reading message ${id} from StoreCache.`) 13 | } 14 | public didNotFind(id: number): void { 15 | console.debug(`No message ${id} found.`) 16 | } 17 | public missingFromCache(id: number): void { 18 | console.debug(`Message ${id} missing from cache.`) 19 | } 20 | public returning(id: number): void { 21 | console.debug(`Returning message ${id}.`) 22 | } 23 | public errorSaving(id: number): void { 24 | console.error(`Error saving message ${id}.`) 25 | } 26 | } -------------------------------------------------------------------------------- /lsp/src/TestExamples.ts: -------------------------------------------------------------------------------- 1 | import MessageStore from './MessageStore'; 2 | import fs from 'fs'; 3 | import path from 'path' 4 | 5 | var dirtest = "./testfiles"; 6 | var dirpath = path.join(__dirname, dirtest) 7 | if(!fs.existsSync(dirpath)){ 8 | fs.mkdirSync(dirpath) 9 | } 10 | 11 | // Test the CustomMessageStore class 12 | console.log("** Test the CustomMessageStore class **") 13 | console.log() 14 | 15 | var messagestore = new MessageStore(dirtest); 16 | 17 | messagestore.save(99, 'Message 99 saved via MessageStore class') 18 | var fileMessage99 = messagestore.read(99) 19 | console.log(fileMessage99) 20 | var fileMessage33 = messagestore.read(33) 21 | console.log(fileMessage33) 22 | messagestore.save(33, 'Message 33 saved via MessageStore class') 23 | console.log() -------------------------------------------------------------------------------- /lsp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"] 17 | } -------------------------------------------------------------------------------- /ocp/README.md: -------------------------------------------------------------------------------- 1 | # Open Closed Principle 2 | 3 | This is a basic TypeScript application to demo the Open Closed Principle (OCP). 4 | 5 | We are progressing on from the SRP excercise so we are essentially refactoring the code that already exists in [../srp/src/](../srp/src/) folder. Please refer to that code as a starting reference. 6 | 7 | ## What is the Open Closed Principle? 8 | 9 | The **Open Closed Principle (OCP)** states that a class should **be open for extensibility and closed for modification**. 10 | 11 | What this means is that as soon as your class is in the wild and being used by other clients then you should not change its behaviour. However, it should be possible to extend the class so that it's possible to redefine its behaviour. Note, of course, that **bug fixes are allowed to be fixed** and therefore you are required to modify the class directly in this case! 12 | 13 | Naturally, if you break this principle and modify a core class functionality that is already deployed into a production environment and is being used by 3rd party client applications or other parts of your system then this change can have a profound impact on the system and users of that system. 14 | 15 | Note that this principle is based around examples of Inheritance and so therefore the examples will be based on Inheritance. However, **later on we will show how we prefer composition over inheritance** as a general rule. 16 | 17 | ## Example: Extending the behaviour StoreLogger Class 18 | 19 | We could use inheritance to redefine one of our classes, for example the [StoreLogger](./src/StoreLogger.ts) class. We could redefine each of the methods with calls to a different type of logger solution and call that the [StoreLoggerSplunk](./src/StoreLoggerSplunk.ts) class since we may want to log to a remote [Splunk](https://www.splunk.com/) application. 20 | 21 | However, one thing to note is that in the [MessageStore](./src/MessageStore.ts) class we are creating an instance of the [StoreLogger](./src/StoreLogger.ts) class and we cannot change that to [StoreLoggerSplunk](./src/StoreLoggerSplunk.ts) class (because the MessageStore class _is closed for modification_). So what is the solution in this case? 22 | 23 | One solution when using inheritance based extensibility then we could change the class using a _Factory Method_. A Factory Method is a design pattern that is used to create a new instance of polymorphic class. 24 | 25 | So the solution is to provide a new _getter method_ in the [MessageStore](./src/MessageStore.ts) class that can be extended if the client wishes to use the new [StoreLoggerSplunk](./src/StoreLoggerSplunk.ts) class if they so desire. We also have to update calls to the logger from `this.logger` to `this.Logger` (using the new method name defined). 26 | 27 | So the getter method in the MessageStore class looks like this: 28 | 29 | ```ts 30 | get Logger() { 31 | return this.logger; 32 | } 33 | ``` 34 | 35 | ...and we also change any calls to `this.logger` to `this.Logger` (note the upper case L so that we end up calling the getter method). In our MessageStore class we only call this in the constructor so its easy to change. 36 | 37 | We then create a hypothetical [StoreLoggerSplunk](./src/StoreLoggerSplunk.ts) class that simply extends StoreLogger and routes all calls to (a fake) Splunk endpoint. Here is a sample of that code: 38 | 39 | ```ts 40 | import StoreLogger from './StoreLogger' 41 | 42 | export default class StoreLoggerSplunk extends StoreLogger { 43 | public saving(id: number): void { 44 | this.SplunkLogger(`Saving message ${id}.`) 45 | } 46 | 47 | // etc 48 | 49 | private SplunkLogger(log: string) { 50 | console.log("Logged to Splunk: ", log); 51 | } 52 | } 53 | ``` 54 | 55 | To use this new type of logger we also need to extend the MessageStore and then redefine the getter method defined earlier to fetch the logger like so: 56 | 57 | ```ts 58 | export default class CustomMessageStore extends MessageStore { 59 | get Logger() { 60 | return new StoreLoggerSplunk() 61 | } 62 | } 63 | ``` 64 | 65 | Now we can use this new class and it will behave exactly as before with just one change - it now logs to Splunk! Take a look at [TestExamples](./src/TestExamples.ts) for an example on how to do this. 66 | 67 | ## Running the application 68 | 69 | Install the dependences. Note this includes `node-ts` which allows to run TypeScript files without having to compile first. 70 | 71 | **NOTE:** Make sure you change your directory into the [ocp](./ocp) directory first! 72 | 73 | ``` 74 | npm install 75 | ``` 76 | 77 | Now, it should be possible to run the application using the following commands. 78 | 79 | ``` 80 | npx ts-node src/TestExamples.ts 81 | ``` 82 | 83 | Alternatively, you can compile the TypeScript files and then run the output JavaScript files form the resuling `dist` folder: 84 | 85 | ``` 86 | npm run build 87 | node dist/TestExamples.js 88 | ``` -------------------------------------------------------------------------------- /ocp/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocp-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "13.13.2", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz", 10 | "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==", 11 | "dev": true 12 | }, 13 | "arg": { 14 | "version": "4.1.3", 15 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 16 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 17 | "dev": true 18 | }, 19 | "buffer-from": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 22 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 23 | "dev": true 24 | }, 25 | "diff": { 26 | "version": "4.0.2", 27 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 28 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 29 | "dev": true 30 | }, 31 | "make-error": { 32 | "version": "1.3.6", 33 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 34 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 35 | "dev": true 36 | }, 37 | "source-map": { 38 | "version": "0.6.1", 39 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 40 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 41 | "dev": true 42 | }, 43 | "source-map-support": { 44 | "version": "0.5.19", 45 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 46 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 47 | "dev": true, 48 | "requires": { 49 | "buffer-from": "^1.0.0", 50 | "source-map": "^0.6.0" 51 | } 52 | }, 53 | "ts-node": { 54 | "version": "8.9.0", 55 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.0.tgz", 56 | "integrity": "sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w==", 57 | "dev": true, 58 | "requires": { 59 | "arg": "^4.1.0", 60 | "diff": "^4.0.1", 61 | "make-error": "^1.1.1", 62 | "source-map-support": "^0.5.17", 63 | "yn": "3.1.1" 64 | } 65 | }, 66 | "typescript": { 67 | "version": "3.8.3", 68 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 69 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 70 | "dev": true 71 | }, 72 | "yn": { 73 | "version": "3.1.1", 74 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 75 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 76 | "dev": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ocp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocp-example", 3 | "version": "1.0.0", 4 | "description": "An example application to demo the Open Closed Principal", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "keywords": [], 11 | "author": "Darren Jensen", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/node": "^13.13.2", 15 | "ts-node": "^8.9.0", 16 | "typescript": "^3.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ocp/src/CustomMessageStore.ts: -------------------------------------------------------------------------------- 1 | import MessageStore from "./MessageStore"; 2 | import StoreLoggerSplunk from "./StoreLoggerSplunk"; 3 | 4 | export default class CustomMessageStore extends MessageStore { 5 | get Logger() { 6 | return new StoreLoggerSplunk() 7 | } 8 | } -------------------------------------------------------------------------------- /ocp/src/FileStore.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'fs'; 2 | import fs from 'fs'; 3 | import path from 'path' 4 | import StoreLogger from './StoreLogger' 5 | 6 | export default class FileStore { 7 | directory: string 8 | logger: StoreLogger 9 | 10 | constructor(_directory: string, _logger: StoreLogger) { 11 | this.directory = _directory; 12 | this.logger = _logger; 13 | } 14 | 15 | public async save(id: number, message: string): Promise { 16 | this.logger.saving(id); 17 | var fileFullName = this.getFileInfo(id); 18 | await fsp.writeFile(fileFullName, message) 19 | .then(() => this.logger.saved(id)) 20 | .catch((err: any) => this.logger.errorSaving(id)) 21 | } 22 | 23 | public read(id: number): string { 24 | this.logger.readingFilestore(id) 25 | var fileFullName = this.getFileInfo(id); 26 | var exists = fs.existsSync(fileFullName); 27 | if(!exists) { 28 | this.logger.didNotFind(id); 29 | return '' 30 | } 31 | return fs.readFileSync(fileFullName, {encoding: 'ASCII'}); 32 | } 33 | 34 | public getFileInfo(id: number) { 35 | return path.join(__dirname, this.directory, `${id}.txt`) 36 | } 37 | } -------------------------------------------------------------------------------- /ocp/src/MessageStore.ts: -------------------------------------------------------------------------------- 1 | import FileStore from "./FileStore"; 2 | import StoreCache from "./StoreCache"; 3 | import StoreLogger from "./StoreLogger"; 4 | 5 | export default class MessageStore { 6 | filestore : FileStore; 7 | cache: StoreCache; 8 | logger: StoreLogger; 9 | 10 | constructor(directory: string) { 11 | this.logger = new StoreLogger(); 12 | this.filestore = new FileStore(directory, this.Logger); 13 | this.cache = new StoreCache(this.Logger); 14 | } 15 | 16 | /** 17 | * A getter that returns an instance of logger 18 | * Purpose of this is to be able to extend this class 19 | * and use a different type of logger (that inherits from StoreLogger) 20 | */ 21 | get Logger() { 22 | return this.logger; 23 | } 24 | 25 | /** 26 | * 27 | * @param id the id of the file to save 28 | * @param message the text message to write to the file 29 | * 30 | * Function writes the file to disk using the id as part 31 | * of the filename. The id is a number and the file name is 32 | * formed as a .txt file using the pattern id.txt. Its saved 33 | * in the relative directory as set in the constructor. 34 | */ 35 | public async save (id: number, message: string) { 36 | await this.filestore.save(id, message); 37 | this.cache.addOrUpdate(id, message); 38 | } 39 | 40 | /** 41 | * 42 | * @param id the id of the file to read 43 | * 44 | * Function checks if the file exists and 45 | * if not returns an empty string. 46 | * If the file does exist then the function 47 | * checks if the file id is in the cache and 48 | * if not will read the contents of the file 49 | * from disk and add to the cache. 50 | * 51 | * @returns message string 52 | */ 53 | public read(id: number): string { 54 | if(!this.cache.exists(id)) { 55 | // Does not exist in cache so read from file 56 | var message = this.filestore.read(id) 57 | this.cache.getOrAdd(id, message) 58 | } 59 | return this.cache.getOrAdd(id) 60 | } 61 | } -------------------------------------------------------------------------------- /ocp/src/StoreCache.ts: -------------------------------------------------------------------------------- 1 | import StoreLogger from './StoreLogger' 2 | 3 | export default class StoreCache { 4 | cache: any; 5 | logger: StoreLogger; 6 | 7 | constructor(_logger: StoreLogger) { 8 | this.cache = {} 9 | this.logger = _logger 10 | } 11 | 12 | public addOrUpdate(id: number, message: string): void { 13 | this.cache[id] = message; 14 | } 15 | 16 | public getOrAdd(id: number, message?: string): string { 17 | this.logger.readingCache(id) 18 | if(!this.exists(id)) { 19 | if(message===undefined) { 20 | throw new Error("Message expected when file does not exist"); 21 | } 22 | this.logger.missingFromCache(id); 23 | // Save the file contents to the cache 24 | this.addOrUpdate(id, message); 25 | } 26 | return this.cache[id]; 27 | } 28 | 29 | public exists?(id: number): boolean { 30 | return this.cache.hasOwnProperty(id); 31 | } 32 | 33 | public checkCache(): object { 34 | return this.cache; 35 | } 36 | } -------------------------------------------------------------------------------- /ocp/src/StoreLogger.ts: -------------------------------------------------------------------------------- 1 | export default class StoreLogger { 2 | public saving(id: number): void { 3 | console.log(`Saving message ${id}.`) 4 | } 5 | public saved(id: number): void { 6 | console.info(`Saved message ${id}.`) 7 | } 8 | public readingFilestore(id: number): void { 9 | console.debug(`Reading message ${id} from FileStore.`) 10 | } 11 | public readingCache(id: number): void { 12 | console.debug(`Reading message ${id} from StoreCache.`) 13 | } 14 | public didNotFind(id: number): void { 15 | console.debug(`No message ${id} found.`) 16 | } 17 | public missingFromCache(id: number): void { 18 | console.debug(`Message ${id} missing from cache.`) 19 | } 20 | public returning(id: number): void { 21 | console.debug(`Returning message ${id}.`) 22 | } 23 | public errorSaving(id: number): void { 24 | console.debug(`Error saving message ${id}.`) 25 | } 26 | } -------------------------------------------------------------------------------- /ocp/src/StoreLoggerSplunk.ts: -------------------------------------------------------------------------------- 1 | import StoreLogger from './StoreLogger' 2 | 3 | export default class StoreLoggerSplunk extends StoreLogger { 4 | public saving(id: number): void { 5 | this.SplunkLogger(`Saving message ${id}.`) 6 | } 7 | public saved(id: number): void { 8 | this.SplunkLogger(`Saved message ${id}.`) 9 | } 10 | public readingFilestore(id: number): void { 11 | this.SplunkLogger(`Reading message ${id} from FileStore.`) 12 | } 13 | public readingCache(id: number): void { 14 | this.SplunkLogger(`Reading message ${id} from StoreCache.`) 15 | } 16 | public didNotFind(id: number): void { 17 | this.SplunkLogger(`No message ${id} found.`) 18 | } 19 | public missingFromCache(id: number): void { 20 | this.SplunkLogger(`Message ${id} missing from cache.`) 21 | } 22 | public returning(id: number): void { 23 | this.SplunkLogger(`Returning message ${id}.`) 24 | } 25 | public errorSaving(id: number): void { 26 | this.SplunkLogger(`Error saving message ${id}.`) 27 | } 28 | // Private method to return a hypothetical SplunkLogger 29 | private SplunkLogger(log: string) { 30 | console.log("Logged to Splunk: ", log); 31 | } 32 | } -------------------------------------------------------------------------------- /ocp/src/TestExamples.ts: -------------------------------------------------------------------------------- 1 | import MessageStore from './MessageStore'; 2 | import CustomMessageStore from './CustomMessageStore'; 3 | import fs from 'fs'; 4 | import path from 'path' 5 | 6 | var dirtest = "./testfiles"; 7 | var dirpath = path.join(__dirname, dirtest) 8 | if(!fs.existsSync(dirpath)){ 9 | fs.mkdirSync(dirpath) 10 | } 11 | 12 | (async () => { 13 | // Test the CustomMessageStore class 14 | console.log("** Test the CustomMessageStore class **") 15 | console.log() 16 | 17 | var messagestore = new CustomMessageStore(dirtest); 18 | 19 | // Note: Simply comment out the above line and uncomment the 20 | // line below and we are back to our orginal MessageStore 21 | // that does not log to Splunk! 22 | 23 | // var messagestore = new MessageStore(dirtest); 24 | 25 | await messagestore.save(99, 'Message 99 saved via MessageStore class') 26 | var fileMessage99 = messagestore.read(99) 27 | console.log(fileMessage99) 28 | console.log() 29 | })(); -------------------------------------------------------------------------------- /ocp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"] 17 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /srp/README.md: -------------------------------------------------------------------------------- 1 | # Single Responsibility Principle 2 | 3 | This is a basic TypeScript application to demo the Single Responsibility Principle (SRP). 4 | 5 | ## What is the Single Responsibility Principle? 6 | 7 | The **Single Responsibility Principle (SRP)** defines that a class should have a single responsibility. However, what is that responsibility supposed to be defined as? 8 | 9 | A definition of responsibility is to have a reason to change. So, therefore, a class should only have one reason to change. This is fundamentally based on the need to separate the concerns. So if we have a need in our application for logging or caching or storing or whatever then these concerns need to be separated and written into separate classes each with their own SRP. 10 | 11 | Another way of putting this would be to say **each class should do one thing and do it well**. The UNIX operating system is built on this principle and has thrived on it. UNIX has a number of CLIs / command line applications that do one thing very well, like `grep` or `sed` and these can be composed into applications or scripts or combined using pipes (|) very easily to make larger, more complex systems. 12 | 13 | ## Reasons for Change 14 | 15 | What is the reason for the [FileStore](../start/src/FileStore.ts) class to change (Answer is below!): 16 | 17 | The answers are: 18 | 19 | * logging 20 | * caching 21 | * storage 22 | * orchestration 23 | 24 | ## Applying the SRP based on the reasons for change 25 | 26 | What we do is take each one of the reasons for change listed above and extract into a separate class. 27 | 28 | So firstly, let's extract all _logging logic_ into a new class called [StoreLogger](./src/StoreLogger.ts) like so. Note that it is domain specific for our Store and not just a generic Logger. 29 | 30 | What we have done is extracted all the various log calls to a new class. Now this means that if we want to change the logging framework that we use - perhaps because the open source project that built the logger framework becomes depreciated or we find a better solution well now we can change in just this one place. 31 | 32 | To use this new logger, we create an instance of it in the [MessageStore](./src/MessageStore.ts) class - perviously called FileStore (I'll explain why the name was changed below). 33 | 34 | Now we replace the calls to `console.xyz(...)` to `this.log.xyz(...)` for example: 35 | 36 | * Replace: `console.log(“some message: ”, id)` 37 | * With: `this.log.Saving(id)` 38 | 39 | The next thing we need to extract is the logic for caching which can be applied in pretty much the same way. We create a new class [StoreCache](./src/StoreCache.ts). 40 | 41 | Then we need to create an instance of this class in our MessageStore class and call that instead through the implementation. 42 | 43 | ```ts 44 | this.cache = new StoreCache(); // in the MessageStore constructor 45 | --- 46 | this.cache.AddOrUpdate(id, message); // in the Save method 47 | ``` 48 | 49 | Next reason to change that we can address is the way that we apply storage. This will allow us to change where we save files - perhaps to a relational database instead of a filestore - who knows! So what we do is create a separate class called FileSore that is just for reading / writing files to the filestore and use that in our [MessageStore](./src/MessageStore.ts) class in the same way as before. 50 | 51 | So what are we left with? A better implementation for this class that is now split into other classes each with a Single Responsibility! 52 | 53 | ## Running the application 54 | 55 | Install the dependences. Note this includes `node-ts` which allows to run TypeScript files without having to compile first. 56 | 57 | **NOTE:** Make sure you change your directory into the [srp](./srp) directory first! 58 | 59 | ``` 60 | npm install 61 | ``` 62 | 63 | Now, it should be possible to run the application using the following commands. Note this is the _first step_ that we are taking to refactor the original file which is in the root of this project which is the file [FileStore.ts](../FileStore.ts). 64 | 65 | ``` 66 | ts-node src/TestExamples.ts 67 | ``` 68 | 69 | Alternatively, you can compile the TypeScript files and then run the output JavaScript files form the resuling `dist` folder: 70 | 71 | ``` 72 | npm run build 73 | node dist/TestExamples.js 74 | ``` -------------------------------------------------------------------------------- /srp/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srp-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "13.13.2", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz", 10 | "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==", 11 | "dev": true 12 | }, 13 | "arg": { 14 | "version": "4.1.3", 15 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 16 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 17 | "dev": true 18 | }, 19 | "buffer-from": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 22 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 23 | "dev": true 24 | }, 25 | "diff": { 26 | "version": "4.0.2", 27 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 28 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 29 | "dev": true 30 | }, 31 | "make-error": { 32 | "version": "1.3.6", 33 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 34 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 35 | "dev": true 36 | }, 37 | "source-map": { 38 | "version": "0.6.1", 39 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 40 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 41 | "dev": true 42 | }, 43 | "source-map-support": { 44 | "version": "0.5.18", 45 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.18.tgz", 46 | "integrity": "sha512-9luZr/BZ2QeU6tO2uG8N2aZpVSli4TSAOAqFOyTO51AJcD9P99c0K1h6dD6r6qo5dyT44BR5exweOaLLeldTkQ==", 47 | "dev": true, 48 | "requires": { 49 | "buffer-from": "^1.0.0", 50 | "source-map": "^0.6.0" 51 | } 52 | }, 53 | "ts-node": { 54 | "version": "8.9.0", 55 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.0.tgz", 56 | "integrity": "sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w==", 57 | "dev": true, 58 | "requires": { 59 | "arg": "^4.1.0", 60 | "diff": "^4.0.1", 61 | "make-error": "^1.1.1", 62 | "source-map-support": "^0.5.17", 63 | "yn": "3.1.1" 64 | } 65 | }, 66 | "typescript": { 67 | "version": "3.8.3", 68 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 69 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 70 | "dev": true 71 | }, 72 | "yn": { 73 | "version": "3.1.1", 74 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 75 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 76 | "dev": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /srp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srp-example", 3 | "version": "1.0.0", 4 | "description": "An example application to demo the Single Responsibility Principal", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "keywords": [], 11 | "author": "Darren Jensen", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/node": "^13.13.2", 15 | "ts-node": "^8.9.0", 16 | "typescript": "^3.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /srp/src/FileStore.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'fs'; 2 | import fs from 'fs'; 3 | import path from 'path' 4 | import StoreLogger from './StoreLogger' 5 | 6 | export default class FileStore { 7 | directory: string 8 | logger: StoreLogger 9 | 10 | constructor(_directory: string, _logger: StoreLogger) { 11 | this.directory = _directory; 12 | this.logger = _logger; 13 | } 14 | 15 | public async save(id: number, message: string): Promise { 16 | this.logger.saving(id); 17 | var fileFullName = this.getFileInfo(id); 18 | await fsp.writeFile(fileFullName, message) 19 | .then(() => this.logger.saved(id)) 20 | .catch((err: any) => this.logger.errorSaving(id)) 21 | } 22 | 23 | public read(id: number): string { 24 | this.logger.readingFilestore(id) 25 | var fileFullName = this.getFileInfo(id); 26 | var exists = fs.existsSync(fileFullName); 27 | if(!exists) { 28 | this.logger.didNotFind(id); 29 | return '' 30 | } 31 | return fs.readFileSync(fileFullName, {encoding: 'ASCII'}); 32 | } 33 | 34 | public getFileInfo(id: number) { 35 | return path.join(__dirname, this.directory, `${id}.txt`) 36 | } 37 | } -------------------------------------------------------------------------------- /srp/src/MessageStore.ts: -------------------------------------------------------------------------------- 1 | import FileStore from "./FileStore"; 2 | import StoreCache from "./StoreCache"; 3 | import StoreLogger from "./StoreLogger"; 4 | 5 | export default class MessageStore { 6 | filestore : FileStore; 7 | cache: StoreCache; 8 | logger: StoreLogger; 9 | 10 | constructor(directory: string) { 11 | this.logger = new StoreLogger(); 12 | this.filestore = new FileStore(directory, this.logger); 13 | this.cache = new StoreCache(this.logger); 14 | } 15 | 16 | /** 17 | * 18 | * @param id the id of the file to save 19 | * @param message the text message to write to the file 20 | * 21 | * Function writes the file to disk using the id as part 22 | * of the filename. The id is a number and the file name is 23 | * formed as a .txt file using the pattern id.txt. Its saved 24 | * in the relative directory as set in the constructor. 25 | */ 26 | public async save (id: number, message: string) { 27 | await this.filestore.save(id, message); 28 | this.cache.addOrUpdate(id, message); 29 | } 30 | 31 | /** 32 | * 33 | * @param id the id of the file to read 34 | * 35 | * Function checks if the file exists and 36 | * if not returns an empty string. 37 | * If the file does exist then the function 38 | * checks if the file id is in the cache and 39 | * if not will read the contents of the file 40 | * from disk and add to the cache. 41 | * 42 | * @returns message string 43 | */ 44 | public read(id: number): string { 45 | if(!this.cache.exists(id)) { 46 | // Does not exist in cache so read from file 47 | var message = this.filestore.read(id) 48 | this.cache.getOrAdd(id, message) 49 | } 50 | return this.cache.getOrAdd(id) 51 | } 52 | } -------------------------------------------------------------------------------- /srp/src/StoreCache.ts: -------------------------------------------------------------------------------- 1 | import StoreLogger from './StoreLogger' 2 | 3 | export default class StoreCache { 4 | cache: any; 5 | logger: StoreLogger; 6 | 7 | constructor(_logger: StoreLogger) { 8 | this.cache = {} 9 | this.logger = _logger 10 | } 11 | 12 | public addOrUpdate(id: number, message: string): void { 13 | this.cache[id] = message; 14 | } 15 | 16 | public getOrAdd(id: number, message?: string): string { 17 | this.logger.readingCache(id) 18 | if(!this.exists(id)) { 19 | if(message===undefined) { 20 | throw new Error("Message expected when file does not exist"); 21 | } 22 | this.logger.missingFromCache(id); 23 | // Save the file contents to the cache 24 | this.addOrUpdate(id, message); 25 | } 26 | return this.cache[id]; 27 | } 28 | 29 | public exists?(id: number): boolean { 30 | return this.cache.hasOwnProperty(id); 31 | } 32 | 33 | public checkCache(): object { 34 | return this.cache; 35 | } 36 | } -------------------------------------------------------------------------------- /srp/src/StoreLogger.ts: -------------------------------------------------------------------------------- 1 | export default class StoreLogger { 2 | public saving(id: number): void { 3 | console.log(`Saving message ${id}.`) 4 | } 5 | public saved(id: number): void { 6 | console.info(`Saved message ${id}.`) 7 | } 8 | public readingFilestore(id: number): void { 9 | console.debug(`Reading message ${id} from FileStore.`) 10 | } 11 | public readingCache(id: number): void { 12 | console.debug(`Reading message ${id} from StoreCache.`) 13 | } 14 | public didNotFind(id: number): void { 15 | console.debug(`No message ${id} found.`) 16 | } 17 | public missingFromCache(id: number): void { 18 | console.debug(`Message ${id} missing from cache.`) 19 | } 20 | public returning(id: number): void { 21 | console.debug(`Returning message ${id}.`) 22 | } 23 | public errorSaving(id: number): void { 24 | console.debug(`Error saving message ${id}.`) 25 | } 26 | } -------------------------------------------------------------------------------- /srp/src/TestExamples.ts: -------------------------------------------------------------------------------- 1 | import StoreLogger from './StoreLogger' 2 | import StoreCache from './StoreCache' 3 | import FileStore from './FileStore' 4 | import MessageStore from './MessageStore'; 5 | import fs from 'fs'; 6 | import path from 'path' 7 | 8 | var dirtest = "./testfiles"; 9 | var dirpath = path.join(__dirname, dirtest) 10 | if(!fs.existsSync(dirpath)){ 11 | fs.mkdirSync(dirpath) 12 | } 13 | 14 | // Test the StoreLogger class 15 | console.log("** Test the StoreLogger class **") 16 | console.log() 17 | var logger = new StoreLogger() 18 | logger.saving(1); 19 | logger.saved(1); 20 | logger.readingFilestore(1); 21 | logger.didNotFind(1); 22 | logger.readingCache(1); 23 | console.log() 24 | 25 | // Test the StoreCache class 26 | console.log("** Test the StoreCache class **") 27 | console.log() 28 | var cache = new StoreCache(logger); 29 | cache.addOrUpdate(1, 'Message 1') 30 | console.log(cache.checkCache()) // Should have { '1': 'Message 1'} 31 | var message1 = cache.getOrAdd(1); 32 | console.log(message1) // Should be 'Message 1' 33 | var exists2 = cache.exists(2) 34 | console.log("Message 2 Exists?", exists2) 35 | var message2 = cache.getOrAdd(2, "Message 2"); 36 | console.log(); 37 | 38 | (async () => { 39 | // Test the FileStore class 40 | console.log("** Test the FileStore class **") 41 | console.log() 42 | var filestore = new FileStore(dirtest, logger) 43 | var fileInfo = filestore.getFileInfo(1) 44 | console.log(fileInfo); 45 | await filestore.save(1, 'Message File 1') 46 | var fileMessage1 = filestore.read(1) 47 | console.log(fileMessage1) 48 | var fileMessage2 = filestore.read(2) 49 | console.log(fileMessage2) 50 | await filestore.save(2, 'Message File 2') 51 | var fileMessage2 = filestore.read(2) 52 | console.log(fileMessage2) 53 | console.log() 54 | 55 | // Test the MessageStore class 56 | console.log("** Test the MessageStore class **") 57 | console.log() 58 | var messagestore = new MessageStore(dirtest); 59 | await messagestore.save(99, 'Message 99 saved via MessageStore class') 60 | var fileMessage99 = messagestore.read(99) 61 | console.log(fileMessage99) 62 | console.log() 63 | })(); -------------------------------------------------------------------------------- /srp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"] 17 | } -------------------------------------------------------------------------------- /start/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "start-solid-here", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "13.13.4", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz", 10 | "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==", 11 | "dev": true 12 | }, 13 | "arg": { 14 | "version": "4.1.3", 15 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 16 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 17 | "dev": true 18 | }, 19 | "buffer-from": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 22 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 23 | "dev": true 24 | }, 25 | "diff": { 26 | "version": "4.0.2", 27 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 28 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 29 | "dev": true 30 | }, 31 | "make-error": { 32 | "version": "1.3.6", 33 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 34 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 35 | "dev": true 36 | }, 37 | "source-map": { 38 | "version": "0.6.1", 39 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 40 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 41 | "dev": true 42 | }, 43 | "source-map-support": { 44 | "version": "0.5.19", 45 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 46 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 47 | "dev": true, 48 | "requires": { 49 | "buffer-from": "^1.0.0", 50 | "source-map": "^0.6.0" 51 | } 52 | }, 53 | "ts-node": { 54 | "version": "8.9.1", 55 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.1.tgz", 56 | "integrity": "sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==", 57 | "dev": true, 58 | "requires": { 59 | "arg": "^4.1.0", 60 | "diff": "^4.0.1", 61 | "make-error": "^1.1.1", 62 | "source-map-support": "^0.5.17", 63 | "yn": "3.1.1" 64 | } 65 | }, 66 | "typescript": { 67 | "version": "3.8.3", 68 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 69 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 70 | "dev": true 71 | }, 72 | "yn": { 73 | "version": "3.1.1", 74 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 75 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 76 | "dev": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "start-solid-here", 3 | "version": "1.0.0", 4 | "description": "An example application to demo of SOLID", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "keywords": [], 11 | "author": "Darren Jensen", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/node": "^13.13.2", 15 | "ts-node": "^8.9.0", 16 | "typescript": "^3.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /start/src/FileStore.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'fs'; 2 | import fs from 'fs'; 3 | import path from 'path' 4 | 5 | export default class FileStore { 6 | directory: string; 7 | cache: any; 8 | 9 | /** 10 | * 11 | * @param _directory the directory where to save the file to 12 | * 13 | * This is the constructor of the FileStore Class 14 | * It sets the directory to use for saving the file 15 | * and also resets the cache object 16 | */ 17 | constructor(public _directory: string) { 18 | this.directory = _directory; 19 | this.cache = {}; 20 | } 21 | 22 | /** 23 | * 24 | * @param id the id of the file to save 25 | * @param message the text message to write to the file 26 | * 27 | * Function writes the file to disk using the id as part 28 | * of the filename. The id is a number and the file name is 29 | * formed as a .txt file using the pattern id.txt. Its saved 30 | * in the relative directory as set in the constructor. 31 | */ 32 | public async save (id: number, message: string) { 33 | console.log("Saving message:", id); 34 | var fileFullName = this.getFileInfo(id) 35 | await fsp.writeFile(fileFullName, message).then(() => { 36 | this.cache[id] = message; 37 | console.log("Message saved:", id); 38 | }).catch((err: any) => console.error('There was an error: ', err)) 39 | } 40 | 41 | /** 42 | * 43 | * @param id the id of the file to read 44 | * 45 | * Function checks if the file exists and 46 | * if not returns an empty string. 47 | * If the file does exist then the function 48 | * checks if the file id is in the cache and 49 | * if not will read the contents of the file 50 | * from disk and add to the cache. 51 | * 52 | * @returns message string 53 | */ 54 | public read(id: number): string { 55 | console.log("Reading message:", id) 56 | var fileFullName = this.getFileInfo(id); 57 | var exists = fs.existsSync(fileFullName); 58 | console.log("File exists: ", exists) 59 | if(!exists) { 60 | console.log(`No message ${id} found`) 61 | return '' 62 | } 63 | // We want to get from the cache or add it to the cache 64 | if(!this.cache.hasOwnProperty(id)) { 65 | console.info(`Message id ${id} not in cache`); 66 | var data = fs.readFileSync(fileFullName, {encoding: 'ASCII'}); 67 | this.cache[id] = data; 68 | } 69 | var message = this.cache[id] 70 | console.log(`Returning message ${id}`) 71 | return message; 72 | } 73 | 74 | // Public getter to check the state of the cache during testing 75 | public checkCache() { 76 | return this.cache; 77 | } 78 | 79 | // Private method to prepare the full file info 80 | private getFileInfo(id: number): string { 81 | return path.join(__dirname, this.directory, `${id}.txt`) 82 | } 83 | } -------------------------------------------------------------------------------- /start/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"] 17 | } --------------------------------------------------------------------------------