Refactoring Improves Design of Existing Codes (Chapter 7) [Encapsulation]

Encapsulate Record

Encapsulate Collection

Replace Primitive with Object with Object

Replace Temporary with Query with Query by Query

Extract Class

Inline Class

Hide Delegate

Remove Middle Man

Substitute algorithm

Package record

Concrete display

// Before reconstruction
organization = { name: "Acme Gooseberries", country: "GB" };

// After reconstruction
class Organization {
  constructor(data) {
    this._name = data.name;
    this._country = data.country;
  }
  get name() { return this._name; }
  get country() { return this._country; }
  set name(arg) { this._name = arg; }
  set country(arg) { this_country = arg; }
}

Record structure can intuitively organize related data, so that I can transfer data as a meaningful unit, rather than just a collection of data.

For variable data, authors tend to use class objects rather than records. Objects can hide structural details. Users of this object do not need to investigate the details of storage and the process of calculation. At the same time, this encapsulation also helps to rename fields: I can rename fields, but at the same time provide access methods for new and old field names, which can also gradually modify the caller until the replacement is complete.

For immutable data, three values are stored in the record directly, and a filling step is added when data transformation is needed.

There are two types of record structure: one needs to declare a valid field name, and the other can use any field name at will. The latter language provides itself, such as hash, map, hash map, dictionary or associative array. But there is also a drawback in using such a structure, that is, what fields are held on a record is often not intuitive enough. If this record is only used in a small area of the program, it is not a big problem, but if it is used more widely, the problem of "data structure is not intuitive" will cause more trouble.

To be more intuitive, you can use classes more directly.

Encapsulation set

When encapsulating a collection, there is often an error: only encapsulating the access to the collection variables, but still allowing the value function to return to the collection itself. So that the member variables of the set can be modified directly, while the classes encapsulating it are totally ignorant and unable to intervene. Usually to avoid this situation, there are some ways to modify the collection -- "add" and "remove" methods.

Avoiding direct modification of collections, access to collections can be restricted in some form, and only read operations on collections can be allowed. The most common approach is to provide a value function for a collection, but return a copy of the collection. This way, even if someone modifies the copy, the encapsulated collection will not be affected.

practice

First, encapsulate the reference of the collection, and then add functions for "adding collection elements" and "removing collection elements" to the list. Find the reference point for the collection. If a caller modifies the collection directly, it calls a function that uses a new add/remove element. Modify the collection's value function to return a read-only data, using a read-only proxy or a copy of the data.

Example

// Before refactoring, you might think that the following encapsulations have been refactored
class Person {
  constructor(name) {
    this._name = name;
    this._courses = [];
  }
  get name() { return this._name; }
  get courses() { return this._courses; }
  set courses(aList) { this._courses = aList; }
}

class Course {
  constructor(name, isAdvanced) {
    this._name = name;
    this._isAdvanced = isAdvanced;
  }
 	get name() { return this._name; }
  get isAdvanced() { return this._isAdvanced; }
}

// Person can use Course to get information about courses
numAdvancedCourses = aPerson.courses.filter(c => c.isAdvanced).length;

Updating the list can be done by

// Way 1: Update the entire list
const basicCourseNames = readBasicCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name, false));
// Way 2: Update the course list directly
for (const name of readBasicCourseNames(filename)) {
  aPerson.courses.push(new Course(name, false));
}

This breaks the encapsulation, and when the list is updated, the Person class is not aware of it. Only field references are encapsulated, but the contents of fields are not really encapsulated.

In order to solve the above problems, we need to continue refactoring, adding two methods to the Person class, "adding courses" and "removing courses" interface.

addCourse(aCourse) {
  this._courses.push(aCourse);
}

removeCourse(aCourse, fnIfAbsent = () => { throw new RangeError(); }) {
  const index = this._courses.indexOf(aCourse);
  if (index === -1) fnIfAbsent();
  else this._sourses.splice(index, 1);
}

With the addition and deletion methods, you can delete the set courses settings function. If you can't delete it, make sure that you assign values to fields with a copy, so that the collection passed in through the parameters is not modified.

set courses(aList) {
	this._courses = aList.slice();
}

In order for the modification to be done by addCourse and removeCourse, the get value function returns a copy, so that the direct push will be invalid when the courses value is obtained.

get courses() {
  return this._courses.splice();
}

Substituting Objects for Basic Types

If you find that the operation of a data is not limited to printing, you create a new class for it. At first, this class may simply wrap the data of a simple type, but as long as there are classes, the business logic added in the future can go.

Replacing Temporary Variables with Queries

One of the functions of a temporary variable is to save the return value of a piece of code so that it can be used later in the function. Temporary variables allow references to previous values, which not only explain their meaning, but also avoid duplication of code.

This technique applies only to certain types of temporary variables: those that are only computed once and are no longer modified.

Example

// Before reconstruction
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000) {
  return basePrice * 0.95;
} else {
  return basePrice * 0.98;
}

// After reconstruction
get basePrice() {
  return this._quantity * this._itemPrice;
}
// ...
if (this.basePrice > 1000) {
  return this.basePrice * 0.95;
} else {
  return this.basePrice * 0.98;
}

Refining class

In practice, the responsibility of class is the abstraction of single function. With the expansion of functions, classes will continue to expand, and eventually this class will become difficult to maintain and understand.

If some parameters and some functions always appear together, and some data often change at the same time or even depend on each other, this means that they should be separated.

Example

// Before reconstruction
class Person {
  get officeAreaCode() { return this._officeAreaCode; }
  get officeNumber() { return this._officeNumber; }
}

// After reconstruction
class Person {
  get officeAreaCode() { return this._telephoneNumber.areaCode; }
  get officeNumber() { return this._telephoneNumber.number; }
}
class TelephoneNumber {
  get areaCode() { return this._areaCode; }
  get number() { return this._number; }
}

Inline class

Reverse operation of refining class

The reason for using inline classes is that if a class is no longer responsible enough, there is no longer a reason for its existence alone. Another scenario is two classes. To rearrange their responsibilities and associate them, it is simpler to inline them in one class and then separate their responsibilities with refined classes.

Hidden delegation

Modular design is inseparable from packaging. Encapsulation means that each module should know as little as possible about other parts of the system, such as the use of methods for other objects.

If some clients first get another object through the field of the object and then call the function of the latter, then the client must be aware of this layer of delegation. The impact is that if the interface of the latter function is modified, it will affect the entire client modification, which can easily lead to errors. At this time, a simple delegation function can be placed on the service object to hide the delegation relationship, thus eliminating this dependency. Subsequent modifications only need to be made in the object, not in the entire client.

Example

// Before reconstruction
class Person {
  constructor(name) {
    this._name = name;
  }
  get name() { return this._name; }
  get department() { return this._department; }
  set department(arg) { this._department = arg; }
}

class Department {
  get chargeCode() { return this._chargeCode; }
  set chargeCode(arg) { this._chargeCode = arg; }
  get manager() { return this._manager; }
  set manager(arg) { this._manager = arg; }
}

Here, if the client wants to know who someone's manager is, it must get the Department object and then call the manager in the object, that is, the manager.

manager = aPerson.department.manager;

If we set up a simple delegation function in Person, we can not only not expose the working principle of Department, but also be more conducive to later modifications.

// After reconstruction
class Person {
  constructor(name) {
    this._name = name;
  }
  get name() { return this._name; }
  get department() { return this._department; }
  set department(arg) { this._department = arg; }
  get manager() { return this._department.manager; }
}

So the next step to get the manager's information is

manager = aPerson.manager;

remove middle man

Removing intermediaries is the reverse reconstruction of hidden delegation relationship.

The benefits of concealing a delegation are mentioned earlier, but they also come at a cost. Whenever the client wants to use the new features of the delegated class (i.e. the functions of the latter mentioned above), a simple delegation function must be added to the server. As the characteristics of the delegated class become more and more, the forwarding function becomes more and more. The service class has completely become an intermediary, so it is better to have the client call the trustee class directly.

In fact, whether hidden or removed, it is very difficult to assess the criteria. Standards can be continuously adjusted during the operation of the system. As the code changes, the "appropriate degree of hiding" scale changes accordingly.

It can be mixed for concealing the principal relationship and removing the middleman. Some delegation relationships are very common and can be retained to make client code calls more friendly.

Replacement algorithm

"Refactoring" can decompose some complex things into simpler pieces, but sometimes the whole algorithm must be deleted and replaced by simpler ones.

Before modifying the algorithm, it may be a huge and complex code. First, decompose the original function as much as possible. Only by decomposing it into smaller functions, can the algorithm replacement work be carried out with great certainty.

Example

// Before reconstruction
function foundPerson(people) {
  for (let i = 0; i < people.length; i++) {
    if (people[i] === 'Don') {
      return 'Don';
    }
    if (people[i] === 'John') {
      return 'John';
    }
    if (people[i] === 'Rent') {
      return 'Rent';
    }
  }
  return '';
}

// After reconstruction
function foundPerson(people) {
  const candidates = ['Don', 'John', 'Rent'];
  return people.find(p => candidates.includes(p) || '');
}

Posted by Josh954r on Tue, 24 Sep 2019 20:26:01 -0700