Skip Navigation
72 comments
  •  undefined
        
    async function createUser(user) {
        validateUserInput(user) || throwError(err.userValidationFailed);
        isPasswordValid(user.password) || throwError(err.invalidPassword);
        !(await userService.getUserByEmail(user.email)) || throwError(err.userExists);
    
        user.password = await hashPassword(user.password);
        return userService.create(user);
    }
    
      

    Or

     undefined
        
    async function createUser(user) {
        return await (new UserService(user))
            .validate()
            .create();
    }
    
    // elsewhere…
    const UserService = class {
        #user;
    
        constructor(user) {
            this.user = user;
        }
    
        async validate() {
            InputValidator.valid(this.user);
    
           PasswordValidator.valid(this.user.password);
    
            !(await UserUniqueValidator.valid(this.user.email);
    
            return this;
        }
    
        async create() {
            this.user.password = await hashPassword(this.user.password);
    
            return userService.create(this.user);
        }
    }
    
      

    I would argue that the validate routines be their own classes; ie UserInputValidator, UserPasswordValidator, etc. They should conform to a common interface with a valid() method that throws when invalid. (I’m on mobile and typed enough already).

    “Self-documenting” does not mean “write less code”. In fact, it means the opposite; it means be more verbose. The trick is to find that happy balance where you write just enough code to make it clear what’s going on (that does not mean you write long identifier names (e.g., getUserByEmail(email) vs. getUser(email) or better fetchUser(email)).

    Be consistent:

    1. get* and set* should be reserved for working on an instance of an object
    2. is* or has* for Boolean returns
    3. Methods/functions are verbs because they are actionable; e.g., fetchUser(), validate(), create()
    4. Do not repeat identifiers: e.g., UserService.createUser()
    5. Properties/variables are not verbs; they are state: e.g., valid vs isValid
    6. Especially for JavaScript, everything is const unless you absolutely have to reassign its direct value; I.e., objects and arrays should be const unless you use the assignment operator after initialization
    7. All class methods should be private until it’s needed to be public. It’s easier to make an API public, but near impossible to make it private without compromising backward compatibility.
    8. Don’t be afraid to use if {} statements. Short-circuiting is cutesy and all, but it makes code more complex to read.
    9. Delineate unrelated code with new lines. What I mean is that jamming all your code together into one block makes it difficult to follow (like run-on sentences or massive walls of text). Use new lines and/or {} to create small groups of related code. You’re not penalized for the white space because it gets compiled away anyway.

    There is so much more, but this should be a good primer.

  • Code before:

     js
        
    async function createUser(user) {
        if (!validateUserInput(user)) {
            throw new Error('u105');
        }
    
        const rules = [/[a-z]{1,}/, /[A-Z]{1,}/, /[0-9]{1,}/, /\W{1,}/];
        if (user.password.length >= 8 && rules.every((rule) => rule.test(user.password))) {
            if (await userService.getUserByEmail(user.email)) {
                throw new Error('u212');
            }
        } else {
            throw new Error('u201');
        }
    
        user.password = await hashPassword(user.password);
        return userService.create(user);
    }
    
      

    Here's how I would refac it for my personal readability. I would certainly introduce class types for some concern structuring and not dangling functions, but that'd be the next step and I'm also not too familiar with TypeScript differences to JavaScript.

     js
        
    const passwordRules = [/[a-z]{1,}/, /[A-Z]{1,}/, /[0-9]{1,}/, /\W{1,}/]
    function validatePassword(plainPassword) => plainPassword.length >= 8 && passwordRules.every((rule) => rule.test(plainPassword))
    async function userExists(email) => await userService.getUserByEmail(user.email)
    
    async function createUser(user) {
        // What is validateUserInput? Why does it not validate the password?
        if (!validateUserInput(user)) throw new Error('u105')
        // Why do we check for password before email? I would expect the other way around.
        if (!validatePassword(user.password)) throw new Error('u201')
        if (!userExists(user.email)) throw new Error('u212')
    
        const hashedPassword = await hashPassword(user.password)
        return userService.create({ email: user.email, hashedPassword: hashedPassword });
    }
    
      

    Noteworthy:

    • Contrary to most JS code, [for independent/new code] I use the non-semicolon-ending style following JavaScript Standard Style - see their no semicolons rule with reasoning; I don't actually know whether that's even valid TypeScript, I just fell back into JS
    • I use oneliners for simple check-error-early-returns
    • I commented what was confusing to me
    • I do things like this to fully understand code even if in the end I revert it and whether I implement a fix or not. Committing refacs is also a big part of what I do, but it's not always feasible.
    • I made the different interface to userService.create (a different kind of user object) explicit
    • I named the parameter in validatePassword plainPasswort to make the expectation clear, and in the createUser function more clearly and obviously differentiate between "the passwords"/what password is. (In C# I would use a param label on call validatePassword(plainPassword: user.password) which would make the interface expectation and label transformation from interface to logic clear.

    Structurally, it's not that different from the post suggestion. But it doesn't truth-able value interpretation, and it goes a bit further.

72 comments