CSC/ECE 517 Fall 2010/ch6 6f AZ

From Expertiza_Wiki
Jump to navigation Jump to search

Interface Segregation Principle-The Interface Segregation Principle states that "Clients should not be forced to depend upon interfaces that they do not use." Take the Interface Segregation principle and catalog the information on it available on the Web and in the ACM DL. Find good descriptions and good, concise, understandable examples.

Definition

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 helps 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 a major hindrance to further development.

The issue with their code was that there was one main Job class that was used by almost all of the tasks. This meant that the Job class was getting huge or 'fat', with tons of different methods which were specific to a variety of different clients. The clients, for example, staple job, were being forced to depend on methods specific to other clients, like print job, that they did not use.

The solution suggested by Robert has now become the Interface Segregation Principle. He suggested the addition of 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. This segregation of interfaces vastly decoupled the software allowing the clients like the Staple class to only depend on the methods it cared about [9].

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 problems [4]:

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.

Example 1

Without ISP

For example, a User Interface class that has a number of operations for each different screen that it can handle [4].

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.

Example 2

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 [6].

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();
    }
}

Example 3

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 [5].

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 is same example but now according to the Interface Segregation Principle [5]:

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);
      }
  }
}

Example 4

Without ISP

Suppose that in your application you are required to write some Data Access Objects (DAO),which should support a variety of data sources. Let the two main data sources be file and database. You must be careful enough to come up with an interface-based design, where the implementation of data access can be varied without affecting the client code using your DAO object. The following design is a good example of the above requirements [10].



What happens if the data source is read-only? The methods for inserting and updating data are not needed. On the other hand, if the DAO object should implement the DAO interface, it will have to provide a null implementation for those methods defined in the interface. This is still acceptable, but the design is gradually going wrong. What if there is a need to rotate the file data source to a different file once a certain amount of data has been written to the file? That will require a separate method to add to the DAO interface. This is just to add the flexibility to the clients using this FileDAO object to enable them to choose either the normal append feature to the file data source or to make use of the improved file rotation feature.

With the DatabaseDAO implementation now broken, it needs to be changed, to provide a null implementation of the new method added to the interface. This is against the Open-Closed Principle.


With ISP

In the basic design, the fact that the file data access operation and database access operation can differ fundamentally must be considered. Interfaces are the behaviors to be provided through particular objects. If two or more objects implementing the interface depict different sets of behaviors, then they probably cannot subscribe to a single interface.

The database access classes and file access classes should subscribe to two separate interfaces. The following design is obtained by applying the Interface Segregation Principle [10].



With this design, the Fat interface symptom is avoided and the interfaces clearly delineate their intended purpose. If any imaginary data access object requires a combination of operations defined in both of these interfaces, they will be able to do so by implementing both the interfaces.

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 [6].

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
  9. ISP Wiki
  10. Database ISP Example