Object-oriented – Xerox SOLID example in PHP

object-orientedPHPsolid

There is a good example on Wikipedia regarding the violation of SOLID principles.

The ISP was first used and formulated by Robert C. Martin while
consulting for Xerox. Xerox had created a new printer system that
could perform a variety of tasks like stapling a set of printed papers
and faxing. The software for this system was created from the ground
up and performed its tasks successfully. As the software grew, making
modification became more and more difficult so that even the smallest
change would take a redeployment cycle of an hour. This was making it
near impossible to continue development. The design problem was that
one main Job class was used by almost all of the tasks. Anytime a
print job or a stapling job had to be done, a call was made to some
method in the Job class. This resulted in a huge or 'fat' class with
multitudes of methods specific to a variety of different clients.
Because of this design, a staple job would know about all the methods
of the print job, even though there was no use for them. The solution
suggested by Martin is what is called the Interface Segregation
Principle today. Applied to the Xerox software, a layer of interfaces
between the Job class and all of its clients was added using the
Dependency Inversion Principle. Instead of having one large Job class,
a Staple Job interface or a Print Job interface was created that would
be used by the Staple or Print classes, respectively, calling methods
of the Job class. Therefore, one interface was created for each job,
which were all implemented by the Job class.

http://en.wikipedia.org/wiki/Interface_segregation_principle

I tried to find a good PHP solution for this, I got this far:

 class Job implements StampleJob, PrintJob {
 }

 class Print {
   protected $objPrintJob;
   public function __construct(PrintJob $objPrintJob) {
     $this->objPrintJob = $objPrintJob;
   }
 }

 class Staple {
   protected $objStapleJob;
   public function __construct(StapleJob $objStapleJob) {
     $this->objStapleJob = $objStapleJob;
   }
 }

I can understand how the interface will limit knowledge but this will not actually change the big Job class or remove the SRP violation. Can you clarify how this solution actually solves the problem: "The design problem was that
one main Job class was used by almost all of the tasks"?

Best Answer

First, lets understand what was the issue: The Staple job knows about many more methods which it doesn't use today, (e.g., the print method) but which are available for it to use, should it want to. However, the Job class "thinks" that the Staple class will be "a good citizen" and never use the print method at all.

There are many potential big issues here - For some reason, the Staple job may start using the print method - by accident or intentionally.

Then down the road, either any changes to the print method may go untested,
OR, any changes to the print method will trigger a regression test in the Staple job also,
AND, any impact analysis for changes to the print job will necessarily involve impact analysis of the staple job too.

This is just the issue of Staple knowing about the print functions. Then there's the case of the Print job knowing all about stapling functions. Same problems.

Very soon, this system would reach a point where any change will require a full blown impact analysis of each module and a full blown regression test.

Another problem is that today, all jobs which can be printed can be stapled, and vice versa on this particular printer.

However, tomorrow, there could be a need to install the same firmware on a device that only prints or only staples. What then? The code already assumes that all Jobs are printable and stapleable. So any further granular breakdown / simplification of responsibilities is impossible.

In more recent terms, imagine a class called "AppleDevice" which has functions for MakePhoneCall as well as PlayMusic. Now your problem is while you can easily use this on an iPhone, you cannot use it for an iPod since the iPod cannot make phone calls.

So, the issue is not that the Job class is all-powerful. In fact, that's how it should be, so that it can act as a common link in the entire "workflow" where someone may scan a job, then print it, then staple it etc. The problem is that the usage of all its methods is not restricted. Anyone and everyone could use and abuse any method whenever they want to, thus making the maintenance difficult.

Hence, the Dependency Injection approach of only telling users "exactly what they need to know, and nothing more" ensures that calling modules only use the code that they are meant to. A sample implementation would look like:

interface IStapleableJob { void stapleYourself(); }
interface IPrintableJob { void printYourself(); }
class Job implements IStapleableJob, IPrintableJob {
....
}

class Staple {
  public static void stapleAllJobs(ArrayList<IStapleableJob> jobs) {
    for(IStapleableJob job : jobs) job.stapleYourself();
  }
}

class Print {
  public static void stapleAllJobs(ArrayList<IPrintableJob> jobs) {
    for(IPrintableJob job : jobs) job.printYourself();
  }
}

Here, even if you pass a Job object to the Staple and Print methods, they dont know that its a Job, so they cannot use any methods that they are not supposed to. Thus, when you make any changes to a module, your scope of impact analysis and regression testing is restricted. That's the problem that ISP solves.

Related Topic