Previous Parts:
Classes
a. Naming
b. Encapsulation
c. Classes instead of prototypes
d. Method chaining
e. Access modifier static
Objects
a. Fields
b. Accessors
a. Naming
When you create a class you already set the context for the class in its name. It will help us to avoid unnecessary context for class methods.
// Bad
class UserRoleServive {
getUserRole() {}
checkUserRole() {}
}
// Better
class UserRoleServive {
getRole() {}
checkRole() {}
}
b. Encapsulation
// Bad
class MakeUserProfile {
constructor() {
this.email = null;
this.phone = null;
/..logic../
}
}
const userProfile = new MakeUserProfile();
userProfile.email = '[email protected]';
In this example, userProfile
fields are opened for modification: we can delete fields, or assign not valid values. It is not safe.
The better way is to encapsulate some class fields and use getters
to get value and setters
to set value. As setter is a method we can make some checking if value is valid before assignment.
// Better
class MakeUserProfile {
private email = null;
private phone = null;
getEmail(): string {
return this.email;
}
setEmail(emailVal: string): void {
// check validity before assignment
if (typeof emailVal === 'string' && isNotUsedBefore(emailVal)) {
this.email = emailVal;
}
}
}
const userProfile = new MakeUserProfile();
userProfile.setEmail('[email protected]');
c. Classes instead of prototypes
class
syntax is more concise and easier to understand.
// bad
function UserProfile(name) {
this.name = name;
}
UserProfile.prototype.checkName = function() {
/..logic../
};
// good
class UserProfile {
constructor(name) {
this.name = name;
}
checkName(): boolean {
/..logic../
}
d. Method chaining
This pattern is very useful and you can see it in many libraries such as jQuery and Lodash. It allows your code to be expressive, and less verbose. For that reason, I say, use method chaining and take a look at how clean your code will be. In your class functions, simply return this
at the end of every function, and you can chain further class methods onto it.
// Bad
class User {
constructor(name: string, email: string, phone: string) {
this.name = name;
this.email = email;
this.phone = phone;
}
setCountry(country: string): void {
this.country = country;
}
setAddress(address: string): void {
this.address = address;
}
setId(id: string): void {
this.id = id;
}
save(): Observable<void> {
/..logic../
}
}
const user = new User("Tom", "[email protected]", "888");
user.setCountry("France");
user.setId("100");
user.save();
// Better
class User {
constructor(name: string, email: string, phone: string) {
this.name = name;
this.email = email;
this.phone = phone;
}
setCountry(country: string): IUser {
this.country = country;
return this;
}
setAddress(address: string): IUser {
this.address = address;
return this;
}
setId(id: string): IUser {
this.id = id;
return this;
}
save(): Observable<void> {
/..logic../
}
}
const user = new User("Tom", "[email protected]", "888");
user.setCountry("France").setId("100").save();
e. Access modifier static
Class methods should employ the use of this
, or they should be transformed into static methods unless an external library or framework necessitates the utilization of particular non-static methods. If a method is designated as an instance method, it should imply that it functions differently based on the properties of the object it is called on.
// Bad
class User {
notify(): void {
console.log('User Loaded');
}
// Better - static methods aren't expected to use this
class User {
static notify(): void {
console.log('User Loaded');
}
}
User.notify();
a. Fields
Use bracket notation []
when accessing properties with a variable.
const user = {
name: 'Tom',
age: 28,
};
function getUserProp(prop) {
return user[prop];
}
const age = getUserProp('age');
b. Accessors
In this case, we will take a look at very similar to Class encapsulation logic. If object fields are not defended from the wrong assignment or deletion our code becomes unsafe. Lets take a look at different approaches.
getters
and setters
:// Bad
const user = {
email: null,
phoe: null,
}
// assign unexpected value
user.email = [{}];
// delete field
delete user.email;
In this example, we can freely assign the wrong value to user
fields or delete fields.
The solution will be to use getters and setters for fields. As setters are functions we also can make additional checks before assignment:
// Better
const user = {
get email(): string {
return this._email;
},
set email(value: string) {
if (typeof value !== 'string') {
return;
}
this._email = value;
}
};
Use closures when a function builds an object
Sometimes we prefer to create function that built objects. In this case, we also need to defend it from unexpected behavior. Let’s take a look at bad example.
// Bad
function makeUserProfile(): UserProfile {
/..logic../
return {
email: null,
phone: null,
// ...
};
}
const userProfile = makeUserProfile();
// Let's assign unxpected value
userProfile.email = new Error();
// And delete this field
delete userProfile.email;
Here we also open userProfile
fields and give the ability to break our logic.
Let’s improve with closures:
// Better
function makeUserProfile(): UserProfile {
// make the field private
let email = null;
// getter for email field
function getEmail(): string {
return email;
}
// setter for email field
function setEmail(emailVal: string): void {
// check validity before assignment
if (emailVal && isNotUsedBefore(emailVal)) {
email = emailVal;
}
}
return {
getEmail,
setEmail,
};
}
const userProfile = makeUserProfile();
userProfile.setEmail('[email protected]');
// Bad
const UserProfile = function(name) {
this.name = name;
};
UserProfile.prototype.getName = function getName() {
return this.name;
};
const userProfile = new UserProfile("Any user");
console.log(`User name: ${userProfile .getName()}`); // Any user: Any user
delete userProfile.name;
console.log(`User name: ${userProfile .getName()}`); // User name: undefined
In this example, we also make name
field public.
Let’s improve with closures to prevent deletion:
// Better
function makeUserProfile(name: string) {
return {
getName(): string {
return name;
}
};
}
const userProfile = new UserProfile("Any user");
console.log(`User name: ${userProfile.getName()}`); // User name: Any user
delete employee.name;
console.log(`User name: ${userProfile.getName()}`); // User name: Any user
In this article we considered code smells in Classes and Objects. In the next article, we will take a look at some problems in architecture and SOLID principles which will help to improve your code.