The call to Adventure:
SOLID principles
The Ordinary World
Types
Hero's Armor
OOP should be
all about types!
OOP design is awesome when is well done,
and that has usually
to do with types!
Encapsulation
Hero's Shield
Encapsulation
- Access modifiers
- Types has behavior
You can hide implementation details under access modifiers
and expose, what you need, with methods.
Fraction oneHalf = new Fraction(1, 2);
Fraction twoThirds = new Fraction(2, 3);
Fraction sum = oneHalf.sum(twoThirds);
What do you think are the attributes of Fraction?
Two int's? A two-sized
array of int? A map?
You don't need to care!
You only care about Fraction's API
You are free to change
the internals of your
class without
breaking client code.
Also classes
have invariants.
Having encapsulation makes them easier to maintain because you
have greater control
of an object's state.
Regarding access
control the golden rule is: make everything as restricted as possible.
Inheritence
Hero's Sword
Inheritance is a
mechanism to express
an IS-A relationship ...
... which peripherally let you re use code, but that's just a nice coincidence.
We would return to Inheritance later.
Polymorphism
Hero's Magic
Polymorphism is
the feature that let's
you have a reference
of a type T refer to any object of a subtype of T
List<String> data = new ArrayList<>();
Ship s = new Battleship();
Link cta = new EmailLink();
DateFormat formatter = new SimpleDateFormat();
The type of the reference, left, doesn't need to be the same of the object, right.
As long as the type of the object
is in the inheritance tree of the
type of the reference
Polymorphism is
the key to the Kingdom!
public void foo(List<String> foo) {
// this method will work with any
// implementation of List, even
// if it hasn't been created
}
public List<String> foo() {
// this method will return a List
// but you shouldn't worry about which.
// In fact it can change in the future!
// But it'll always be a List
}
public class Foo {
// This attribute is a List
// but it's implementation can change
private List<String> data;
}
public class PersonService {
private PersonDAO dao;
public PersonService(final PersonDAO dao) {
// I can inject the implementation I need!
this.dao = dao;
}
}
How do you define a
Type regardless of its implementation?
Use Interfaces for types so you have the flexibility that brings Polymorphism.
If you have an interface
and more than one implementation, move common code to an Abstract Class.
Tools of the Hero
- Types
- Encapsulation
- Inheritance
- Interfaces,
Abstract Classes - Polymorphism
Are you ready for
the adventure?
Single Responsibility Principle - SRP
A class should only
have one responsibility
to change
A class should have
high cohesion
Every method related to one functionality should be in the same class.
Methods related
to different
functionalities should
be on their own class.
public class ContactServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res) {
String id = req.getParameter("id");
Contact contact = this.fetchFromDatabase(id);
String message = String.format("%s - %s", id, contact);
res.getWriter().println(message);
}
}
Reasons to change
- Changes in parameters
- Changes in the DB
- Changes in the output format
public class ContactServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res) {
String id = re.getParameter("id");
Contact contact = service.findById(id);
res.setAttribute("contact", contact);
RequestDispatcher dispatcher = getServletContext()
.getRequestDispatcher(CONTACT_VIEW);
dispatcher.forward(request, response);
}
}
Reason to change
- Changes in the parameters
public class Fraction {
private int numerator;
private int denominator;
public Fraction(int numerator, int denominator) {
final int GCF = gcf(numerator, denominator);
this.numerator = numerator / GCF;
this.denominator = denominator / GCF;
}
private static int gcf(int a, int b) {
return b == 0 ? a : gcf(b, a % b);
}
public int getNumerator() {
return numerator;
}
public int getDenominator() {
return denominator;
}
}
Does it break SRP?
Yes!
Reason to Change
- Changes in Fraction implementation
- Changes in gcf() implementation
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;
}
public int getNumerator() {
return numerator;
}
public int getDenominator() {
return denominator;
}
}
public class final ArithmeticOperations {
private ArithmeticOperations() {
throw new AssertionError("This class shouldn't be instantiated");
}
public static int gcf(int a, int b) {
return b == 0 ? a : gcf(b, a % b);
}
}
Beware of static
methods because they usually break OOP
If you can think of
a Type where a static method can be, it usually means it should be!
Cohesion also has to do with having all functionality in the appropriate Type.
public class final FractionOperations {
private FractionOperations() {
throw new AssertionError("This class shouldn't be instantiated");
}
public static Fraction sum(Fraction this, Fraction that) {
final int LCM = ArithmeticOperations.lcm(this.getDenominator(), that.getDenominator());
final int numerator = LCM / this.getDenominator() * that.getNumerator()
+ LCM / that.getNumerator() * this.getNumerator();
return new Fraction(numerator, LCM);
}
}
Adding 2 fractions has
a lot to do with the implementation, isn't?
If I'm a fraction I
should know how
to add me another one.
public class Fraction {
// code
public Fraction sum(Fraction that) {
final int LCM = ArithmeticOperations.lcm(this.denominator, that.denominator);
final int numerator = LCM / this.denominator * that.numerator
+ LCM / that.denominator * this.numerator;
return new Fraction(numerator, LCM);
}
}
Beware of God Classes, see how methods related to attributes of a class.
If a method doesn't use much of the attributes of a class then maybe it should be in a new Type.
Open/Closed Principle - OCP
A class should be open
for extension but closed
for modification
If you have existing
code that's working,
how can you extend it without breaking
existing functionality?
Unit Tests help, but you can design in a way that you don't depend on them!
How can you reuse code?
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double distance(Point p) {
return Math.sqrt(Math.pow(x - p.x, 2)
+ Math.pow(y - p.y, 2))
}
}
If I have a simulation
traffic I could need a
Type vehicle that occupies a place in a grid system.
public class Vehicle {
// more code
private Point location;
// more code
public double distance(Vehicle v) {
// reuse of distance code from Point
return location.distance(v.location);
}
}
Delegation: implementing a method by just calling a method on another object
Delegation can be expanded and in fact a lot of Design Patterns use it!
The easiest way to apply OCP is with Design Patterns: Decorator, State / Strategy, Template, etc.
Another small thing is to be wary of using instanceof
public Square implements Shape {
// code
public double getLength() {
return this.length;
}
}
public Circle implements Shape {
// code
public double getRadius() {
return this.radius;
}
}
public double sumOfAreas(List<Shape> shapes) {
double total = 0.0;
for (Shape s : shapes) {
if (s instanceof Square) {
Square sq = (Square) s;
total += sq.getLength() * sq.getLength();
} else if (s instanceof Circle) {
Circle c = (Circle) s;
total += c.getRadius() * c.getRadius() * Math.PI;
}
}
return total;
}
If I add a new shape,
then the sumOfAreas
will need to add a
new instanceof test
I should use Types!
public interface Shape {
// code
double getArea();
}
public class Square implements Shape {
// code
public double getArea() {
return this.length * this.length;
}
}
public class Circle implements Shape {
// code
public double getArea() {
return this.radius * this.radius * Math.PI;
}
}
public double sumOfAreas(List<Shape> shapes) {
double total = 0.0;
for (Shape s : shapes) {
total += s.getArea();
}
return total;
}
Now new Shapes can
be created and sumOfAreas() will still work
instanceof has its uses
but try to use polymorphism first
Liskov Substitution Principle - LSP
A method that expects a type T should work with a type S if its a subtype of T
In the end what it means
is that inheritance is powerful but you
need to not abuse it.
Inheritance always define an IS-A relationship, don't use it for code reuse.
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double distance(Point p) {
return Math.sqrt(Math.pow(x - p.x, 2)
+ Math.pow(y - p.y, 2))
}
}
public class Vehicle extends Point {
// Vehicle now has a distance() method
}
A Vehicle IS NOT a Point
What could go wrong? There's a strong coupling between classes.
If you are considering Inheritance, try to use Delegation first, if it
doesn't work then use it.
At school we learn that
a Square is a special
case of a Rectangle.
public class Rectangle {
private double a;
private double b;
public double getA() {
return a;
}
public void setA(double a) {
this.a = a;
}
public double getB() {
return b;
}
public void setB(double b) {
this.b = b;
}
}
public void foo(Rectangle r) {
r.setA(5.0);
r.setB(4.0);
r.getA(); // it's 5.0
}
public class Square extends Rectangle {
// Square has two attributes, but
// it should have only one right?
@Override
public void setA(double a) {
this.a = a;
this.b = a; // b and a should be equal
}
@Override
public void setB(double b) {
this.b = b;
this.a = b; // b and a should be equal
}
}
Square s = new Square();
foo(s);
* * *
public void foo(Rectangle r) {
r.setA(5.0);
r.setB(4.0); // a also set a to 4.0
r.getA(); // it's not longer 5.0!
}
If you use inheritance and you need to do weird things in your code, you are doing it wrong!
Inheritance, when done correctly is cool!
Servlet, GenericServlet, HttpServlet, MyServlet
Don't satanize Inheritance, learn when and how
to use it.
Interface Segregation Principle - ISP
No client should
be forced to depend
on methods it
does not use
In Java there's a TreeSet class which is: a collection, a set, sorted, which let's you do range searches.
There are 4 interfaces for each of those features: Collection, Set,
SortedSet, NavigableSet
Collection has 13 methods.
Set doesn't adds any.
SortedSet adds 6.
NavigableSet adds 14.
Don't have 1 big fat interface, because implementers
will probably have
empty methods.
Dependency Inversion
Principle - DIP
-
High-level modules shouldn't depend on low level modules. Both should depend on abstraction.
-
Abstractions should not depend on details. Details should depend on abstractions.
In OOP, abstractions
usually means Interface.
Return values of
public methods
should be Interfaces.
Parameters of public methods/constructors
should be Interfaces.
Attributes of classes
should be interfaces (*)
When I say "should" I
don't mean "they always have to", but if you don't follow that advice learn what you are missing
Other useful Non-SOLID principles
Don't Repeat Yourself - DRY
Every piece of knowledge must have a single, unambiguous, authoritative representation within
a system
If I want to make a
fix / change for some functionality, there should be only one place to do it.
Duplication can range from obvious to non-trivial
public GridList(SlingHttpServletRequest request) {
ValueMap properties = request.getResource().adaptTo(ValueMap.class);
String leftLink = properties.get("leftlink", "");
String leftTitle = properties.get("lefttitle", "");
this.leftGridBlock = new GridBlockDialog(leftLink, leftTitle);
String rightLink = properties.get("rightlink", "");
String rightTitle = properties.get("righttitle", "");
this.rightGridBlock = new GridBlockDialog(rightLink, rightTitle);
}
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];
}
Try to use arrays to
your advantage
public GridList(SlingHttpServletRequest request) {
ValueMap properties = request.getResource().adaptTo(ValueMap.class);
this.leftGridBlock = createDialog(properties, "left");
this.rightGridBlock = createDialog(properties, "right");
}
Or use functions
You Aren't Gonna Need It - YAGNI
Only code what you need! Don't code something because you think
you'll need it.
You can waste a lot of
time doing something
that nobody will ever use.
Be very careful about setters! There's some frameworks / libraries that need them, and that's it.
Adventurer's
Tricks of the Trade
-
Create new Types
-
Use Interfaces to define Types
-
Don't overuse static
-
Don't overuse instanceof
-
Learn Design Patterns
-
Use Inheritance only
for IS-A relationships
-
Prefer Delegation over Inheritance
-
Use Interfaces for Return Values and Parameters of public method. Also for attributes
-
Avoid duplication
-
Don't code something unless you need it now!
-
Don't be afraid
to ask questions!
Q&A
Thanks!
@gaijinco
The call to Adventure: SOLID principles
By Carlos Obregón
The call to Adventure: SOLID principles
- 1,536