A good example may be a system where you need to generate reports.
Let’s say you need to generate reports in a csv format. You have no idea what future reports you may need to write. You also have no idea whether the format will ever change (for example, whether the same report should be available say in xml or json format).
I was given this task a while back - my manager wanted to be able to create a few csv reports. However, I know from experience that there’s always the possibility that they may want reports in these other formats in the future.
Also we have another issue - how do we guarantee that we can add new reports without breaking old ones? It would be nice to know that we can achieve these two aims then:
- Have a way of making reports compatible with different file formats in the future (even if we don’t need to implement this right now).
- Make sure that we can add new reports and know for certain that we have not broken any existing reports.
Polymorphism and Interfaces/Abstract classes present an excellent way to design this. The reason for this is because we can use interfaces to design our entities in a way that makes them compatible with one another, and we can use these interfaces to make sure that future objects adhere to this pre-planned design. It’s a way of forcing the system to work a set way, rather than allowing things to be done in a willy-nilly fashion.
So let’s think about this.
Here’s a good way to start about doing something like this (I’ve found this method helps me) - let’s start at the end, instead of the beginning. By this I mean let’s show a use case of what we want the final result to look like for generating a report.
//let's create a user invoice report:
$report = new UserInvoiceReport();
//let's output this report as a csv to the browser:
$csvReportWriter = new CsvReportWriter();
$csvReportWriter->setReport($report);
$csvReportWriter->output(); //outputs a csv report to the screen
But now we want to be able to output this same report in a json format. Let’s see how that looks:
//let's create a user invoice report:
$report = new UserInvoiceReport();
//let's output this report in a json format:
$jsonReportWriter = new JsonReportWriter();
$jsonReportWriter->setReport($report);
$jsonReportWriter->output(); //outputs a json formatted report to the screen
Say we now want to output a different report in a json format. Let’s say we want to output a Sales Stats report:
//let's create a user invoice report:
$report = new SalesStatsReport();
//let's output this report in a json format:
$jsonReportWriter = new JsonReportWriter();
$jsonReportWriter->setReport($report);
$jsonReportWriter->output(); //outputs a json formatted report to the screen
So from the above code, we can see that we have a way of ensuring that ReportWriter objects are compatible with Report objects, and we can see that the data that comes from each report is contained (encapsulated) within an Object context - this is what ensures a new report cannot break an old report. If we messed up and there’s an error in our SalesStatsReport() object, this would make no difference to the UserInvoiceReport object - all other reports would simply carry on working as normal. It’s also much cleaner and easier to work with for the future. Need a new report? Just create a new Report object and it will instantly be compatible with all other report writer objects. So once the new report is written, it will be available in all the formats we already have.
So how does this all work?
Firstly we have an interfaces for reports.
Let’s do something like this:
interface Report{
public function getData();
}
Then we either want an interface or an Abstract class for the report writer. Let’s write it like an Abstract class:
abstract class ReportWriter{
/**
* @var Report
*/
protected $_report;
protected $_reportData;
public function setReport(Report $report){
$this->_report = $report;
$this->_report->getData();
}
abstract public function output();
}
So the above code creates the setReport() method for all our ReportWriter objects. This method will mean that all report writers will automatically have a report object available in $this->_report, and will have the data from the report available in $this->_reportData. Notice how we keep the concept of the report abstract right now (We don’t ask for specific reports, just a ‘Report’ type of object). Our setReport object will not allow us to pass anything other than a Report object to it, so we’re forcing the code to work a certain way now. The Report interface above is what ensures that all reports are compatible with this method.
Now we can write a specific instance of a report, and a specific report writer to go with it.
I’m not going to write all the implementation code here, but in general it’d be like this:
class UserInvoiceReport implements Report{
public function getData(){
//code that queries the database and returns the data goes here
return $data;
}
}
Because we’ve implemented the Report interface, we are forced to create the getData() method, which is used later by our ReportWriters’ setReport() method.
Now in this instance we’d want to go maybe a bit further and perhaps return an object from the getData() method - if we return a ReportData object, we can ensure the data is available in a certain set way that can then be used by the report writers you see. I’ve not gone that far here because I’m just demonstrating the point. You could even just return an array format that is always going to be the same or something like that, but an object (enforced by an interface) would be best as it can force the data to come back in a set way.
So the writer code would be something like:
class CsvReportWriter extends ReportWriter{
public function output(){
//logic to output to a csv format goes here
//remember, this class has access to $this->_report + $this->_reportData automatically
}
}
This would ensure that when you create future reports, they are compatible with CsvReportWriter objects and any other report writer object you eventually create. So just by having a few interfaces and an Abstract class, we’ve made our system much easier to maintain and scale for future cases.
Does this help?