Code Reviews to Eliminate Common Anti-Patterns

Background

The anti-pattern, which is a common mistake, is bad practice, and we always try to avoid it and improve. So that we can reduce bugs and get a reusable solution to manage applications easily.

Problem

We design and implement the application, and review it to avoid anti-patterns. The web application uses domain models, while the server-side web API also has domain models. We always need to validate both input data and business logic. It doesn’t matter whether it’s a web application or a web API.

Topics

  • Input Kludge Anti-Pattern and Magic Pushbutton Anti-Pattern.
  • Encapsulation Violation Anti-Pattern.
  • Anemic Domain Model and Rich Domain Model.

Part 1. Scenarios

Reviewing the Customer Profile Page. Focusing on the input validation based on the business rules on the UI page.

  1. Business Rule: Get valid information from a customer profile.
  2. Business logic: Get a valid name and profile photo from the UI to send to the API.
  3. Profile page description: We have a UI form that has input controls to get the customer's name and profile photo.
    Profile page description

The Input Kludge Anti-Pattern

We have first-name and last-name name text-box controls where we can add text, numbers, and special characters, and we can’t get any expected error messages. That means it produces the input kludge anti-pattern. These should accept only text, otherwise, we expect dynamic error messages for invalid inputs.

Look at the file uploader control that accepts any file extensions like PDF, TXT, PNG, JPG, etc.; without showing the error messages, it produces an input kludge antipattern. This means that these input controls can’t handle unexpected inputs.

Magic Pushbutton Anti-Pattern

Focusing on the submit button of the profile. form.

Submit button

I added the valid names to the text boxes and an image to the file uploader. But the content of an image is empty. I clicked on the submit button from the UI, and it didn’t show an error message for the invalid contents. That means that the TypeScript logic failed to catch an error. So, after clicking on the submit button, the information is accepted to send to the API service, which produces the magic pushbutton antipattern.

Part 2. Scenarios

Display a customer list on a grid view page. UI logic will receive the customer profiles from the RESTful API service to display them on a grid page. The customer profile has three columns: customer-Id, name, and profile photo.

Encapsulation Violation Anti-Pattern

Look at the CustomerProfileDisplay model. We are using Angular with TypeScript.

TypeScript

The ICustomerProfileDisplay interface has properties, and we don't need to mention the access modifiers (public, private, protected, etc.) because, by default, the interface holds public properties and methods.

The CustomerProfileDisplay model has five properties, and no access modifiers are specified. So, by default, all these properties are public. But based on business rules, this is an immutable class. We don’t need to modify the record, firstName, and lastName, which should not be visible, so we need to use private access modifiers for these names. So, we’re violating the encapsulation concept of object-oriented programming, and this is an antipattern.

We need to refactor this model class as below.

Model class

Public access modifier and the read-only keyword: We added the public modifier along with read-only, so that the public property can be visible after creating the instance, and the read-only keyword prevents data modification from outside.

Private access modifier: We added the private access modifier for the first and last name properties because we don’t need to access these properties from outside after creating an instance.

Anemic Domain Model

The above CustomerProfileDisplay model can’t validate the data. Based on the business rules of the customer profiles, all three public properties must be non-empty.

If any of the public properties are empty, then just throw an error exception with the error messages. If you don't include the business rules in the model class, then it will be an anemic domain model, which is an anti-pattern.

Data validation and data mapping are both important for microservices communication. Because the model class will receive data from the API, if there are mapping issues between the web API responses and the web application, then the application model class needs to throw an exception. Without data validation and business rules inside the model class, only the set-get properties introduce the anemic domain antipattern.

Rich Domain Model

You can check the data validation manually, or you can use the built-in data annotations to check for invalid data. For example, I used the class-validator and class-transformer npm packages with the model class.

Install the required packages

npm install class-validator --save
npm install class-transformer --save
npm install reflect-metadata --save

Find all the details at the URL below.

Let’s refactor the CustomerProfileDisplay model class. I added all the data annotations from the class-validation packages to this class below.

Data Annotations

I need another model class to keep a list of the CustomerProfileDisplay. I created the CustomerProfileDisplayArray model class and, added all the required data annotations with these properties and one validation method. The method validates all the CustomerProfiles to get the error result.

Validation method

customer-profile-display.model.ts

import { Type } from "class-transformer";
import { 
  ArrayNotEmpty, 
  IsArray, 
  IsInt, 
  IsNotEmpty, 
  NotEquals, 
  ValidateNested, 
  validate 
} from "class-validator";

export class CustomerProfileDisplay implements ICustomerProfileDisplay {
  @IsNotEmpty()
  @IsInt()
  @NotEquals(0)
  public readonly CustomerProfileId: number;

  @IsNotEmpty({ message: 'A name must have a value.' })
  public readonly FullName: string;

  @IsNotEmpty()
  public readonly ProfilePhoto: string;

  private firstName: string;
  private lastName: string;

  constructor(customerProfileJsonData: any) {
    //// Converting string to number.
    this.CustomerProfileId = +customerProfileJsonData['customerProfileId'];
    this.firstName = customerProfileJsonData['firstName'];
    this.lastName = customerProfileJsonData['lastName'];
    this.FullName = this.firstName + ' ' + this.lastName;

    //// Base64String.
    this.ProfilePhoto = customerProfileJsonData['profilePhoto'];
  }
}

export interface ICustomerProfileDisplay {
  CustomerProfileId: number;
  FullName: string;
  ProfilePhoto: string;
}

export class CustomerProfileDisplayArray {
  @IsArray()
  @ArrayNotEmpty({ message: 'Customer-Profile-Display array should not be empty' })
  @ValidateNested({ each: true })
  @Type(() => CustomerProfileDisplay)
  customerProfileDisplayItem: CustomerProfileDisplay[] = new Array();

  async GetErrorResults(customerProfilesDisplay: CustomerProfileDisplayArray): Promise<string> {
    let errorResultArray = new Array();
    let errorResult: string = '';

    try {
      await validate(customerProfilesDisplay).then(errors => {
        if (errors.length > 0) {
          for (const error of errors) {
            if (error.children && error.children.length > 0) {
              for (const childError of error.children) {
                let childErrors = childError.children;
                let errorItem: string = '';

                childErrors?.forEach(e => {
                  let constraint: any = JSON.parse(JSON.stringify(e.constraints));
                  let target: any = JSON.parse(JSON.stringify(e.target));
                  let customerProfileId = target['CustomerProfileId'];

                  if (constraint['notEquals'] != null) {
                    errorItem += 'Property: ' + e.property + '. Error: ' + constraint['notEquals'];
                  } else if (constraint['isNotEmpty'] != null) {
                    errorItem += ' CustomerProfileId: ' + customerProfileId + '. Property: ' + e.property + '. Error: ' + constraint['isNotEmpty'];
                  } else {
                    errorItem += 'CustomerProfileId: ' + customerProfileId + '. Property: ' + e.property + '. Constraints: ' + JSON.stringify(e.constraints);
                  }

                  if (errorItem != '') {
                    errorResultArray.push(errorItem);
                  }
                });
              }
            }
          }
        }
      });
    } catch (e) {
      return JSON.stringify(e);
    }

    if (errorResultArray.length > 0) {
      errorResult = JSON.stringify(errorResultArray);
    }

    return errorResult;
  }
}

Similarly, I created the CustomerProfile model class below.

CustomerProfile

customer-profile.model.ts

import { IsNotEmpty, NotEquals, validate } from "class-validator";

export class CustomerProfile implements ICustomerProfile {

  @NotEquals(0)
  CustomerProfileId: number | any;

  @IsNotEmpty({ message: 'First-Name must have a value.' })
  FirstName!: string;

  @IsNotEmpty({ message: 'Last-Name must have a value.' })
  LastName!: string;

  @IsNotEmpty({ message: 'A photo must have a value.' })
  ProfilePhoto!: string;

  async GetErrorResults(customerProfiles: CustomerProfile): Promise<string> {
    let errorResultArray = new Array();
    let errorResult: string = '';

    try {
      await validate(customerProfiles).then(errors => {
        if (errors.length > 0) {
          errors?.forEach(error => {
            let errorItem: string = '';
            let constraint: any = JSON.parse(JSON.stringify(error.constraints));
            
            if (constraint['notEquals'] != null) {
              errorItem += 'Property: ' + error.property + '. Error: ' + constraint['notEquals'];
            } else if (constraint['isNotEmpty'] != null) {
              errorItem += 'Property: ' + error.property + '. Error: ' + constraint['isNotEmpty'];
            } else {
              errorItem += 'Property: ' + error.property + '. Constraints: ' + JSON.stringify(error.constraints);
            }

            if (errorItem != '') {
              errorResultArray.push(errorItem);
            }
          });
        }
      });
    } catch (e) {
      return JSON.stringify(e);
    }

    if (errorResultArray.length > 0) {
      errorResult = JSON.stringify(errorResultArray);
    }

    return errorResult;
  }
}

export interface ICustomerProfile {
  CustomerProfileId: number;
  FirstName: string;
  LastName: string;
  ProfilePhoto: string;
}

You can call the getErrorResults method in the component class. Use the codes below to validate the data.

GetErrorResults

page.component.ts

let profileJsonDataList: any = this.getProfileDataList();
let displayResult: CustomerProfileDisplay[] = new Array();

// Json-data to CustomerProfileDisplay array mapper.
displayResult = profileJsonDataList.map((data: any) => new CustomerProfileDisplay(data)) as CustomerProfileDisplay[];

let profileDisplayList: CustomerProfileDisplayArray = new CustomerProfileDisplayArray();

// Array of CustomerProfileDisplay
profileDisplayList.customerProfileDisplayItem = displayResult;

// Data validation check. It returns error list.
let errorResults = await profileDisplayList.GetErrorResults(profileDisplayList);

console.log(errorResults);
// alert(errorResults);

In the next part, I’ll continue with other anti-patterns to improve the quality of the code. Anyway, gotta go.