Open-Closed Principle

🔗 SOLID Principles

Objects or entities should be open for extension, but closed for modification.

Let’s say we want to a way to show a progress bar to the user which represents the progress of a download.

We have a File class:

class File {
	/* both measured in bytes */
	public $length;
	public $sent;

}

And a Progress file to display the progress:

class Progress {

	protected $file;

	public function __construct(File $file) {
		$this->file = $file;
	}

	public function getProgress() {
		return $this->file->sent * 100 / $this->file->length;
	}

}

So far so good. The type-hinted constructor of Progress means I can’t input anything but a File, making the code safe to execute.

Changes to the Progress class

But let’s say our product matures, requirements change, and management wants to show a progress bar for different things, for example a song that is playing. We could represent that as a Music class, which also has $length and $sent. However, in this case, those two variables are not mearured in bytes, but in seconds.

Currently, our Progress class violates the Open-Closed Principle, because I can’t extend it easily to also show progress for my new Music class.

I could simply remove the typehint from the Progress class’ constructor, which would then also work for a Music instance.

The problem is this:

echo (new Progress('this is a string and not a class'))->getProgress();

Syntactically valid, but will result in a fatal error.

Better design

All classes that should be measurable by Progress have a requirement: they have to have a length variable and one to represent how much was sent already.

We can call this a contract and design our software like this:

graph TD
	A[Progress]-->|uses|B["Measurable [interface]"];
	C[File]-->|implements|B;
	D[Music]-->|implements|B;

^interface-graph

We have an interface which represents the contract that we described earlier:

interface Measurable
{
	public function getLength();
	public function setLength($length);
	public function getSent();
	public function setSent($sent);
}

Then we can modify both File and Music to adhere to that interface:

class File implements Measurable {
	public function getLength() { 
		return $this->bytes; 
	}

	public function setLength($length) {
		$this->bytes = $length;
	}

	public function getSent() {
		return $this->sentBytes;
	}

	public function setSent($sent) {
		$this->sentBytes = $sent;
	}
}

class Music implements Measurable {
	public function getLength() { 
		return $this->seconds; 
	}

	public function setLength($length) {
		$this->seconds = $length;
	}

	public function getSent() {
		return $this->sentSeconds;
	}

	public function setSent($sent) {
		$this->sentSeconds = $sent;
	}
}

And change Progress to use that interface, too:

class Progress {

	protected $measurableContent;

	public function __construct(Measurable $measurableContent) {
		$this->measurableContent = $measurableContent;
	}

	public function getProgress() {
		return $this->getMeasurableContent->getSent() * 100 / $this->measurableContent->getLength();
	}

}

This way, our Progress class adheres to the Open-Closed Principle. In the future, if we want to use the Progress class to also be able to measure new classes, like Fax or Torrent or Lifespan, these classes just have to agree to the contract described by the Measurable interface.

The Progress class is open for extension, but closed for modification.