TL;DR
JavaScript Getters and Setters can be used to provide custom object properties and enforce business rules. See example here, or in the embed below.
Introduction
Most production applications have at least a few "business rules" (and often times, very many). Enforcing these rules in a client-side application can be challenging and somewhat tedious. I'd like to present one way to enforce such rules using JS getters and setters.
What we will build
To demonstrate this idea, I created a very simple application that revolves around "special rectangles" (I just made this phrase up). In this case, a "special rectangle" is a rectangle that always has the same perimeter (or, distance around the outside of it). So if the width of the rectangle increases, the height has to shrink accordingly. Check out the embed above to get a feel for how the rectangle will behave.
Getters and Setters
Getters and Setters (a.k.a. "Accessors") allow us to define custom object property behaviors.
Getters
MDN defines a "getter" in the following way:
The
get
syntax binds an object property to a function that will be called when that property is looked up.
Basically, this allows you to make a "custom" readable property on an object. Here's a really simple example:
const obj = {
x: 7,
// Double the value of x
get doubleX() {
return 2*this.x;
}
};
console.log(obj.doubleX); // -> 14
obj.x = 12.3;
console.log(obj.doubleX); // -> 23.6
Getters allow us to create "computed" properties with ease. This is wonderful - anytime you update obj.x
in the example above, obj.doubleX
will be "updated" accordingly - and you never have to do the manual updating.
NOTE: getters only affect accessing a property. That is, we can read obj.doubleX
, but at the moment, trying to set this property's value will not work as you might expect.
Setters
MDN defines a setter in the following way:
The set syntax binds an object property to a function to be called when there is an attempt to set that property.
Now, instead of providing behavior for when a property is being read, we provide behavior for when a property is being set. Let's adjust our previous example:
const obj = {
x: 7,
// Double the value of x
get doubleX() {
return 2*this.x;
},
// Setting doubleX: x will be *half* of the value
set doubleX(val) {
this.x = val/2;
}
};
console.log(obj.doubleX); // -> 14
obj.doubleX = 70;
console.log(obj.x); // -> 35
This is really cool stuff! We can create custom properties without having to keep track of excessive amounts of values. This is great for adding custom/computed properties, but it's also great for enforcing business rules!
I like to enforce business rules within setters. That way you can write your rules once, and then just set properties like you normally would. Let's check out an example.
Example: A rectangle with a fixed perimeter
A little scratch work
Before we start writing code, let's make sure we understand our problem space. We want to make a rectangle that has a fixed perimeter, and as the width or height of the rectangle changes - the other dimension will change accordingly. Keep in mind that for any rectangle,
(2 * width) + (2 * height) = perimeter
For reference, here's a diagram representing how the width, height, and perimeter of a rectangle are related.
If we take away the two "width" sides of the rectangle, it leaves us with the two "height" sides. So one "height" side is the perimeter minus two "widths":
height = (perimeter - (2 * width)) / 2
The same goes for the width:
width = (perimeter - (2 * height)) / 2
If we change the width of the rectangle, we need to adjust the height using the first rule above. If we change the height, we set the width using the second rule.
Coding our rectangle rules
We're going to create an ES6 class to apply our new tools and enforce our rules. If you're not familiar with classes in ES6, check out MDN's guide on them. We'll start a file named SpecialRectangle.class.js
to hold this Special Rectangle class.
// Create class
export default class SpecialRectangle {}
For this example, we'll instantiate a SpecialRectangle instance with a perimeter that we want to use as the fixed perimeter of the rectangle, and an initial width. If we know the width, we can determine the corresponding height. Let's do that now.
// Create class
export default class SpecialRectangle {
// Constructor
constructor(perimeter, width) {
// Set the perimeter and width
this.perimeter = perimeter;
this.width = width;
// Set the height using the perimeter and width
this.height = (this.perimeter - 2*this.width)/2;
}
}
Whenever we set the width of the rectangle, we'll update the height accordingly, so let's abstract this out to a method and use it in our constructor.
// Create class
export default class SpecialRectangle {
// Constructor
constructor(perimeter, width) {
// Set the perimeter and width
this.perimeter = perimeter;
// Set the width (which will update the height)
this.setWidth(width);
}
// Set width
setWidth(val) {
this.width = width;
// Set the height using the perimeter and width
this.height = (this.perimeter - 2*this.width)/2;
}
}
Now, let's use getters and setters within our class definition so that we can get/set our width and automatically have these rules enforced. Since we already have a width
property, we'll create a new property named _width
that will "wrap" around the actual width
property. There's nothing special about the name _width
, call it whatever you'd like.
// Create class
export default class SpecialRectangle {
// Constructor
constructor(perimeter, width) {/* ... */}
// Set width
setWidth(val) {/* ... */}
// Get/set the width. Use the helper method we already defined.
get _width() {
return this.width;
}
set _width(val) {
this.setWidth(val);
}
}
Now we can access and "bind to" the _width
property of any SpecialRectangle
instances and automatically have our rules enforced! We can extend this to the height property as well - the logic is just about the same:
// Create class
export default class SpecialRectangle {
// Constructor
constructor(perimeter, width) {/* ... */}
// Set width
setWidth(val) {/* ... */}
// Set the height
setHeight(val) {
this.height = val;
this.width = (this.perimeter - 2*this.height)/2;
}
// Get/set the width. Use the helper method we already defined.
get _width() {/* ... */}
set _width(val) {/* ... */}
// Get/set the width. Use the helper method we already defined.
get _height() {
return this.height;
}
set _height(val) {
this.setHeight(val);
}
}
Alright, this handles the base logic for this class! Now we can use it to create "special rectangles". Here's a simple example:
// Import SpecialRectangle class
// Create rectangle with 600 unit perimeter, initial width of 75 units.
const rect = new SpecialRectangle(600, 75);
// Let's set the width
rect._width = 200;
console.log(rect._height); // -> 100
Adding "bumpers" to our dimensions
The width and height of our rectangle should never be less than 0, and either dimension can be at most half of the total perimeter. Rules like this are very common when doing computations, and therefor I almost always create a utility function that will add "bumpers" to a number - so we never go below a minimum, or above a maximum.
Here's an example of such a function:
// Utility function
const keepBetween = (x, min, max) => {
if (min !== null && x < min) return min;
if (max !== null && x > max) return max;
return x;
};
The logic here is pretty simple: just don't allow x
to be less than min
or more than max
. If x
is between min
and max
, we use the value of x
.
We can use this function when setting values (or even accessing values!) to make sure we don't do mathematically naughty things (like set the width of a rectangle to a negative number). If we factor this into our SpecialRectangle
class, it might look like the following:
/**
* Utility function to keep a number between two other numbers
*/
const keepBetween = (x, min, max) => {
if (min !== null && x < min) return min;
if (max !== null && x > max) return max;
return x;
};
/**
* "SpecialRectangle" class
* - Has a fixed perimeter
*/
export default class SpecialRectangle {
/**
* Instantiate a Photo instance
* @param number perimeter
* @param number width
*/
constructor(perimeter, width) {
// Set the perimeter
this.perimeter = keepBetween(perimeter, 0, null);
// Set the width
this.setWidth(width);
}
/**
* Method to set the width.
* - Width can be at most half of the perimeter
* - Compute height based on what's left
*/
setWidth(val) {
// Set the length. Can be at most half the perimeter
this.width = keepBetween(val, 0, this.perimeter / 2);
// Width is half of what we have left after removing two "lengths" from the perimeter
this.height = keepBetween(
(this.perimeter - 2 * this.width) / 2,
0,
this.perimeter / 2
);
}
/**
* Method to set the height.
* - Works effectively the same as setWidth
*/
setHeight(val) {
// Set the width. Can be at most half the perimeter
this.height = keepBetween(val, 0, this.perimeter / 2);
// Length is half of what we have left after removing two "lengths" from the perimeter
this.width = keepBetween(
(this.perimeter - 2 * this.height) / 2,
0,
this.perimeter / 2
);
}
/**
* Handle getting/setting length
*/
get _width() {
return this.width;
}
set _width(val) {
this.setWidth(val);
}
/**
* Handle getting/setting width
*/
get _height() {
return this.height;
}
set _height(val) {
this.setHeight(val);
}
}
Using our class with Vue
Let's create a really simple user interface using Vue.JS to showcase our new class. We'll create a single component with the following JS:
import SpecialRectangle from "@/assets/SpecialRectangle.class";
export default {
name: "App",
data: () => ({
rect: new SpecialRectangle(100, 10)
})
};
All we're doing is creating an instance of our SpecialRectangle
class that we'll use in our template/markup. Since we have getters and setters for the _width
and _height
properties of our SpecialRectangle
instance, we can use Vue's v-model
directive to bind right to these properties. For example, we can create a slider to control the width of our rectangle:
<input
name="length"
type="range"
:min="0"
:max="rect.perimeter/2"
step="0.1"
v-model="rect._width"
>
The full code is shown in the embed below. Try using the sliders in the result to see it in action!
With this method, we can push our business rules into class definitions. This keeps our Vue logic clean, and allows us to re-use these rules over and over again!
Conclusion
If you've got a big application with a lot of business/data rules, moving your logic out of your UI components and into class definitions (using getters and setters) can keep your codebase cleaner, and make these rules re-usable.
I'm sure there are some downsides to this approach, and I'd love to hear about them! Let me know what you think.
Top comments (0)