Skip to content

Instantly share code, notes, and snippets.

@reggi
Last active April 9, 2020 22:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save reggi/305fefa87980f7a072047dcf26089c87 to your computer and use it in GitHub Desktop.
Save reggi/305fefa87980f7a072047dcf26089c87 to your computer and use it in GitHub Desktop.

Class Design Discussion

This is a document containg many different examples of how to create the same base code. Each example is different, contains a different API, or different added functionality such as cashing.

Example A

  • No constructor arguments
  • No use of any internal / external caching
  • No use any internal this
  • Uses static methods for organization
class FileLoader {
    path: string | null = null
    cwd: string | null = null

    constructor () {}

    static getFullPath(options: { path: string, cwd: string }) {
        if (nodePath.isAbsolute(options.path)) return options.path
        return nodePath.join(options.cwd, options.path)
    }
    
    static getExtension(options: { path: string, cwd: string }) {
        return nodePath.extname(options.path)
    }
    
    static async getContents (options: { fullPath: string }) {
        return util.promisify(fs.readFile)(options.fullPath, { encoding: 'utf8' })
    }
    
    async main (options: { path: string, cwd: string }) {
        const fullPath = FileLoader1.getFullPath(options)
        const extension = FileLoader1.getExtension(options)
        const contents = FileLoader1.getContents({ fullPath })
        return { ...options, fullPath, extension, contents }
    }
}

const options = { path: 'README.md', cwd: './'}

FileLoader.main(options).then(console.log)

Example B

  • All methods return value
  • All methods don't call each other
  • .main() must call each method in order and assign this values
  • No external use of calling methods directly
class FileLoader1 {
    path: string | null = null
    cwd: string | null = null

    constructor (private readonly options: { path: string, cwd: string }) {
        this.path = this.options.path
        this.cwd = this.options.cwd
    }

    fullPath: string | null = null
    private getFullPath() {
        return nodePath.isAbsolute(this.path) ? this.path : nodePath.join(this.cwd, this.path);
    }
    
    extension: string | null = null
    private getExtension() {
        return nodePath.extname(this.path)
    }
    
    contents: string | null = null
    private async getContents () {
        return util.promisify(fs.readFile)(this.fullPath, { encoding: 'utf8' })
    }
    
    async main () {
        this.fullPath = this.getFullPath()
        this.extension = this.getExtension()
        this.contents = await this.getContents()
        return this
    }
}

Example C

  • All methods must assign value to this and not return
  • .main() must call each method in order
class FileLoader2 {
    path: string | null = null
    cwd: string | null = null

    constructor (private readonly options: { path: string, cwd: string }) {
        this.path = this.options.path
        this.cwd = this.options.cwd
    }

    fullPath: string | null = null
    private getFullPath() {
        this.fullPath = nodePath.isAbsolute(this.path) ? this.path : nodePath.join(this.cwd, this.path);
    }
    
    extension: string | null = null
    private getExtension() {
        this.extension = nodePath.extname(this.path)
    }
    
    contents: string | null = null
    private async getContents () {
        if (!this.fullPath) throw new Error('missing fullPath');
        this.contents = util.promisify(fs.readFile)(this.fullPath, { encoding: 'utf8' })
    }
    
    async main () {
        this.getFullPath()
        this.getExtension()
        await this.getContents()
        return this
    }
}

Example D

  • All methods return value
  • Use of .this for constructor args
  • Directly pass in computed values (eg. this.contents(fullPath))
class FileLoader {
  constructor(private readonly options: {
    cwd: string
    path: string
  }) { }

  fullPath() {
    const { path, cwd } = this.options
    return nodePath.isAbsolute(path) ? path : nodePath.join(cwd, path)
  }

  extention() { 
    const { path } = this.options
    return nodePath.extname(path)
  }

  async contents(fullPath: string) {
    return readFile(fullPath, { encoding: 'utf8' })
  }

  async main() {
    const fullPath = this.fullPath()
    const extension = this.extention()
    const contents = await this.contents(fullPath)
    return {
      ...this.options,
      fullPath,
      extension,
      contents
    }
  }

}

Example E

  • Methods cache value
  • Sibling functions can call upon sibling functions directly because cached
class FileLoader {
  constructor(private readonly options: {
    cwd: string
    path: string
  }) { }

  _fullPath: string | null = null
  fullPath() {
    if (this._fullPath) return this._fullPath
    const { path, cwd } = this.options
    this._fullPath = nodePath.isAbsolute(path) ? path : nodePath.join(cwd, path)
    return this._fullPath
  }

  _extention: string | null = null
  extention() { 
    if (this._extention) return this._extention
    const { path } = this.options
    this._extention = nodePath.extname(path)
    return this._extention
  }

  _contents: string | null = null
  async contents() {
    if (this._contents) return this._contents
    this._contents = await readFile(this.fullPath(), { encoding: 'utf8' })
    return this._contents
  }

  async main() { 
    return {
      ...this.options,
      fullPath: this.fullPath(),
      extention: this.extention(),
      contents: await this.contents(),
    }
  }
}

Example F

  • Use of getters for non async processes (will be problematic for async-to-async dependency)
class FileLoader {
  constructor(private readonly options: {
    cwd: string
    path: string
  }) { }

  get fullPath() {
    const { path, cwd } = this.options
    return nodePath.isAbsolute(path) ? path : nodePath.join(cwd, path)
  }

  get extention() { 
    const { path } = this.options
    return nodePath.extname(path)
  }

  async contents() {
    return readFile(this.fullPath, { encoding: 'utf8' })
  }

  async main() { 
    return {
      ...this.options,
      fullPath: this.fullPath,
      extention: this.extention,
      contents: await this.contents(),
    }
  }
}

Example W

  • Purely functional
const getFullPath = (o: { path: string, cwd: string }) => {
    return nodePath.isAbsolute(o.path) ? o.path : nodePath.join(o.cwd, o.path)
}

const getExtension = (path: string) => {
    return nodePath.extname(path)
}

const getContents = (path: string) => {
    return util.promisify(fs.readFile)(path, { encoding: 'utf8' })
}

const fileLoader = async (o: { path: string, cwd: string }) => {
    const fullPath = getFullPath(o)
    const extension = getExtension(fullPath)
    const contents = await getContents(fullPath)
    return { ...o, fullPath, extension, contents }
}

Example X

  • Each method "private"
  • Each function has no arguments
  • Each method bound with parent this
function fullPath() {
    return nodePath.isAbsolute(this.path) ? this.path : nodePath.join(this.cwd, this.path);
}

function extension() {
    return nodePath.extname(this.path);
}

function contents() {
    return util.promisify(fs.readFile)(this.fullPath, { encoding: 'utf8' });
}

function fileLoader(o: { cwd: string, path: string }) {
    this.cwd = o.cwd
    this.path = o.path
    this.fullPath = fullPath.bind(this)();
    this.extension = extension.bind(this)();
    this.contents = await contents.bind(this)();
    return this;
}

Example W.1

function fullPath() {
    this.fullPath = nodePath.isAbsolute(this.path) ? this.path : nodePath.join(this.cwd, this.path);
}

function extension() {
    this.extension = nodePath.extname(this.path);
}

async function contents() {
    this.contents = await util.promisify(fs.readFile)(this.fullPath, { encoding: 'utf8' });
}

function fileLoader(o: { cwd: string, path: string }) {
    this.cwd = o.cwd
    this.path = o.path
    fullPath.bind(this)();
    extension.bind(this)();
    await contents.bind(this)();
    return this;
}

Example Y

  • Not a class
  • Assign direct values in function
async function fileLoader (o: {
    cwd?: string,
    path?: string
} ) {
    const fullPath = nodePath.isAbsolute(o.path) ? o.path : nodePath.join(o.cwd, o.path)
    const extension = nodePath.extname(o.path)
    const contents = await util.promisify(fs.readFile)(fullPath, { encoding: 'utf8' })
    return {...o, fullPath, extension, contents }
}

Example Z

  • Not a class
  • Single function with iife to encapsulate value
async function fileLoader (o: {
    cwd?: string,
    path?: string
} ) {
    const fullPath = (() => {
        return nodePath.isAbsolute(o.path) ? o.path : nodePath.join(o.cwd, o.path)
    })();

    const extension = (() => {
        return nodePath.extname(o.path)
    })();
    
    const contents = await (async () => {
        return util.promisify(fs.readFile)(fullPath, { encoding: 'utf8' })
    })();

    return {...o, fullPath, extension, contents }
}

Example Middleware

const nodePath = require('path');
const fs = require('fs');

function fullPath(context) {
    console.log('fullPath')
    context.fullPath = nodePath.isAbsolute(context.path) ? context.path : nodePath.join(context.cwd, context.path);
}

function extension(context) {
    console.log('extension')
    context.extension = nodePath.extname(context.path);
}

function contents(context, callback) {
    console.log('contents')
    return fs.readFile(context.fullPath, { encoding: 'utf8' }, (err, contents) => {
        console.log('contents-i')
        if (err) return callback(err);
        context.contents = contents;
        return callback();
    });
}

function middleware(stack) {
    return (context = {}, master) => {
        var hasError = false
        return stack.reduceRight((callback, fn) => {
            if (hasError) return () => {};
            return () => {
                const isAsync = fn.length === 2
                if (isAsync) {
                    fn(context, (err) => {
                        if (err) {
                            hasError = true
                            return master(err);
                        }
                        callback(null, context);
                    })
                } else {
                    fn(context)
                    callback(null, context)
                }
            }
        }, master)()
    }
}

const fileLoader = middleware([fullPath, extension, contents])

fileLoader({ path: './example.md', cwd: '' }, (err, context) => {
    console.log({ err, context })
})

Example Pure Functional Class

const nodePath = require('path');
const fs = require('fs');
const util = require('util');

const getFullPath = (o) => {
    return nodePath.isAbsolute(o.path) ? o.path : nodePath.join(o.cwd, o.path);
};

const getExtension = (path) => {
    return nodePath.extname(path);
};

const getContents = (path) => {
    return util.promisify(fs.readFile)(path, { encoding: 'utf8' });
};

const fileLoader = async (o) => {
    const fullPath = getFullPath(o);
    const extension = getExtension(fullPath);
    const contents = await getContents(fullPath);
    return Object.assign(Object.assign({}, o), { fullPath, extension, contents });
};

class FileLoader {
    constructor(o) {
        this.path = o.path;
        this.cwd = o.cwd;
        this.fullPath = getFullPath(this);
        this.extension = getExtension(this.fullPath);
    }
    async getContents() {
        return getContents(this.fullPath);
    }
    async main() {
        const [contents] = await Promise.all([this.getContents()]);
        return Object.assign(Object.assign({}, this), { contents });
    }
}

new FileLoader({ cwd: '', path: '/Users/thomasreggi/Desktop/example.md' }).main().then(console.log);

Example object with methods

const nodePath = require('path');
const fs = require('fs');
const util = require('util');

const FileLoader = {
    fullPath() {
        this.fullPath = nodePath.isAbsolute(this.path) ? this.path : nodePath.join(this.cwd, this.path);
    },

    extension() {
        this.extension = nodePath.extname(this.path);
    },

    async contents() {
        this.contents = await util.promisify(fs.readFile)(this.fullPath, { encoding: 'utf8' });
    },

    async main(o: { cwd: string, path: string }) {
        this.cwd = o.cwd
        this.path = o.path
        this.fullPath();
        this.extension();
        await this.contents();
        return this;
    }
}

FileLoader.main({ cwd: '', path: '/Users/thomasreggi/Desktop/example.md'}).then(console.log)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment