Principios SOLID


Robert C. Martin
(Lo pueden recordar como el autor de Clean Code)
¿Qué es el diseño orientado por objetos? ¿De qué se trata? ¿Cuáles son sus beneficios? ¿Cuáles son sus costos?
Parece tonto hacerse estas preguntas ahora que prácticamente todos los programadores usan un lenguaje orientado por objetos. Pero la pregunta
es importante porque, me parece, que la mayoría de nosotros utiliza esos lenguajes sin saber por qué y sin saber cómo sacarle el mayor beneficio posible.


DRY!

Don't Repeat Yourself!
  • Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
  • Not a SOLID principle but it's heavily related.

What's the problem with repetition?

  • When you have a fix or an upgrade, you have to remember to do the change on all the places where the repetition is present.
  • If you forget at least one, you will have an inconsistent system.
  • Those errors are not easy to spot, until is too late.

But eliminating repetition is easy, right?!

How about this...
public GridList(SlingHttpServletRequest request) {
    ValueMap properties = request.getResource().adaptTo(ValueMap.class);
    
    String leftLink = properties.get("leftlink", "");
    String leftTitle = properties.get("lefttitle", "");
    String leftDescription = properties.get("leftdescription", "");                    
    this.leftGridBlock = new GridBlockDialog(leftLink, leftTitle, leftDescription);

    String rightLink = properties.get("rightlink", "");
    String rightTitle = properties.get("righttitle", "");
    String rightDescription = properties.get("rightdescription", "");                    
    this.rightGridBlock = new GridBlockDialog(rightLink, rightTitle, rightDescription);
}
The repetition is easy to spot, but it's the kind of code that's written everywhere!
How do we eliminate it?
public GridList(SlingHttpServletRequest request) {
  ValueMap properties = request.getResource().adaptTo(ValueMap.class);

  GridBlockDialog[] blocks = new GridBlockDialog[2];
  String[] values = {"left", "right"}; 

  for (int i = 0; i < blocks.length; ++i) {
    String link = properties.get(values[i] + "link", "");
    String title = properties.get(values[i] + "title", "");
    String desc = properties.get(values[i] + "description", "");                    
    blocks[i] = new GridBlockDialog(link, title, description);
  }

  this.leftGridBlock = blocks[0];
  this.rightGridBlock = blocks[1];
}
Look what it's common, and what changes put it inside an array!
public GridList(SlingHttpServletRequest request) {
  ValueMap properties = request.getResource().adaptTo(ValueMap.class);
this.leftGridBlock = new GridBlockDialog(properties, "left"); this.rightGridBlock = new GridBlockDialog(properties, "right");}
You can also use functions!
and what about this?
public String square(int size) {
   StringBuilder builder = new StringBuilder();
   for (int row = 1; row <= size; ++row) {
      for (int column = 1; column <= size; ++row) {
         builder.append('*');
      }
      builder.append(String.format("%n");
   }
   return builder.toString();}
public String pyramid(int size) {
   StringBuilder builder = new StringBuilder();
   int totalSpaces = (2 * size) - 1; int totalCharacters = 1;
   for (int row = 1; row <= size; ++row) {
      for (int spaces = 1; spaces <= totalSpaces; ++spaces) {
         builder.append(' ');
      }
      for (int chars = 1; chars <= totalCharacters; ++chars) {
         builder.append('*');
      }
      --totalSpaces; totalCharacters += 2;
   }
   return builder.toString();
}
  • There is a lot of repetition!
  • ... and magic numbers which
    is a related concept!
public static String square(int size) {
   StringBuilder builder = new StringBuilder();
   for (int row = 1; row <= size; ++row) {
      for (int column = 1; column <= size; ++column) {
         builder.append('*');
      }
   }
   return builder.toString(); 
}
public static String pyramid(int size) {   int totalSpaces = (2 * size) - 1;
   int totalCharacters = 1;
   StringBuilder builder = new StringBuilder();   
   for (int row = 1; row <= size; ++size) {
      for (int spaces = 1; spaces <= totalSpaces; ++spaces) {
         builder.append(' ');
      }
      for (int chars = 1; chars <= totalCharacters; ++chars) {
         builder.append('*');
      }
      --totalSpaces;
      totalCharacters += 2;
   }
   return builder.toString();
}
What do we do?
private static final char SPACE = ' ';
private static final char CHARACTER = '*';

private static String makeString(int size, char character) {
   StringBuilder builder = new StringBuilder();
   for (int n = 1; n <= size; ++n) {
      builder.append(character);
   }
   return builder.toString();
}
Use constants to get rid of magic numbers! If their value change there is just one place to update it!

Create an utility method for the code that is repeated!
public static String square(int size) {
   StringBuilder builder = new StringBuilder();
   for (int row = 1; row <= size; ++row) {
      builder.append(String.format("%s%n",makeString(size, CHARACTER)));
   }
   return builder.toString();
}public static String pyramid(int size) {
   int totalSpaces = (2 * size) - 1;
   int totalChar = 1;
   StringBuilder builder = new StringBuilder();   
   for (int row = 1; row <= size; ++size) {
      builder.append(makeString(totalSpaces, SPACE));
      builder.append(String.format("%s%n",makeString(totalChar, CHARACTER)));
      --totalSpaces; totalChar += 2;
   }
   return builder.toString();
}
What about now?
We still have repetition, both methods are doing the same: creating a "2D figure" using spaces and a character!
private static String makeFigure(int size, int spaces, int chars, int modifierSpaces, int modifierChar) {
   
   StringBuilder builder = new StringBuilder();   
   for (int row = 1; row <= size; ++size) {
      builder.append(makeString(spaces, SPACE));
      builder.append(String.format("%s%n",makeString(chars, CHARACTER)));
      totalSpaces += modifierSpaces;
      totalChar += modifierChar;
   }
   return builder.toString();
}
And with this abstraction, our code to "draw" figures is
public static String square(int size) {
   return makeFigure(size, 0, size, 0, 0);
}

public static String pyramid(int size) {
   return makeFigure(size, 2 * size - 1, 1, -1, 2);
}

If possible make similar methods call one another. 
In our case we only need to test 
one method instead of two.

Very important with constructors!

Eliminating repetition
helps with

  • Code that's more testable
  • Code that's easier to understand
  • Code that's consistent

Writing repetition-less code is not easy

Best done in refactorings!
Code Reviews also help a lot!


SRP

Single Responsibility Principle
(or how to react to a class of 1000+ lines)
  • A class should only have one reason to change

  • A class with a single responsibility is easier to re use, so you can use code that is already tested!
  • If you need to re use some of the functionality of a class but you find that the other responsibilities are a drag, you will write a new class which breaks DRY!

Does this code breaks SRP?

public class Fraction {   private int denominator;
   private int numerator;
   private static int gcf(int a, int b) {
      return b == 0 ? a : gcf(b, a % b);
   }   public Fraction(int numerator, int denominator) {
final int GCF = gcf(numerator, denominator); this.denominator = denominator / GCF; this.numerator = numerator / GCF; }}

Let's introduce a measure
to help us judge SRP

Cohesion:
The cohesion of a method is the number
of attributes that it uses. 

The cohesion of a class is related
with the cohesion of all its methods.
public class IntStack {
   private int i = 0;
   private int[] stack = new int[10];

   public void push(int n) {
      stack[i++] = n; // has 100% cohesion
   }
   public int pop() {
      return stack[i--]; // has 100% cohesion
   }
   public int size() {
      return i + 1; // has 50% cohesion
   }
}
This class doesn't break SRP

Cohesion

It's not a measure to use lightly
(you can't say "all classes with methods
with less than X% break SRP")
But it helps
Of course DTO have low cohesion but they don't break SRP

A collorary

A static methods has 0% cohesion
Every static method on a class with behavior, probably, makes that class break SRP
Some exceptions: e.g. Static Factories
(they use private members)

How to avoid it?

Use a Utility Class

A Utility Class should: 
  • Be final
  • Have only one constructor,
    it must be private and no attributes
  • Have only static methods

You can always do this, but some people think it explode the number of classes

Java has quite a few

  • Math
  • Arrays
  • Collections
  • Files
  • FileSystems
  • Objects
  • Channels
  • etc.
Notice the standard of, for a given class Foo
its Utility Class is named Foos
(not always the best name but a good fallback)
public class Fraction {   private int numerator;
   private int denominator;   public Fraction(int numerator, int denominator) {
      final int GCF = ArithmeticOperations.gcf(numerator, denominator);
      this.numerator = numerator / GCF;
      this. denominator = denominator / GCF;
   }}

If you have a class with methods with low cohesion try splitting into two classes

What about this code?

public Config {
   public Config(Path filename) throws IOException {
      List<String> content = Files.readAllLines(filename);
// more code. } // more code. }
If the source of the configuration changes
Config will need to change!

Is this better?

public Config {
   public class Config(List<String> content) {
      // code.
   }
   // more code.
}
Now we can add new configuration sources
without changing Config.

Think about the "moving parts"
and decouple them.


OCP

Open / Closed Principle
(or how to not panic while adding functionality)

Benefits

  • When in need to add functionality, you don't have to change code that's already working. You just create new code to work with the old.

How do you do it?

  • In OOP you already have a mechanism to that: Inheritence!

But what if inheritence is not the right solution?
public void draw(List<Shape> shapes) {
   for(Shape s : shapes) {
      if (s instanceof Circle) {
         // code specific to drawing a Circle
      } else if (s instanceof Square) {
         // code specific to drawing a Square
      }
      // more if statements for other shapes
   }
}
Every time a new figure is created
we will have to chage this code
public class Circle extends Shape {
   // code
   public void draw() {
      // code specific to draw a Circle
   }  
}public class Square extends Shape {
   // code
   public void draw() {
      // code specific to draw a Square
   }
}

...

public void draw(List<Shape> shapes) {
   for (Shape s : shapes) {
      s.draw();
   }
}
Now draw works with all Shapes, even onces that have been not created!

How to adhere to OCP

  • Use Interfaces
  • Change conditional statements 
    for polymorphic behavior

There are some design patterns
that can help too

  • Decorator Design Pattern
  • Strategy Design Pattern
  • Factory Method
  • etc.


LSP

Liskov Sustitution Principle 
(or how to not regret relying on your family)

What's the problem with inheritance?

  • Inheritance create high coupling with all the classes on the inheritance tree! A change on any class affects all other classes below.
  • Inheritance is a powerful mechanism; there is no other way to model an IS-A relationship, but do not use it haphazardly!

Favor Composition over Inheritance

  • To reuse code you can always use composition & delegation
public enum Number {
   ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, J, Q, K;

   public int value() {
      return ordinal() + 1;
   }
}

public enum Suit {
   HEARTS, DIAMONDS, SPADES, CLUBS;
}
A Card also have a value, should we try
to "extend" Number just to reuse the method?

Of course not, a Card doesn't have
an IS-A relantionship with Number
public class Card {   private Number number;
   private Suit suit;   public Card(Number number, Suit suit) {
      this.number = number;
      this.suit = suit;
   }   public int value() {
      return number.value(); // Composition + Delegation
   }
}

A more subtle problem with inheritance

We want to model a Circle class,
but we previously have an Ellipsis class.

We would use inheritance since we now that
a Circle IS-AN Ellipsis
(that's what we were taught on school)
public class Ellipsis {   private int a;
   private int b;   public Ellipsis(int a, int b) {
      this.a = a;
      this.b = b;
   }   public void setA(int a) {
      this.a = a;
   }
   public int getA() {
      return a;
   }
   public void setB(int b) {      this.b = b;
   }
   public int getB() {
      return b;
   }}

Now the "easy" part

public class Circle extends Ellipsis {

}
But  a Circle has a radius,
but now we have two attributes,
which one we treat as the radius?

Let's treat both as the radius...
public class Circle extends Ellipsis {
   @Override public void setA(int a) {
      this.a = a;
      this.b = a;
   }
   @Override public void setB(int b) {
      this.b = b;
      this.a = b;
   }
}
But now this code is broken
public void foo(Ellipsis e) {
   e.setA(10);
   e.setB(5);
   assert e.getA() == 10;
}

...

foo(new Circle());

What went wrong?

The behavior of a Circle object
is not consistent with the behavior
of an Ellipsis object. 


Behaviorally, a Circle is not anEllipsis!
And it'
s behavior that software
is really all about.

Another example:
java.sql.Timestamp
extends
java.util.Date

does that makes any sense?

It may seem natural
but there are not exactly related:

A Timestamp is related to when something
happened in a local context, where a date
is something that should take into account
different calendars, timezones and offsets!


There may be a lot of reasons for which each class would need to evolve independently of each other!

How to avoid breaking LSP

  • Only use inheritance if it represents an IS-A relationship, in case of doubt use composition instead.
  • If you think the above is true also think in terms of invariants (behavior: preconditions and postconditions) to see if the relationship still makes sense.
  • If you need to try hard to hold the relationship, you are definitely doing it wrong!


ISP

Interface Segregation Principle
(or how simple should be really simple)
  • Make fine grained interfaces that are client specific

Tree Set

Tree Set is an implementation of a collection
that doesn't allow repeated objects and
uses a binary search tree to store its elements
so all the common operations take O(log n) time

What operations are desirable?

  • We need to add, remove, inquiry size and akin operations.
  • We need to be able to traverse it.
  • We need to be able to get the "first" and "last" element and akin operations.
  • We need to be able to implement operations like ceil and floor and akin operations.

So, should we put all
those operations in
a big clunky interface?

Of course not!

interfaces that TreeSet implements:

  • Collection
  • Set
  • Iterable
  • SortedSet
  • NavigableSet

Big Interfaces would probably
mean that a lot of the implementers
would override some methods
with an empty implementation
breaking SRP!

How to avoid breaking lSP:

  • When designing interfaces always
    strive to simplicity and think in
    terms of the clients of your code.


DIP

Dependency Inversion Principle
(or the art of accepting you are dealing with a fake)
  • Concrete classes should only depend on abstraction

The problem of compilation

  • When code is changed or added, we need to re-compile.
  • When a class is recompiled, every class that depends on it needs to be recompiled too and so on.
  • This ripple effect can significantly increased compilation time!

What's the problem?

  • (Much) less time coding.
  • Which makes tempting to not write
    or write fewer unit tests.
  • Can cause bad practices on the team!

How we decouple code?

  • Interfaces doesn't change that often, because they don't have implemented code so using interfaces decouples code.
  • Remember that from Java 8 onward they can, but that doesn't mean that you should start polluting interfaces with default methods.

Use Interfaces

  • As parameters types
  • As returned values
  • As attributes types
  • Use dependency injection


Q&A

References


Thanks!

  • @gaijinco

Principios S.O.L.I.D.

By Carlos Obregón

Principios S.O.L.I.D.

Descripción de los principios SOLID, esenciales para escribir buen código OOP

  • 1,848