Original text: vuejs-course.com/blog/separa…

One of the common things I’ve observed on large Vue and React projects is that UI logic and business logic become entangled and mutually destructive over time. Let’s look at an example of this situation and how to separate these concerns.

Mixed concerns also mean poor test coverage for applications — unit tests always force you to separate business logic from UI logic and otherwise make testing difficult. Even without testing, since Vue and React are designed to build user interfaces rather than encapsulate application logic, it’s important to keep them separate.

In this article, I’ll explore the separation of business logic and UI logic by refactoring a password strength component written by Milad Dehghan. The source code for this component can be found here: github.com/miladd3/vue… .

A basic overview of how this component works is as follows:

<template>
<div class="po-password-strength-bar" :class="passwordClass"></div>
</template>

<script>
export default {
props: {
  password: {
    type: String.required: true}},computed: {
  passwordStrength() {
    if (this.password) {
      return this.checkPassword(this.password)
    }
  },

  passwordClass() {
    return[{scored: this.passwordStrength && this.password },
      {
        risky: this.passwordStrength === 0.// ...
        secure: this.passwordStrength === 4}}},methods: {
  checkPassword(pass) {
    // Logic based on character, length, number
    // Return a number between 0 and 4
    // 0 is the weakest, 4 is the strongest}}}</script>

<style>
.po-password-strength-bar.risky {
background-color: #f95e68;
}

/ *... Other styles */

.po-password-strength-bar.secure {
background-color: #35cc62;
}
</style>
Copy the code

The computed value passwordClass is a UI concern — depending on the numeric return value from the computed property passwordStrength, the different classes are returned so that the associated styles are adopted. If JavaScript had a private method feature, passwordStrength would certainly count as one — it’s basically a utility function that connects the UI to the main business logic contained in checkPassword.

CheckPassword encapsulates all business logic. It defines several regular expressions and applies them to the Password attribute. Calculate a number between 0 and 4, depending on the number of regular expressions matching the password. If we decide to make the logic a little more robust, say like ZXCVBN, just make a change in this method.

For improvement and reconstruction

The component is currently working well and has no apparent problems. But if I wanted to start using this component in a product, there were a few changes besides improving to a more robust estimation algorithm that would give me confidence to move forward. The improvements I will investigate are:

  • The test!
  • Separate UI and business logic

Separating the business logic makes it much easier to achieve one of my other goals, which is to move to a more secure password strength estimation algorithm.

Writing regression tests

Before embarking on any refactoring, I always write some basic regression tests. I wanted to make sure that my changes didn’t break existing functionality. I’ll start by writing tests that revolve around two extremes — an unsafe password (0 points, “Risky and dangerous”) and a secure password (4 points).

import { shallowMount } from '@vue/test-utils'
import SimplePassword from '@/SimplePassword.vue'

const riskyPassword = 'abcdef'
const securePassword = 'abc123ABC? ! '

test('handling a dangerously SimplePassword with SimplePassword'.() = > {
const wrapper = shallowMount(SimplePassword, {
  propsData: {
    password: riskyPassword
  }
})
expect(wrapper.classes()).toContain('risky')
})

test('handle a secure SimplePassword with SimplePassword'.() = > {
const wrapper = shallowMount(SimplePassword, {
  propsData: {
    password: securePassword
  }
})
expect(wrapper.classes()).toContain('secure')})Copy the code

definecheckPasswordinterface

I want SimplePassword to have a minimal public interface. In particular, I don’t want SimplePassword to know anything like a scoring system; it’s enough to know risky, guessable, secure, and so on. Because I’m using TDD for this refactoring, I’ll write the tests first. I’ve only dealt with two extreme cases for the sake of brevity; in a real system you should test all cases.

describe('checkPassword'.() = > {
it('It's a dangerous password.'.() = > {
  const actual = checkPassword(riskyPassword)
  expect(actual).toBe('risky')
})

it('It's a secure password'.() = > {
  const actual = checkPassword(securePassword)
  expect(actual).toBe('secure')})})Copy the code

ReferenceError: checkPassword is not defined. I’ll create a logic.js file in the sibling of Simplepassword.vue and move the checkPassword method from Simplepassword.vue to it.

export function checkPassword(pass) {
/ /... A set of variables declared...

if (pass.length > 4) {
  if ((hasLowerCase || hasUpperCase) && hasNumber) {
    numCharMix = 1;
  }

  if (hasUpperCase && hasLowerCase) {
    caseMix = 1;
  }

  if ((hasLowerCase || hasUpperCase || hasNumber) && hasSpecialChar) {
    specialChar = 1;
  }

  if (pass.length > 8) {
    length = 1;
  }

  if (pass.length > 12 && !hasRepeatChars) {
    length = 2;
  }

  if (pass.length > 25 && !hasRepeatChars) {
    length = 3;
  }

  score = length + specialChar + caseMix + numCharMix;

  if (score > 4) {
    score = 4; }}return score;
}
Copy the code

Now everything will still fail because SimplepassWord.vue has nothing to do with the new checkPassword yet. Update it to:

<script>
import { checkPassword } from './logic'

export default {
name: "password-meter".props: {
  password: String
},
computed: {
  passwordStrength() {
    if (this.password) return checkPassword(this.password);
    return null;
  },
  passwordClass() {
    / /... Several lines are omitted here}}}</script>
Copy the code

I’m happy with the refactoring so far. The only change I made to SimplepassWord.vue was:

// import this
import { checkPassword } from './logic'

passwordStrength() {
// Change 'this.checkPassword' to 'checkPassword'
if (this.password) return checkPassword(this.password);
return null;
}
Copy the code

It may not seem like much, but it’s a big win. CheckPassword is easier to test. At the same time, changing this.checkPassword to checkPassword reflects the decoupling between business logic and UI logic. This refers to a Vue instance or component, so:

Anything attached to this should not be associated with the UI

CheckPassword is also a pure function — there is no global variable that references this, which means it has deterministic output based entirely on its input. This is great for testing, and generally feels great.

updatecheckPasswordinterface

Updating the test to import {checkPassword} from ‘@/logic’ will result in the following error:

Expected: "risky"
Received: 0

Expected: "secure"
Received: 4
Copy the code

Continue updating checkPassword to pass the test. The minimal change is to move passwordClass from a computed to logic.js.

function passwordClass(passwordStrength) {
if (passwordStrength === 0) {
  return 'risky'
}

/ /... Several lines omitted here...

if (passwordStrength === 4) {
  return 'secure'}}Copy the code

Note that we did not export the passwordClass function directly — this reflects my statement at the beginning that passwordClass is a “private function.”

We can now pass the test by updating the return statement in checkPassword to use the new passwordClass method:

export function checkPassword(pass) {
// ... 
return passwordClass(score);
}
Copy the code

Now the checkPassword-related test case has passed. But the component tests still failed. To fix it!

<script>
import { checkPassword } from './logic'

export default {
// ...
computed: {
  passwordClass() {
    if (this.password) {
      const className = checkPassword(this.password);

      return {
        [className]: true.scored: true}}}}};</script>
Copy the code

I passed all the tests.

We no longer need passwordStrength and passwordClass, and a bunch of if statements, because checkPassword defines a clean interface and encapsulates all the business logic. I love this one — checkPassword is now very easy to test, and the code in the Script tag of the SimplePassword component is directly related to the UI. It was also easy to switch password strength algorithms — since we had unit tests, I would know immediately if something was broken.

Refactoring also greatly improved our testing — we had two test cases using Render that only made assertions about CSS classes, and some test cases that only focused on business logic without touching any of SimplePassword’s UI concerns.

conclusion

I have a few other styles THAT I want to improve, and I can now do so with confidence because I already have fairly good test coverage. Separating business logic from UI logic made SimplePassword easier to understand and allowed us to improve test coverage. Refactoring also hides implementation details, mimicking private features that don’t exist in JavaScript.

Separating business logic from UI logic is valuable; Testability and readability are at the top of the list, with minor drawbacks. Although it requires more forethought, what you get is better design and more maintainable code.






–End–






View more front-end good article please search fewelife concern public number reprint please indicate the source