CSC/ECE 517 Fall 2010/ch6 6f AZ

From Expertiza_Wiki
Revision as of 15:30, 15 November 2010 by Student (talk | contribs) (→‎Example2)
Jump to navigation Jump to search

Introduction

Interface Segregation Principle(ISP) states that "Clients should not be forced to depend upon interfaces that they do not use". Instead of one fat interface many small interfaces are preferred based on groups of methods, each one serving one submodule.

If followed, the ISP will help a system stay decoupled and thus easier to refactor, change, and redeploy. It is one of the 5 principles of Object-Oriented Programming called SOLID. This helps in low coupling and high cohesion.

Origin

The ISP was first used and formulated by Robert C. Martin when he was doing some consulting for Xerox. Xerox had created a new printer system which could perform a variety of tasks such as stapling a set of printed papers, faxing, and so forth. The software for this system was created from the ground up and performed its tasks successfully, but as the software grew it became harder and harder to change. Each time a change, even the smallest of changes, was made it could take nearly an hour to recompile and redeploy. This was making it near impossible to continue development, so they hired Robert to help them out.

The issue with their code was that there was one main Job class that was used by almost all of the tasks. Anytime a print job or a stapling had to be done, a call was made to some method in the Job class. This meant that the Job class was getting huge or 'fat', with of tons of different methods which were specific to a variety of different clients. This meant that a staple job would know about all the methods of the print job, even though the staple class had no use for them. The clients were being forced to depend on methods they did not use. When a developer had to change a small detail about a print job, every one of the classes that used the Job class would have to be recompiled.

The solution suggested by Robert has now become the Interface Segregation Principle. He suggested that they add a layer of interfaces to sit between the Job class and all of its clients. Instead of having just one 'fat' Job class that all the tasks used, there would be a Staple Job interface or a Print Job interface that would be used by the Staple class or Print class, respectively, and would call methods of the Job class. Ergo, there was an interface created for each job, and the Job class would inherit from all of these interfaces. This segregation of interfaces vastly decoupled the software allowing the clients like the Staple class to only depend on the methods it cared about. Thus if a developer made a change to the Print Job, the Staple Job would be unaffected and have no need to recompile.

Motivation

When we design an application we should take care how we are going to make abstract a module which contains several submodules. Considering the module implemented by a class, we can have an abstraction of the system done in an interface. But if we want to extend our application by adding another module that contains only some of the submodules of the original system, we are forced to implement the full interface and to write some dummy methods. Such an interface is named as a ‘fat’ interface or ‘polluted’ interface. Having a polluted interface is not a good solution and might induce inappropriate behavior in the system 6.

Need for ISP

Listed below are the problems caused when not using ISP, and how ISP helps solves these problem4:

Without ISP With ISP
Some interfaces are created with huge amount of functionalities. But when those interfaces are implemented by client implementor classes, not required functionalities are forced to be implemented, and so the code will have many dummy or empty implementations. This situation can be prevented by segregating (i.e seperating) big interfaces into smaller ones. Only strictly related method definitions must be in the same interface. Different types of functionalities must be placed in different interfaces. This is ISP.
The larger a component gets, the larger will be its interface. This can not only make it harder to understand the purpose of a component, but it can also cause increased coupling, where by components that make use of such a component are exposed to more of that component’s capabilities that are appropriate. ISP aims to tackle this problem by breaking a component’s interface into functionally separate sub-interfaces. Although a component may still end up with the same set of public members, those members will be separated into separate interfaces such that a calling component can operate on the component by referring only to the interface that concerns the calling component.

That way calling components will not be overwhelmed by irrelevant operations that have been separated out into other interfaces.

Examples

Listed below are a few examples.

Example1

Without ISP

For example, a User Interface class that has a number of operations for each different screen that it can handle4.

In this example each screen may have a number of display operations, but in such a class the sheer number of operations that the class offers could become unmanageable when it is not easy to see which operations belong to which screens.

As a result of this, problems may arise, should a developer working on the UI class unwittingly attempt to call a UI operation that belongs to a different screen to the one currently being displayed by the UI object.

This would be a breach of the Liskov Substitution Principle (LSP) and could lead to unexpected and unpredictable behaviour.

With ISP

In the example of a UI class, each operation will belong to a particular screen. Each screen that the UI class is to handle could have an individual UI screen interface defined for it, containing only those operations that are required for that screen.

When this is done it becomes clear to the developer using a UI object which operations are related to the screen currently being displayed, because the UI interface used in the variable declaration will only expose those operations that relate to that screen.

Example2

Without ISP

We have a Manager class which represent the person which manages the workers. And we have 2 types of workers some average and some very efficient workers. Both types of workers works and they need a daily lunch break to eat. But now some robots were used by the company to work as well , but they don't eat so, they don't need a lunch break. On one side the new Robot class need to implement the IWorker interface because robots work. On the other side, they don't have to implement it because they don't eat.

This is why in this case the IWorker is considered a polluted interface.

If we keep the present design, the new Robot class is forced to implement the eat method. We can write a dummy class which does nothing(let's say a lunch break of 1 second daily), and can have undesired effects in the application(For example, the reports seen by managers will report more lunches taken than the number of people)6.

interface IWorker {
    public void work();
    public void eat();
}

class Worker implements IWorker{
    public void work() {
        // ....working
    }
    public void eat() {
        // ...... eating in lunch break
    }
}

class SuperWorker implements IWorker{
    public void work() {
        //.... working much more
    }

    public void eat() {
        //.... eating in lunch break
    }
}

class Manager {
    IWorker worker;

    public void setWorker(IWorker w) {
        worker=w;
    }

    public void manage() {
        worker.work();
    }
}

With ISP

According to the Interface Segregation Principle, a flexible design will not have polluted interfaces. In our case the IWorker interface should be split in 2 different interfaces.

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.

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 lunch 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 lunch break
    }
}

class Manager {
    Workable worker;

    public void setWorker(Workable w) {
        worker=w;
    }

    public void manage() {
        worker.work();
    }
}

Example3

Without ISP

In the below example you see one class CarConfigurator interface implementing interface ICarConfigurator that is being used by three different classes; DrawExterior, DrawInterior and DrawEngine. All these different classes are using different functionality of the ICarConfigurator interface.This means that all of these classes are connected to each other and if there is a change for one class in the interface then this will affect all of them.

Interface: ICarConfigurator

namespace ISP
{
  public interface ICarConfigurator
  {
      string ExteriorColor { get; set; }
      string InteriorColor { get; set; }
      string EngineType { get; set; }
  }
}

Class: CarConfigurator : ICarConfigurator

namespace ISP
{
  public class CarConfigurator : ICarConfigurator
  {
      private string _ExteriorColor;
      private string _InteriorColor;
      private string _EngineType;

      public string ExteriorColor
      {
          get { return _ExteriorColor; }
          set { _ExteriorColor = value; }
      }
      public string InteriorColor
      {
          get { return _InteriorColor; }
          set { _InteriorColor = value; }
      }
      public string EngineType
      {
          get { return _EngineType; }
          set { _EngineType = value; }
      }
  }
}

Class: DrawExterior

namespace ISP
{
  public class DrawExterior
  {
      private readonly ICarConfigurator _Car;

      public DrawExterior(ICarConfigurator car)
      {
          _Car = car;
      }

      public void Draw()
      {
          Console.WriteLine("The car will have this exterior color: "+ _Car.ExteriorColor);
      }
  }
}

Class: DrawInterior

namespace ISP
{
  public class DrawInterior
  {
      private readonly ICarConfigurator _Car;

      public DrawInterior(ICarConfigurator car)
      {
          _Car = car;
      }

      public void Draw()
      {
          Console.WriteLine("The car will have this interior color: "+ _Car.InteriorColor);
      }
  }
}

Class: DrawEngine

namespace ISP
{
  public class DrawEngine
  {
      private readonly ICarConfigurator _Car;

      public DrawEngine(ICarConfigurator car)
      {
          _Car = car;
      }

      public void Draw()
      {
          Console.WriteLine("The car will have this engine: "+ _Car.EngineType);
      }
  }
}


With ISP

Below you will see the same example but now accordingly to the Interface Segregation Principle:

Interface: ICarExterior

namespace ISP
{
  public interface ICarExterior
  {
      string ExteriorColor { get; set; }
  }
}

Interface: ICarInterior

namespace ISP
{
  public interface ICarInterior
  {
      string InteriorColor { get; set; }
  }
}

Interface: ICarEngine

namespace ISP
{
  public interface ICarEngine
  {
      string EngineType { get; set; }
  }
}

Class: CarConfigurator : ICarExterior, ICarInterior, ICarEngine

namespace ISP
{
  public class CarConfigurator : ICarExterior, ICarInterior, ICarEngine
  {
      private string _ExteriorColor;
      private string _InteriorColor;
      private string _EngineType;

      public string ExteriorColor
      {
          get { return _ExteriorColor; }
          set { _ExteriorColor = value; }
      }
      public string InteriorColor
      {
          get { return _InteriorColor; }
          set { _InteriorColor = value; }
      }
      public string EngineType
      {
          get { return _EngineType; }
          set { _EngineType = value; }
      }
  }
}

Class: DrawExterior

namespace ISP
{
  public class DrawExterior
  {
      private readonly ICarExterior _Car;

      public DrawExterior(ICarExterior car)
      {
          _Car = car;
      }

      public void Draw()
      {
          Console.WriteLine("The car will have this exterior color: "+ _Car.ExteriorColor);
      }
  }
}

Class: DrawInterior

namespace ISP
{
  public class DrawInterior
  {
      private readonly ICarInterior _Car;

      public DrawInterior(ICarInterior car)
      {
          _Car = car;
      }

      public void Draw()
      {
          Console.WriteLine("The car will have this interior color: "+ _Car.InteriorColor);
      }
  }
}

Class: DrawEngine

namespace ISP
{
  public class DrawEngine
  {
      private readonly ICarEngine _Car;

      public DrawEngine(ICarEngine car)
      {
          _Car = car;
      }

      public void Draw()
      {
          Console.WriteLine("The car will have this engine: "+ _Car.EngineType);
      }
  }
}

Conclusion

If the design is already done, fat interfaces can be segregated using the Adapter pattern.

Like every principle Interface Segregation Principle is one principle which require additional time and effort spent to apply it during the design time and increase the complexity of code. But it produces a flexible design. If we are going to apply it more than is necessary it will result a code containing a lot of interfaces with single methods, so applying should be done based on experience and common sense in identifying the areas where code extensions are more likely to happen in the future.

References

  1. SOLID in OO Design
  2. Coupling in OO Design
  3. Code Balance - ISP Example
  4. ISP from an IT architecture perspective
  5. ISP Example
  6. Good explanation of ISP
  7. Cohesion in OO Design
  8. Liskov substitution principle