Search code examples
node.jsmodel-view-controllerdependency-injectionarchitecturesoftware-design

How to prevent a God model class in an MVC architecture?


How can one prevent their model classes getting too large or depending on objects not directly related to them?


Example

Imagine a user registration form, that once successfully registered, the user receives an email to verify their email address.

Sample code 1

disclaimer - this is demonstration code

// route.js

app.post('/register', (req, res) => {
  container.get('UserController').postRegisterUser(req, res);
});

Option 1 - adding emailSender dependency inside the User model

// userController.js

class UserController {
  public constructor(model) {
    this.model = model;
  }

  public postRegisterUser(req, res) {
    const {email, password, repeatPassword} = req.body;
    const user = this.model.registerUser({email, password, repeatPassword});
    return res.render('/', user);
  }
}
// UserModel.ts

class UserModel {
  constructor(repository, validator, emailSender) {
    this.repository = repository;
    this.validator = validator;
    this.emailSender = emailSender;  // <--- emailSender injected in to model
  }

  public registerUser(user) {
    if (this.validator.isValid(user)) {
      this.repository.save(user);

      this.emailSender.sendVerificationEmail(user.email); // <---- emailSender called here

      return user;
    }
  }
}

Option 2 - adding emailSender dependency inside the controller

// userController.js

class UserController {
  public constructor(model, emailSender) {
    this.model = model;
    this.emailSender = emailSender;  // <--- Injected in constructor
  }

  public postRegisterUser(req, res) {
    const {email, password, repeatPassword} = req.body;
    const user = this.model.registerUser({email, password, repeatPassword});

    this.emailSender.sendVerificationEmail(email); // <--- emailSender called here

    return res.render('/', user);
  }
}
// UserModel.ts

// no EmailSender injected
class UserModel {
  constructor(repository, validator) {
    this.repository = repository;
    this.validator = validator;
  }

  public registerUser(user) {
    if (this.validator.isValid(user)) {
      this.repository.save(user);
      return user;
    }
  }
}

One can see how large these dependencies can become when other systems become involved - such as sending data to an S3 bucket, generating a csv file, etc. I ideally don't want my controller or model to have these dependencies, but then where do they go?

I imagine the ideal solution is something in-between these two options outlined above. I am just having difficulty with how to implement my web application.


Solution

  • You can build another component UserProcessor or UserRegistrationProcessor, which contains all those external calls to other services, and inject that component into UserController.