Tuesday, May 29, 2012

Object Oriented Design Principles

Object-oriented analysis and design:

This is a software engineering approach that models a system as a group of interacting objects. Each object represents some entity of interest in the system being modeled, and is characterized by its class, its state (data elements), and its behavior. Various models can be created to show the static structure, dynamic behavior, and run-time deployment of these collaborating objects

What are Software Design Principles?

Software design principles represent a set of guidelines that helps us to avoid having a bad design. Three important characteristics of a bad design that should be avoided:

Rigidity - It is hard to change because every change affects too many other parts of the system.
Fragility - When you make a change, unexpected parts of the system break.
Immobility - It is hard to reuse in another application because it cannot be disentangled from the current application.

Class Design principles:

  • Open Close Principle
  • Dependency Inversion Principle
  • Interface Segregation Principle
  • Single Responsibility Principle
  • Liskov's Substitution Principle

Open Close Principle

Software entities like classes, modules, functions should be open for extension but closed for modification.


// Open-Close Principle - Bad example
class GraphicEditor
{
    public void Draw(Shape s)
    {
        if (s.type == 1)
            DrawRectangle(s);
        else if (s.type == 2)
            DrawCircle(s);
    }

    public void DrawRectangle(Shape s){}
    public void DrawCircle(Shape s){}
}

class Shape { public int type;}

class Rectangle:Shape
{
    Rectangle() { base.type = 1; }
}

class Circle:Shape
{
    Circle() { base.type = 2; }
}

// Open-Close Principle - Good example
class GraphicEditor
    {
        public void Draw(Shape s) {s.Draw();}
    }

    abstract class Shape
    {
        public abstract void Draw();
    }

    class Rectangle : Shape
    {
        public override void Draw() { }
    }

    class Circle:Shape
    {
        public override void Draw() { }
    }

Key Points:
no unit testing required.
no need to understand the source code from Graphic Editor.
since the drawing code is moved to the concrete shape classes, it's a reduced risk to affect old functionality when new functionality is added.

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.


// Dependency Inversion Principle - Bad example
class Worker 
{
public void work() {// ....working}
}
class Manager 
{
public void manage(Worker w) {w.work();}
}
class SuperWorker 
{
public void work() {//.... working much more}
}


// Dependency Inversion Principle - Good example
interface IWorker 
{
public void work();
}
class Worker implements IWorker
{
public void work() {// ....working}
}
class SuperWorker  implements IWorker
{
public void work() {//.... working much more}
}
class Manager 
{
public void manage (IWorker w) {w. work();}
}


Manager class should not be changed.
minimized risk to affect old functionality present in Manager class.
no need to redone the unit testing for Manager class.


Interface segregation


Clients should not be forced to depend upon interfaces that they don't use


// interface segregation principle - bad example
interface IWorker 
{
public void work();
public void eat();
}
class Worker implements IWorker
{
public void work(){// ....working}
public void eat() {// ...... eating in launch break}
}
class SuperWorker implements IWorker
{
public void work() {//.... working much more}
public void eat() {//.... eating in launch break}
}
class Manager 
{
IWorker worker;
public void setWorker(IWorker w) {worker=w;}
public void manage() {worker.work(); }
}


// interface segregation principle - good example
interface IWorker extends Feedable, Workable 
{
}
interface IWorkable 
{
public void work();
}
interface IFeedable
{
public void eat();
}
class Worker implements IWorkable, IFeedable
{
public void work() {// ....working}
public void eat() {//.... eating in launch break}
}
class Robot implements IWorkable
{
public void work() {// ....working}
}
class SuperWorker implements IWorkable, IFeedable
{
public void work() {//.... working much more}
public void eat() {//.... eating in launch break}
}
class Manager 
{
Workable worker;
public void setWorker(Workable w) {worker=w;}
public void manage() {worker.work();}
}


Following it's the code supporting the Interface Segregation Principle. By splitting the IWorker interface in 2 different interfaces the new Robot class is no longer forced to implement the eat method. Also if we need another functionality for the robot like recharging we create another interface IRechargeble with a method recharge


Single responsibility



A class should have only one reason to change.


// single responsibility principle - bad example


interface IEmail 
{
public void setSender(String sender);
public void setReceiver(String receiver);
public void setContent(String content);
}
class Email implements IEmail 
{
public void setSender(String sender) {// set sender; }
public void setReceiver(String receiver) {// set receiver; }
public void setContent(String content) {// set content; }
}


// single responsibility principle - good example
interface IEmail 
{
public void setSender(String sender);
public void setReceiver(String receiver);
public void setContent(IContent content);
}
interface Content 
{
public String getAsString(); // used for serialization
}
class Email implements IEmail 
{
public void setSender(String sender) {// set sender; }
public void setReceiver(String receiver) {// set receiver; }
public void setContent(IContent content) {// set content; }
}


Key Points:
adding a new protocol causes changes only in the Email class.
adding a new type of content supported causes changes only in Content class.


Liskov’s substitution

Derived types must be completely substitutable for their base types.


// Violation of Likov's Substitution Principle
class Rectangle
{
protected int m_width;
protected int m_height;


public void setWidth(int width){m_width = width;}
public void setHeight(int height){m_height = height; }
public int getWidth(){return m_width;}
public int getHeight(){ return m_height;}
public int getArea(){return m_width * m_height;}
}
class Square extends Rectangle 
{
public void setWidth(int width){m_width = width;m_height = width; }
public void setHeight(int height){m_width = height;m_height = height;}
}
class LspTest
{
private static Rectangle getNewRectangle()
{
// it can be an object returned by some factory ... 
return new Square();
}


public static void main (String args[])
{
Rectangle r = LspTest.getNewRectangle();
        
r.setWidth(5);
r.setHeight(10);
// user knows that r it's a rectangle. 
// It assumes that he's able to set the width and height as for the base class


System.out.println(r.getArea());
// now he's surprised to see that the area is 100 instead of 50.
}
}


Below is the classic example for which the Likov's Substitution Principle is violated. In the example 2 classes are used: Rectangle and Square. Let's assume that the Rectangle object is used somewhere in the application. We extend the application and add the Square class. The square class is returned by a factory pattern, based on some conditions and we don't know the exact what type of object will be returned. But we know it's a Rectangle. We get the rectangle object, set the width to 5 and height to 10 and get the area. For a rectangle with width 5 and height 10 the area should be 50. Instead the result will be 100



1 comment:

  1. Explained well in brief Tanmay.. :) How about a post on Design patterns?

    ReplyDelete