3
\$\begingroup\$

Lately I've been working on a practice project using JS ES6 to understand it better, therefore I've read a lot about JS design patterns and JS coding strategy. My question is about splitting the functionality of the project so I have to briefly explain to what I'm trying to do in this project.

I have an input that gets a command with a URL of a page. The commands are summarize and crawl. summarize actually summarizes the content of the given URL and crawl extracts the words in the URL.

class App {

    constructor(options) {
        this.email = options.email;
        this.inputElement = options.inputElement;
        this.progressElement = options.progressElement;
        this.infoElement = options.infoElement;
        this.outputElement = options.outputElement;
        this.init();
    }

    init() {
        this.socket = io();
        this.socket.emit('join', {email: this.email});
        this.progress = {
            action: document.querySelector('.progress-action'),
            percentage: document.querySelector('.progress-percentage'),
            bar: document.querySelector('.progress-bar')
        };
        this.bindEvents();
    }

    bindEvents() {
        let that = this;

        this.inputElement.addEventListener('keyup', function (e) {
            if (e.keyCode === 13) {
                let compiledInput = that.tools().compileInput(this.value);
                if (!compiledInput.target) {
                    that.tools().showInfo(that.resources().messages.noURL);
                }
                else if (!compiledInput.command) {
                    that.tools().showInfo(that.resources().messages.noCommand);
                }
                else {
                    that.tools().showInfo(that.resources().messages.syntax);
                    that.progressElement.classList.add('show');
                    that.action()[compiledInput.command](compiledInput);
                }
            }
            else if (this.value.trim() !== '') {
                that.tools().showInfo(that.resources().messages.pressEnter);
            }
            else {
                that.tools().showInfo(that.resources().messages.syntax);
            }
        });

        that.socket.on('crawlProcess', function (resp) {
            that.progress.action.innerHTML = resp.action;
            that.progress.percentage.innerHTML = resp.index + ' Files Crawled - ' + resp.percentage + '%';
            that.progress.bar.style.width = resp.percentage + '%';

            that.outputElement.innerHTML = '';
            for (let i = 0; i < resp.response.length; i++) {
                let fieldKey = Object.keys(resp.response[i])[0];
                let fields = '<span class="word">' + resp.response[i][fieldKey] + '</span>';
                that.outputElement.insertAdjacentHTML('beforeend', fields);
            }

            if (parseInt(resp.percentage) === 100) {
                setTimeout(()=> {
                    that.progressElement.classList.remove('show');
                    if (that.outputElement.innerHTML.trim() === '') {
                        that.tools().showInfo(that.resources().messages.alreadyCrawled);
                    }
                    else {
                        that.outputElement.classList.add('show');
                    }
                }, 500);
            }
        });

        that.socket.on('summarize', function (resp) {
            that.progress.action.innerHTML = resp.action;
            that.progress.percentage.innerHTML = resp.percentage + '%';
            that.progress.bar.style.width = resp.percentage + '%';
            if (parseInt(resp.percentage) === 100) {
                setTimeout(()=> {
                    if (!resp.error) {
                        that.outputElement.innerHTML = resp.response;
                        that.outputElement.classList.add('show');
                    }
                    else {
                        that.outputElement.innerHTML = '';
                        that.outputElement.classList.remove('show');
                        that.tools().showInfo(resp.response.message);
                    }
                    that.progressElement.classList.remove('show');
                }, 500);
            }
        });
    }

    action() {
        let that = this;
        return {
            crawl(options){
                that.socket.emit(options.command, {
                    url: options.target,
                    maxCrawlCount: options.option || 10,
                    includeAbsolute: true
                });
            },
            summarize(options){
                that.socket.emit(options.command, {
                    url: options.target,
                    paragraphs: options.option || 1
                });
            }
        }
    }

    resources() {
        return {
            messages: {
                pressEnter: 'Press enter to go!',
                noURL: 'Please provide a valid target URL to act on!',
                noCommand: 'Command not found!',
                syntax: 'What can I do for you?',
                alreadyCrawled: 'This page is already crawled!'
            },
            commands: ['summarize', 'crawl']
        }
    }

    tools() {
        let that = this;
        return {
            levenshteinDistance(a, b) {
                if (a.length == 0) return b.length;
                if (b.length == 0) return a.length;

                let matrix = [];

                // increment along the first column of each row
                let i;
                for (i = 0; i <= b.length; i++) {
                    matrix[i] = [i];
                }

                // increment each column in the first row
                let j;
                for (j = 0; j <= a.length; j++) {
                    matrix[0][j] = j;
                }

                // Fill in the rest of the matrix
                for (i = 1; i <= b.length; i++) {
                    for (j = 1; j <= a.length; j++) {
                        if (b.charAt(i - 1) == a.charAt(j - 1)) {
                            matrix[i][j] = matrix[i - 1][j - 1];
                        } else {
                            matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
                                Math.min(matrix[i][j - 1] + 1, // insertion
                                    matrix[i - 1][j] + 1)); // deletion
                        }
                    }
                }

                return matrix[b.length][a.length];
            },

            levenshteinPercentage(a, b) {
                return 1 - (1 / (Math.max(a.length, b.length)) * that.tools().levenshteinDistance(a, b));
            },

            compileInput (input) {
                let url = that.tools().extractURL(input),
                    command = that.tools().extractCommand(input.replace(url || '', '')),
                    option = input.match(/ \d+/);

                option = (option) ? parseInt(option[0]) : null;

                return {
                    target: url,
                    command: command,
                    option: option
                };
            },

            extractURL (input) {
                let matchedURLs = input.match(/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/ig);
                return (matchedURLs) ? matchedURLs[0] : null;
            },

            extractCommand (input) {
                let matchedCommands = [];

                input = input.split(' ');

                for (let j = 0; j < that.resources().commands.length; j++) {
                    for (let i = 0; i < input.length; i++) {
                        if (input[i].trim() !== '') {
                            let distance = that.tools().levenshteinPercentage(input[i].toLowerCase(), that.resources().commands[j].toLowerCase());
                            if (distance > .5) {
                                matchedCommands.push({
                                    command: that.resources().commands[j],
                                    distance: distance
                                });
                            }
                        }
                    }
                }

                return that.tools().sortByKeyVal(matchedCommands, 'distance')[0].command;
            },

            sortByKeyVal (arr, key) {
                return arr.sort(function (a, b) {
                    if (a[key] < b[key])
                        return 1;
                    if (a[key] > b[key])
                        return -1;
                    return 0;
                });
            },

            showInfo(message){
                that.infoElement.innerText = message;
            }
        }
    }
}

new App({
    email: '[email protected]',
    inputElement: document.querySelector('.input'),
    progressElement: document.querySelector('.progress'),
    infoElement: document.querySelector('.info'),
    outputElement: document.querySelector('.output')
});

Is this a valid coding strategy to have only one class and wrapping and grouping the functions into methods? Will this make things complicated in larger projects? If so, what are the suggested or best practices coding strategy for projects like my current projects?

\$\endgroup\$

1 Answer 1

2
+100
\$\begingroup\$

tools(), resources() and actions() should all be statically implemented as Objects instead of being a function that returns an Object. Otherwise they will get re-created in memory every time they are being used.

Example:

class App { ... }
App.Tools = { tools code... };
App.Resources = { resources code... };
App.Actions = { actions code };

Taking it a bit further; tools often are called Utilities and may be defined in a separate utilities file/module, that can be referenced and re-used thoughout the application. Especially if the utilities contains commonly used functions.

\$\endgroup\$
7
  • \$\begingroup\$ thanks for your suggestion, let me know if I get the general idea right! public functions and object that we may use throughout the project (in this case Tools and Resources) must be different modules or classes, and the rest of functionalities must be defined as objects bound to their respective class, am I right? \$\endgroup\$ Commented Dec 1, 2016 at 15:18
  • \$\begingroup\$ Yes, if you happend to have code that will get re-used anywhere else in the app, you want to extract it into a separate module/class. Typically you have a Utils object that holds all comon methods, variables, constants etc. \$\endgroup\$
    – Eric
    Commented Dec 1, 2016 at 15:20
  • \$\begingroup\$ thanks again, and one final question if I may, are there any other approaches? in general I mean, just for expanding my knowledge not for this case specifically \$\endgroup\$ Commented Dec 1, 2016 at 15:22
  • \$\begingroup\$ I just tested your suggestion, if I do App.Resources={ resources code... }, after defining the App class, then create a new instance of App lets say var myApp = new App(); then myApp.Resources is undefined, am I missing something? \$\endgroup\$ Commented Dec 1, 2016 at 15:38
  • \$\begingroup\$ There are probably many approaches, but using utility modules is definitely used by 99.99999% of all code. +/- 0.1%. You did define App.Resources as the object you are returning from your current implementation, right? eg. App.Resources = {myConstant:123, myFunction() { return 10; } } and so on \$\endgroup\$
    – Eric
    Commented Dec 1, 2016 at 15:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.