GRASP Principles - Part 3: Polymorphism, Pure Fabrication, Indirection, Protected Variations

Written by serhiirubets | Published 2022/07/26
Tech Story Tags: dependency-injection | grasp | polymorphism | oop | design-patterns | design-principles | best-practices | oop-patterns

TLDRThis is the last part of GRASP, where we will learn all other principles. OOP polymorphism is almost about the same, where OOP says what we can do with OOP. In the first part of this article, we learned about what is **GRASP polymorphism, why we should use it, and how to use it. In this article we will also learn about the principles that are used in many famous frameworks, such as Inversion of Inversion (Inversion of Control)via the TL;DR App

Hello guys. In the first part and the second part, we learned about what is GRASP, why we should use it, and saw examples of different principles. This is the last part of GRASP, where we will learn all other principles.

Polymorphism

GRASP polymorphism as you may guess it’s an implementation OOP polymorphism principle. What is the difference between OOP polymorphism and GRASP polymorphism?

OOP polymorphism says that we could use inheritors from one class from the place, where their parent could be used and at the same time they could do different polymorphic actions.

GRASP polymorphism says, that using OOP polymorphism we can solve a specific task. What problem we can solve?

Let’s check an example. Imagine that we use external API, for example, bank API in our system.

At some point, they decided to switch to the second version, API 2, with supporting the first version? What we can do? For example:

if (API === 1) {
// do1
// do2
} else if (API === 2) {
// do3
// do4
}

// do5
// do6

if (API === 1) {
// do7
// do8
} else if (API == 2) {
// do9
// do10
}

And we can have the same conditions in different places, different files. And imaging after some time external API create next, the third version API 3 and we should switch to this version. What we should do? Go through all files, though all if/else (switch/case), and add a new condition.

What the problem could be in this approach:

  1. We can forget about some places, where used these conditions (or we may not know about them)
  2. All old API versions most likely will be kept forever, no one will remove them
  3. If external API will be changed a lot, we will have a huge codebase, it will be really hard to maintain.

What can we do to avoid it? We can use the polymorphism principle. Only at 1 place, do we create conditions (depends on ENV, CI, config…), and then we use, we use polymorphic functionality that uses the same interface throughout the whole application.

And now, if we migrate to the new API version, we can easily remove 1 class with the old version and only 1 import where this class was used before. We don’t have a huge code base, we don’t need to remember many places where if/else could be, and our system is easy to maintain.

So, OOP polymorphism and GRASP polymorphism is almost about the same, where OOP polymorphism says what we can do and where GRASP polymorphism, its implementation, it says what we can solve using OOP polymorphism.

Pure Fabrication

If we remember about OOP, is that objects in our code are objects from the real world. But sometimes we use objects, that are not exist in the real world, some abstractions (facade, mediator, proxy). Such not real objects are called pure fabrication. In general, pure fabrication it’s just a term for not real objects, nothing special.

Indirection (Inversion of control or Dependency injection)

It’s one of the most important patterns, that are used in many famous frameworks.

One of the good things that we should remember, is that all communication with class should be used from the interface, not from class directly.

Let’s see an example.

In a common use case we have controller, service and API service (controller, entity, repository)

For simplicity, we have class A, class B, and class C. Let’s think we work with user. So we have User controller, user entity and user repository that works with DB using Postgress.

So, how do they depend on each other? A → B → C

// User controller
import userService from './userService';

const body = req.body;
userService.create(user);

// User service
import userApi './userApi';

async function create(user) {
  // some code
  await userApi.create(user)
}

And sometime later, a new engineer has a task to do something pretty the same as it is now, but use for specific case saving data not to Postgress, but to MongoDB.

In our case, we can see that now we create our user in some DB.

And what we will do when we need the same functionality but for another kind of DB?

Probably we will create almost the same userApi class (file) for working with DB. But how we will use it? We have a couple of options (both of them are bad)

  1. We can add if/else in the current userService file and use 2 kinds of DB
  2. We can copy/paste userService that works almost the same but uses another DB

And if sometime later we decide to add more DBs? We should do the first or the second option for each of them?

The correct way is to use not class directly, but use interface.

In our case we can create an interface for the DB class:

inteface IUserApi {
  getById(id: string): User
  create(body: UserCreateRequestDTO): User;
  deleteById(id: string): void;
}

For each DB we can create a class (functions) that implements this interface.

class UserService {
  userApi: IUserApi;
  constructor(userApi: IUserApi) {
    this.userApi = userApi;
  }

  getById(id: string) {
    return this.userApi.get(id)
  }
}

And in our UserService we can create the ability, to decide what will be used with our service right now (using dependency injection or manually):

// User Controller
class UserController() {
  constructor() {
    if (a) {
      this.userService = new UserService(userWithMongoDbApi)
    } else {
      this.userService = new UserService(userWithPostgressDbApi)
    }
  }
}

And also for the UserController we can use the same teqniqe.

So now, we don’t use DB classes directly in userService as it was before.

Our userService work only with interface IUserApi and for userService doesn’t matter is it mongoDb, Postgress, MySql…

Our dependency is not like A→B-C, but A-B→InterfaceApi

So, when we need to add a new DB, we don’t need to duplicate the code or create if/else in each case, we just need to add a new interface implementation and only once setup what implementation class will be used.

Protected Variations

It’s a pretty simple pattern and it is a unifying principle from all previous GRASP patterns.

This principle says: that we should create our system in a way, where we can change something in one place, all other code should work as before, it shouldn’t break the previous implementation.

For example, A→B→C→D and we want to change from C to X, A→B-X-D our system should work.

If we want to add something new or remove unused code, our system shouldn’t fail.

How can we achieve it? We should follow all previous GRASP principles, that we’ve already discussed. Let’s remember them briefly:

Information expert: this pattern says that all methods that work with data (variables, fields), should be in the same place where data (variables or fields) exist.

Creator says who and where should objects be created.

Low coupling says, that in our system, we should create as fewer connections between our classes, modules, files, etc… as possible.

The high cohesion principle says, that if you have a class (object, file, etc…) all fields and methods should do only one well-defined job.

Inderection says that we should work with other system parts using the interface, not directly.

I really hope that you enjoyed all 3 parts of GRASP articles and if you haven’t known them before, now you can take 1, 2 or all of them and use in your project.


Written by serhiirubets | I'm a Fullstack JS engineer with 10 years of experience. Also, I'm a mentor, teacher, and author of front-end courses.
Published by HackerNoon on 2022/07/26