Tags

, , , , ,

As we saw on the previous post (Programming Principles), SOLID is a pretty extensive subject and deserves its exclusive post.

SOLID is a principle that teaches the best use of object orientation and some techniques that we will see below. SOLID means:

  • S – Single-responsiblity principle
  • O – Open-closed principle
  • L – Liskov substitution principle
  • I – Interface segregation principle
  • D – Dependency Inversion Principle

Single Responsibility: an object must have only one responsibility.

  • It is common to see objects with various responsibilities, doing various things, classes with thousands of lines, DAO with business rules, validation hardcoded on the Controller, etc.
  • A class must have one and only one reason to change, which means that it should have only one job.

Example (Controller):

BEFORE:

public String update(User user) throws Exception {
    /*
    * Controller Class should intermediate Model and redirect to 
    * the View, should not BE the Model and redirect 
    * to the View.
    */
    if(isValidEmail(user.getEmail()) && someBusinessRule(user)) {
        userDAO.update(user);
    } else {
        throw new Exception("Something is wrong, dude.");
    }

    return "myPage";
}

AFTER:

public String update(User user) throws Exception {
    /*
    * Controller Class call the right Class to deal with 
    * User stuffs and redirect to the View. One change on 
    * the business rule will not interfere on Controller code.
    */
    userService.update(user);

    return "myPage";
}

Open-Close: entities should be open for extension, but closed for modification.

  • It means that a class should be extended without having to change it.
  • For that, polymorphism is widely used.

Example:

BEFORE:

public void validate(String name, String email) {
    /*
    * Any change to the logic, this class has to be modified. 
    * Any new logic, this class has to be modified.
    */
    if( email == null || email.trim().equals("") ) {
        throw new IllegalArgumentException("Email cannot be empty");
    } 
    if( name == null || name.trim().equals("") ) {
        throw new IllegalArgumentException("Name cannot be empty");
    }
    if( !name.matches("[a-zA-Z]+") ) {
        throw new IllegalArgumentException("Name should contain only characters");
    }
}

AFTER:

  • Let’s create a class with the validation data
public class ValidationData{
  private String name;
  private String email;
  // Getters and Setters
}
  • Let’s create an interface for validations
public interface ValidationRule {
    void validate(ValidationData data);
}
  • Let’s implement one validation rule as example
public class EmailEmptinessRule implements ValidationRule {
  @Override
  public void validate(ValidationData data) {
    if ( data.getEmail() == null || data.getEmail().trim().equals("")) {
      throw new IllegalArgumentException("Email cannot be empty");
    }
  }
}
  • Let’s improve the validations call
public void validate(ValidationData data) {
    /*
    * Any change to the logic, this class DO NOT need to be modified. 
    * Any new logic, only this existing class has to be modified, 
    * and it will need a new implementation to the ValidationRule.
    */
  List<ValidationRule> rules = new ArrayList<>();

  rules.add(new EmailEmptinessRule());
  rules.add(new NameEmptinessRule());
  rules.add(new AlphabeticNameRule());

  for ( ValidationRule rule : rules){
    rule.validate(data);
  }
}

Liskov substitution: very specific client objects must be substitutable for instances of its subtypes.

  • In short, all subclasses or derived classes must be replaced by the parent class or base class.

Example:

BEFORE:

/*
* Ostrich is a bird that doesn't fly. So the concept here is
* wrong coded.
*/
class Bird {
  public void eat(){}
  public void fly(){}
}
class Owl extends Bird {}
class Ostrich extends Bird {
  public void fly(){
    throw new UnsupportedOperationException();
  }
}

AFTER:

/*
* A possible solution (as following) would be two classes: 
* FlightBird and NonFlightBird.
* Another solution would be one interface Flyable and Ostrich 
* will not implement that, and you would use Flyable instead of
* Bird.
*/
class Bird {
  public void eat(){}
}
class FlightBird extends Bird {
  public void fly() {}
}
class NonFlightBird extends Bird {}

Interface segregation: small and specific interfaces are better than a generic interface.

Example:

  • Using the example above, another possible solution would be:
class Bird {
  public void eat(){}
}
interface Flyable {
  public void fly();
}
class Owl extends Bird implements Flyable {
 public void fly(){}
}
class Ostrich extends Bird {}
  • This is good because depending on how your system works, there may be the need to add an airplane, for example. An airplane is not a bird, but also flies, then all you need do is implement Flyable.

Dependency inversion: do not depend on concrete implementations, but abstractions

Example:

BEFORE:

class UserService {
  private ConsoleLogger logger;
  public UserService(ConsoleLogger logger){
    this.logger = logger;
  } 
  //etc.
  //this logger call is: logger.log(String msg);
}

AFTER:

  • Now we also support file logging, and its call is: logger.logMessage(String msg);
  • We would have to change much of the structure, or duplicate. With the Dependency Inversion Principle, this would be easily solved as follows:
interface Logger {
  void log(String msg);
}
class ConsoleLogger implements Logger {
  // ...
  public void log(String msg) {
    logger.log(msg);
  }
}
class FileLogger implements Logger {
  // ...
  public void log(String msg) {
    logger.logMessage(msg);
  }
}
  • Now the class is independent of the concrete implementation and therefore extensible.
class UserService {
  private Logger logger;
  public UserService(Logger logger){
    this.logger = logger;
  } 
  //etc.
  //this logger will always call: logger.log(String msg);
}

 

Advertisements