In the early days of PHP programming, PHP code was essentially process-oriented. Process code is characterized by the use of process to build application blocks. Procedures provide some degree of reuse by allowing calls between procedures.
However, without an object-oriented language construction, programmers can still introduce OO features into PHP code. This is a bit difficult and makes the code difficult to read because it is a hybrid paradigm (procedural language containing pseudo-OO design). Using OO constructs in PHP code—such as being able to define and use classes, being able to build relationships between classes that use inherited, and being able to define interfaces—make easier to build code that conforms to excellent OO practices.
Although the pure process design without too much modularity runs well, the advantages of OO design lies in maintenance. Since most of the life cycle of a typical application is spent on maintenance, code maintenance is an important part of the application life cycle. And code maintenance is easily forgotten during development. If there is competition in application development and deployment, long-term maintainability may be placed in a relatively minor position.
Modularity — one of the main features of excellent OO design — can help with such maintenance. Modularity will help encapsulate changes, which makes it easier to scale and modify applications over time.
In general, although there are more than 7 habits to build OO software, following the 7 habits here can make the code comply with basic OO design standards. They will provide you with a stronger foundation, build more OO habits and build software that can be easily maintained and expanded. These habits target several key features of modularity. For more information on the advantages of language-independent OO design, see References.
7 excellent PHP OO habits include:
Stay modest.
Be a good neighbor.
Avoid seeing Medusa.
Take advantage of the weakest links.
You are rubber; I am glue.
Restrict transmission.
Consider using mode.
Stay humble
Staying modesty means avoiding exposing yourself in class implementations and function implementations. Hiding your information is a basic habit. If you cannot develop the habit of hiding implementation details, it will be difficult to develop any other habits. Information hiding is also called encapsulation.
There are many reasons why it is a bad habit to expose public fields directly, and the most important reason is that you don't have the option you deserve in implementing changes. Use the OO concept to isolate changes, and encapsulation plays an integral role in ensuring that changes are not essentially viral changes. Viral changes are small changes at the beginning — such as changing an array that holds three elements to an array containing only two elements. Suddenly, you find that you need to change more and more code to adapt to changes that should be very trivial.
An easy way to start hiding information is to keep the fields private and expose them with a public access method, just like a window in your home. Instead of opening the entire wall to the outside, just one or two windows open (I will introduce more information about access methods in "Good Habits: Using Public Access Methods".
In addition to allowing your implementation to be hidden after the changes, using public access methods instead of directly exposing fields will allow you to build on the basis of the basic implementation, which overrides the implementation of the access method to perform behavior slightly different from the parent method. It also allows you to build an abstract implementation so that the actual implementation is delegated to the class that covers the basic implementation.
Bad habit: open public fields
In the bad code example in Listing 1, fields of Person objects are exposed directly as public fields rather than using access methods. While this behavior is very attractive, especially for lightweight data objects, it will limit you.
Listing 1. Bad habit of exposing public fields
<?php
class Person
{
public $prefix;
public $givenName;
public $familyName;
public $suffix;
}
$person = new Person();
$person->prefix = "Mr.";
$person->givenName = "John";
echo($person->prefix);
echo($person->givenName);
?>
If there is any change to the object, all code using that object needs to be changed as well. For example, if someone's teaching, last name, and other names are encapsulated into a PersonName object, all code needs to be modified to accommodate the changes.
Good habit: Use public access methods
By using excellent OO habits (see Listing 2), the same object now has private fields instead of public fields, and is cautiously exposed to the outside world through a get and set public method called access methods. These access methods now provide a public way to get information from PHP classes, so that when the implementation changes, the need to change all code using the class is likely to become smaller.
Listing 2. Good habits to use public access methods
<?php
class Person
{
private $prefix;
private $givenName;
private $familyName;
private $suffix;
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
public function getPrefix()
{
return $this->prefix;
}
public function setGivenName($gn)
{
$this->givenName = $gn;
}
public function getGivenName()
{
return $this->givenName;
}
public function setFamilyName($fn)
{
$this->familyName = $fn;
}
public function getFamilyName()
{
return $this->familyName;
}
public function setSuffix($suffix)
{
$this->suffix = $suffix;
}
public function getSuffix()
{
return $suffix;
}
}
$person = new Person();
$person->setPrefix("Mr.");
$person->setGivenName("John");
echo($person->getPrefix());
echo($person->getGivenName());
?>
At first glance, this code might do a lot of work and might actually be more of a front-end job. However, generally, using good OO habits is very cost-effective in the long run, as it will greatly consolidate future changes.
In the version of the code shown in Listing 3, I have changed the internal implementation to use the associative array of the name component. Ideally, I would like to have error handling and check more carefully for elements to exist, but the purpose of this example is to show how much the code using my class does not need to change - the code does not notice that the class changes. The reason to remember to adopt OO habits is to be careful to encapsulate changes so that the code will be more scalable and easier to maintain.
Listing 3. Another example using different internal implementations
Code
<?php
class Person
{
private $personName = array();
public function setPrefix($prefix)
{
$this->personName['prefix'] = $prefix;
}
public function getPrefix()
{
return $this->personName['prefix'];
}
public function setGivenName($gn)
{
$this->personName['givenName'] = $gn;
}
public function getGivenName()
{
return $this->personName['givenName'];
}
/* etc... */
}
/*
* Even though the internal implementation changed, the code here stays exactly
* the same. The change has been encapsulated only to the Person class.
*/
$person = new Person();
$person->setPrefix("Mr.");
$person->setGivenName("John");
echo($person->getPrefix());
echo($person->getGivenName());
?>
Be a good neighbor
When building a class it should handle its own errors correctly. If the class does not know how to handle the errors, the errors should be encapsulated in the format that its caller understands. In addition, avoid returning empty objects or objects with invalid states. Many times, this can be achieved by simply checking the parameters and throwing a specific exception to indicate the reason why the parameters are invalid. It can save you a lot of time when you develop this habit—and the people who maintain code or use objects.
Bad habit: Don't handle errors
Consider the example shown in Listing 4, which will take some parameters and return a Person object filled with some values. However, in the parsePersonName() method, there is no verification that the provided $val variable is empty, is a zero-length string or is the string in an unparsable format. The parsePersonName() method does not return a Person object, but returns null. Administrators or programmers using this approach may find it troublesome — at least they need to start setting breakpoints and debugging PHP scripts now.
Listing 4. Bad habit of not throwing or handling errors
class PersonUtils
{
public static function parsePersonName($format, $val)
{
if (strpos(",", $val) > 0) {
$person = new Person();
$parts = split(",", $val); // Assume the value is last, first
$person->setGivenName($parts[1]);
$person->setFamilyName($parts[0]);
}
return $person;
}
}
The parsePersonName() method in Listing 4 can be modified to initialize the Person object outside the if condition, ensuring that a valid Person object is always obtained. However, what you get is Person without the set attribute, which still doesn't improve your dilemma well.
Good habit: Each module handles its own errors
Don't let the caller guess out of thin air, but pre-verify the parameters. If an unset variable cannot generate a valid result, check the variable and throw an InvalidArgumentException. If the string cannot be empty or must be of a specific format, check the format and throw an exception. Listing 5 explains how to create exceptions and some new conditions in the parsePerson() method that demonstrates some basic validation.
Listing 5. Good habit of throwing errors
<?php
class InvalidPersonNameFormatException extends LogicException {}
class PersonUtils
{
public static function parsePersonName($format, $val)
{
if (! $format) {
throw new InvalidPersonNameFormatException("Invalid PersonName format.");
}
if ((! isset($val)) || strlen($val) == 0) {
throw new InvalidArgumentException("Must supply a non-null value to parse.");
}
}
}
?>
The ultimate goal is to hope that people can use your class without having to understand how it works. If the method they are using is incorrect or not as expected, there is no need to guess why it doesn't work. As a good neighbor, you need to know that the person who reuses your class does not have special features, so you need to solve the guesswork.
Avoid seeing Medusa
When I first learned about the OO concept, I was very skeptical if the interface really helped. My colleague gave me an example, saying that if you don't use the interface, it's like seeing Medusa's head. In Greek mythology, Medusa is a female monster with snake hair. Anyone who glances at her will turn into a stone. Pelhus, who killed Medusa, was able to fight her by observing her shadow on the shield, avoiding turning into a stone.
The interface is the mirror to deal with Medusa. When you use a specific concrete implementation, the code must also change as the implementation code changes. Using the implementation directly will limit your choices because you have essentially turned the class into a "stone".
Bad habit: not using interfaces
Listing 6 shows an example of loading Person objects from the database. It will take the name of the person and return the matching Person object in the database.
Listing 6. Bad habit of not using interfaces
<?php
class DBPersonProvider
{
public function getPerson($givenName, $familyName)
{
/* go to the database, get the person... */
$person = new Person();
$person->setPrefix("Mr.");
$person->setGivenName("John");
return $person;
}
}
/* I need to get person data... */
$provider = new DBPersonProvider();
$person = $provider->getPerson("John", "Doe");
echo($person->getPrefix());
echo($person->getGivenName());
?>
The code that loads Person from the database will work properly before the environment changes. For example, loading Person from a database might work for the first version of the application, but for the second version, it might be necessary to add the ability to load people from a web service. In fact, this class has become "stone" because it is directly using the implementation class and now there are very limited changes that can be made.
Good habit: use interfaces
Listing 7 shows a code example that has not been changed after implementing a new method to load the user. This example shows an interface called PersonProvider that declares a single method. If any code uses PersonProvider, the code prohibits the direct use of the implementation class. Instead, it uses PersonProvider just like an actual object.
Listing 7. Good habits to use interfaces
<?php
interface PersonProvider
{
public function getPerson($givenName, $familyName);
}
class DBPersonProvider implements PersonProvider
{
public function getPerson($givenName, $familyName)
{
/* pretend to go to the database, get the person... */
$person = new Person();
$person->setPrefix("Mr.");
$person->setGivenName("John");
return $person;
}
}
class PersonProviderFactory
{
public static function createProvider($type)
{
if ($type == 'database')
{
return new DBPersonProvider();
} else {
return new NullProvider();
}
}
}
$config = 'database';
/* I need to get person data... */
$provider = PersonProviderFactory::createProvider($config);
$person = $provider->getPerson("John", "Doe");
echo($person->getPrefix());
echo($person->getGivenName());
?>
When using an interface, try to avoid direct reference to the implementation class. Instead, using content outside the object can provide the correct implementation. If your class will load implementations based on some logic, it still needs to get the definition of all implementation classes, and doing that won't achieve any effect.
You can use Factory pattern to create an instance of an implementation class that implements an interface. By convention, the factory method will start with create and return to the interface. It can get the necessary parameters for your factory to figure out which implementation class should be returned.
In Listing 7, the createProvider() method simply gets $type. If $type is set to database, the factory returns an instance of DBPersonProvider. Any new implementation of loading people from the database does not require any changes in classes that use factories and interfaces. DBPersonProvider will implement the PersonProvider interface and have the actual implementation of the getPerson() method.
Take advantage of the weakest links
Loosely coupling modules together is a good thing; it is one of the properties that allows you to encapsulate changes. Two other habits—“stay cautious” and “avoid seeing Medusa”—can help you build loosely coupled modules. To achieve loosely coupled classes, it can be achieved by developing the habit of reducing class dependencies.
Bad habits: tightly coupled
In Listing 8, reducing dependencies does not necessarily require reducing dependencies for clients using objects. Instead, this example will demonstrate how to reduce dependencies with the correct class and minimize such dependencies.
Listing 8. Bad habits of tightly coupled in Address
<?php
require_once "./";
class Address
{
private $addressLine1;
private $addressLine2;
private $city;
private $state; // or province...
private $postalCode;
private $country;
public function setAddressLine1($line1)
{
$this->addressLine1 = $line1;
}
/* accessors, etc... */
public function getCountry()
{
return $this->country;
}
public function format($type)
{
if ($type == "inline") {
$formatter = new InlineAddressFormatter();
} else if ($type == "multiline") {
$formatter = new MultilineAddressFormatter();
} else {
$formatter = new NullAddressFormatter();
}
return $formatter->format($this->getAddressLine1(),
$this->getAddressLine2(),
$this->getCity(), $this->getState(), $this->getPostalCode(),
$this->getCountry());
}
}
$addr = new Address();
$addr->setAddressLine1("123 Any St.");
$addr->setAddressLine2("Ste 200");
$addr->setCity("Anytown");
$addr->setState("AY");
$addr->setPostalCode("55555-0000");
$addr->setCountry("US");
echo($addr->format("multiline"));
echo("\n");
echo($addr->format("inline"));
echo("\n");
?>
The code that calls the format() method on the Address object might look great — what this code does is to use the Address class, call format() and complete it. On the contrary, the Address class is not that lucky. It requires understanding the various formatting methods for correct formatting, which may make the Address object not well reusable by others, especially if others are not interested in using the format method class in the format() method. While the code using Address does not have many dependencies, the Address class has a lot of code, and it may be just a simple data object.
The Address class is tightly coupled with an implementation class that knows how to format Address objects.
Good habit: loose coupling between objects
When building excellent OO designs, a concept called Separation of Concerns (SoC) must be considered. SoC refers to trying to separate objects by content that is really focused on, thereby reducing coupling. In the initial Address class, it must focus on how to format it. This may not be a good design. However, the Address class should consider the parts of Address, and a certain formatting method should focus on how to format the address correctly.
In Listing 9, the code for formatting the address is moved to the interface, implementation class, and factory - to develop the habit of "using interfaces". The AddressFormatUtils class is now responsible for creating the format method and formatting the Address. Any other object can now use Address without worrying about requiring a definition of the formatting method.
Listing 9. Good habits of loose coupling between objects
<?php
interface AddressFormatter
{
public function format($addressLine1, $addressLine2, $city, $state,
$postalCode, $country);
}
class MultiLineAddressFormatter implements AddressFormatter
{
public function format($addressLine1, $addressLine2, $city, $state,
$postalCode, $country)
{
return sprintf("%s\n%s\n%s, %s %s\n%s",
$addressLine1, $addressLine2, $city, $state, $postalCode, $country);
}
}
class InlineAddressFormatter implements AddressFormatter
{
public function format($addressLine1, $addressLine2, $city, $state,
$postalCode, $country)
{
return sprintf("%s %s, %s, %s %s %s",
$addressLine1, $addressLine2, $city, $state, $postalCode, $country);
}
}
class AddressFormatUtils
{
public static function formatAddress($type, $address)
{
$formatter = AddressFormatUtils::createAddressFormatter($type);
return $formatter->format($address->getAddressLine1(),
$address->getAddressLine2(),
$address->getCity(), $address->getState(),
$address->getPostalCode(),
$address->getCountry());
}
private static function createAddressFormatter($type)
{
if ($type == "inline") {
$formatter = new InlineAddressFormatter();
} else if ($type == "multiline") {
$formatter = new MultilineAddressFormatter();
} else {
$formatter = new NullAddressFormatter();
}
return $formatter;
}
}
$addr = new Address();
$addr->setAddressLine1("123 Any St.");
$addr->setAddressLine2("Ste 200");
$addr->setCity("Anytown");
$addr->setState("AY");
$addr->setPostalCode("55555-0000");
$addr->setCountry("US");
echo(AddressFormatUtils::formatAddress("multiline", $addr));
echo("\n");
echo(AddressFormatUtils::formatAddress("inline", $addr));
echo("\n");
?>
Of course, the disadvantage is that as long as the pattern is used, it usually means that the number of artifacts (classes, files) will increase. However, this disadvantage can be compensated by reducing maintenance in each class, and even when the correct reusability is achieved, the workpiece volume can be reduced.
You are rubber; I am glue
OO designs with high cohesion are concentrated and organized into relevant modules. Understanding "focus" is important for deciding how to closely link functions and classes.
Bad habits: reduce cohesion
When the cohesion of the design is low, it cannot organize the classes and methods well. The term spaghetti code is commonly used to describe classes and methods that are bundled together and have low cohesion. Listing 10 provides examples of spaghetti-style code. The relatively common Utils class will use many different objects and have many dependencies. It performs a lot of operations, so it is difficult to reuse.
Listing 10. Bad habits to reduce cohesion
<?php
class Utils
{
public static function formatAddress($formatType, $address1,
$address2, $city, $state)
{
return "some address string";
}
public static function formatPersonName($formatType, $givenName,
$familyName)
{
return "some person name";
}
public static function parseAddress($formatType, $val)
{
// real implementation would set values, etc...
return new Address();
}
public static function parseTelephoneNumber($formatType, $val)
{
// real implementation would set values, etc...
return new TelephoneNumber();
}
}
?>
Good habit: Use high cohesion
High cohesion refers to grouping interrelated classes and methods together. If both methods and classes have a high degree of cohesion, the entire group can be easily broken down without affecting the design. Designs with high cohesion will provide opportunities for reduced coupling. Listing 11 shows two methods that are better organized into classes. The AddressUtils class will contain methods used to handle the Address class, showing a high degree of cohesion between the address-related methods. Likewise, PersonUtils will contain methods that specifically handle Person objects. Both of these new classes with highly cohesive methods have low coupling because they can be used completely independently.
List 11. Good habits of high cohesion
<?php
class AddressUtils
{
public static function formatAddress($formatType, $address1,
$address2, $city, $state)
{
return "some address string";
}
public static function parseAddress($formatType, $val)
{
// real implementation would set values, etc...
return new Address();
}
}
class PersonUtils
{
public static function formatPersonName($formatType, $givenName,
$familyName)
{
return "some person name";
}
public static function parsePersonName($formatType, $val)
{
// real implementation would set values, etc...
return new PersonName();
}
}
?>
Restricted transmission
I often mention to members of my software team, where I work as a technical supervisor or architect, that the biggest enemy of OO language is the copy and paste operations. When used in the absence of pre-OO design, no operation is as destructive as copying code between classes. Whenever you want to copy your code from one class to the next, stop and think about how to use the class hierarchy to take advantage of similar or the same functionality. In most cases, after using a good design, you will find that there is no need to copy the code at all.
Bad habit: not using class hierarchy
Listing 12 shows a simple example of some classes. They start with duplicate fields and methods—not conducive to the application making changes in the long run. If there is a defect in the Person class, there is also a likely one in the Employee class, because it seems that the implementation is copied between the two classes.
Listing 12. Bad habit of not using hierarchies
<?php
class Person
{
private $givenName;
private $familyName;
}
class Employee
{
private $givenName;
private $familyName;
}
?>
Inheritance is a difficult habit to start with, because building the analysis of the correct inheritance model usually takes a lot of time. In turn, it only takes a few seconds to build a new implementation using the Ctrl+C key combination and the Ctrl+V key combination. But this part of the time saved is usually offset quickly during the maintenance phase, because the application will actually cost a lot of maintenance.
Good habit: use inheritance
In Listing 13, the new Employee class extends the Person class. It will now inherit all common methods and will not reimplement these methods. Additionally, Listing 13 shows the usage of abstract methods, demonstrating how to put basic functions into a base class and how to prevent implementation classes from using specific functions.
Listing 13. Take advantage of good habits of inheritance
<?php
abstract class Person
{
private $givenName;
private $familyName;
public function setGivenName($gn)
{
$this->givenName = $gn;
}
public function getGivenName()
{
return $this->givenName;
}
public function setFamilyName($fn)
{
$this->familyName = $fn;
}
public function getFamilyName()
{
return $this->familyName;
}
public function sayHello()
{
echo("Hello, I am ");
$this->introduceSelf();
}
abstract public function introduceSelf();
}
class Employee extends Person
{
private $role;
public function setRole($r)
{
$this->role = $r;
}
public function getRole()
{
return $this->role;
}
public function introduceSelf()
{
echo($this->getRole() . " " . $this->getGivenName() . " " .
$this->getFamilyName());
}
}
?>
Consider using mode
Design patterns refer to common interactions between objects and methods, and time proves that it can solve certain problems. When you think about using design patterns, you need to understand how classes interact. It is an easy way to build classes and their interactions without repeating others’ mistakes and benefiting from proven design.
Bad habit: consider one object at a time
There are actually no appropriate code examples to demonstrate how to consider using patterns (although there are a wealth of excellent examples to show pattern implementations). However, in general, you know that only one object can be considered at a time when the following conditions are met:
The object model will not be designed in advance.
Start writing a single method implementation without removing most of the models.
Instead of using design pattern names in conversations, rather talk about implementation.
Good habit: add objects formed in the pattern at the same time
Generally speaking, you are considering using patterns when doing the following:
Build classes and their interactions in advance.
Apply classes according to the pattern.
Use pattern names such as Factory, Singleton, and Facade.
Remove most of the models and start adding implementations.
---------------------------------------------------------------------------------------------------------------------------
Conclusion
Building good OO habits in PHP will help you build applications that are more stable, easier to maintain, and easier to scale. remember:
Stay cautious.
Be a good neighbor.
Avoid seeing Medusa.
Take advantage of the weakest links.
You are rubber, I am glue.
Restrict transmission.
Consider using mode.
Once you develop and apply these habits, you will most likely be surprised to find the application’s leap in quality.