In the previous tutorial, we covered a handful of concepts, all of which are going to be necessary to fully understand what we're doing in this tutorial.
Specifically, we covered the following topics:
- object-oriented interfaces
- the single responsibility principle
- how these look in PHP
- where we're headed with our plugin
In some series, it's easy to skip tutorials that may not build on one another; however, this series isn't intended to be like that. Instead, it's meant to be read in sequential order, and it's meant to build on the content of each previous tutorial.
With that said, I'm assuming you're all caught up.
Getting Started
Even though I might have mentioned this in the first tutorial, I still like to make sure we're all on the same page with regard to what we're doing in each tutorial and with what software you're going to need.
Our Roadmap
So in this tutorial, the plan is as follows:
- Examine the code that we have written thus far.
- Determine how we may be able to refactor it using object-oriented techniques.
- Provide the high-level outline for our implementation.
Ultimately, we won't be writing much code in this tutorial, but we'll be writing some. It is, however, a practical tutorial in that we're performing object-oriented analysis and design. This is a necessary phase for many large-scale projects (and something that should happen for small-scale projects).
What You Need
If you've been following along, you should have this already set up. But to make sure, here's the short version of everything you need:
- a local development environment suitable for your operating system
- a directory out of which WordPress 4.6.1 is being hosted
- a text editor or IDE
- knowledge of the WordPress Plugin API
With all of that in place, we're ready to work on the code shared in the previous tutorial. So let's get started.
Analyzing the Code
The very first thing we want to do is analyze the current state of our autoloader. It might seem like a lot of code to paste into a single block of code, but that in and of itself shows us that we have some work to do.
With that said, here's the current state of our autoloader:
<?php function tutsplus_namespace_demo_autoload( $class_name ) { // If the specified $class_name does not include our namespace, duck out. if ( false === strpos( $class_name, 'Tutsplus_Namespace_Demo' ) ) { return; } // Split the class name into an array to read the namespace and class. $file_parts = explode( '\\', $class_name ); // Do a reverse loop through $file_parts to build the path to the file. $namespace = ''; for ( $i = count( $file_parts ) - 1; $i > 0; $i-- ) { // Read the current component of the file part. $current = strtolower( $file_parts[ $i ] ); $current = str_ireplace( '_', '-', $current ); // If we're at the first entry, then we're at the filename. if ( count( $file_parts ) - 1 === $i ) { /* If 'interface' is contained in the parts of the file name, then * define the $file_name differently so that it's properly loaded. * Otherwise, just set the $file_name equal to that of the class * filename structure. */ if ( strpos( strtolower( $file_parts[ count( $file_parts ) - 1 ] ), 'interface' ) ) { // Grab the name of the interface from its qualified name. $interface_name = explode( '_', $file_parts[ count( $file_parts ) - 1 ] ); $interface_name = $interface_name[0]; $file_name = "interface-$interface_name.php"; } else { $file_name = "class-$current.php"; } } else { $namespace = '/' . $current . $namespace; } } // Now build a path to the file using mapping to the file location. $filepath = trailingslashit( dirname( dirname( __FILE__ ) ) . $namespace ); $filepath .= $file_name; // If the file exists in the specified path, then include it. if ( file_exists( $filepath ) ) { include_once( $filepath ); } else { wp_die( esc_html( "The file attempting to be loaded at $filepath does not exist." ) ); } }
At this point, remember that the single responsibility principle states the following:
A class should have only one reason to change.
Right now, we don't even have a class, let alone multiple individual methods that have only a single reason to change.
And though it might make sense to start by breaking this autoloader method into smaller, individual methods, let's start from a higher level and begin thinking about an autoloader in terms of an interface. Then we'll drill down into creating a class (or classes).
Object-Oriented Analysis: Responsibilities
Recall from the previous tutorial that an interface is defined by the PHP manual as follows:
Object interfaces allow you to create code which specifies which methods a class must implement, without having to define how these methods are handled.
Given the code and the definitions above, let's think about what an autoloader needs to do from a more modular perspective. Specifically, let's break it down into points that represent what might be enough to change. No, we may not use all of these points, but this it's why it's called analysis. We'll work on the design later.
The code does the following:
- Validates we're working explicitly with our namespace.
- Splits the incoming class name into parts to determine if it's a class or an interface (so
$class_name
is a poor variable name). - Checks to see if we're working with an interface file.
- Checks to see if we're working with a class file.
- Checks to see if we're working with an interface.
- Based on the outcome of the above conditionals, generates a file name.
- Builds a file path based on the generated filename.
- If the file exists at the generated name, includes it.
- Otherwise, the code generates an error.
Thus, the above code does nine things—that is, it has at least nine reasons to change—before it's done completing its work.
This should go without saying, but this particular function is a perfect example that we can refactor to demonstrate object-oriented analysis, design, interfaces, and implementation.
And this raises a question: Where do we even begin?
Object-Oriented Analysis
At this point, it's fair to say that we can begin doing object-oriented analysis—that is, looking at what potential classes we may have and how they interact—given everything we've listed above. Remember, we also want the single responsibility principle to help guide us in our decision making.
At this point, we're not terribly concerned with how the classes will communicate with one another. Instead, we're more focused on creating classes that have a single reason to change.
With that said, I'm going to provide a sample set of classes that I think might work. Before going any further, look at what we've done and attempt to come up with your own list. Then we can compare notes.
A Word About Skills
Note that you may have a better idea than what's listed below, or you may take something away from what we've shared. Regardless, this is a learning exercise. We're attempting to improve our code, our organization, and ultimately become better programmers.
Our Potential Classes
Given what I've listed above, I've come up with the following classes:
- Autoloader. This is the main class that's responsible for ultimately including our class, our namespace, or our interface. We'll call this class. The rest are classes that will take care of necessary work that this one class needs to include the file.
- NamespaceValidator. This file will look at the incoming class, interface, or what have you, and will determine if it's valid. This will give us the deciding factor if we can proceed with the rest of our code our not.
- FileInvestigator. This class looks at the type of file that's being passed into the autoloader. It will determine if it's a class, an interface, or a namespace and return the fully-qualified path name to the file so that it may be included.
- FileRegistry. This will use the fully qualified file path ultimately returned from the other classes and will include it in the plugin.
And that's it. Now third-party classes in our plugin only need to know about the autoloader class, but the autoloader will need knowledge of another class, and other classes will need knowledge of yet other classes.
There are ways to handle this (using dependency injection containers, but that's beyond the scope of this project). But what we'll aim to do through our code is minimize how many classes know about one another.
Object-Oriented Design
At this point, different developers, firms, agencies, and teams will take a different approach to how they design the system on which they are working.
One of the most common ways to go about doing this is to use something called a UML diagram. Though it's useful, it's not something that's worth doing within the scope of this tutorial because it will require a whole other tutorial to explain all of the pieces.
So for the purposes of our tutorial, and since we're working with such a small amount of code, we'll try to stub out how each of the above classes may work before we implement them. This way, we'll get an idea of how we can organize our code.
Note that we won't be namespacing any of this code just yet, and none of this code should be implemented or tested against WordPress just yet. We'll get into that in the next tutorial.
Let's start with the Autoloader
and work from there.
Autoloader
Remember, this class is responsible for including the necessary file. This is the file that will be registered with the spl_autoload_register
function.
<?php class Autoloader { private $namespace_validator; private $file_registry; public function __construct() { $this->namespace_validator = new NamespaceValidator(); $this->file_registry = new FileRegistry(); } public function load( $filename ) { if ( $this->namespace_validator->is_valid( $filename ) ) { $this->file_registry->load( $filename ); } } }
Note that this class depends on the NamespaceValidator
and the FileRegistry
class. We'll see each of these in more detail in just a moment.
NamespaceValidator
This file will look at the incoming filename and will determine if it's valid. This is done by looking at the namespace in the filename.
<?php class NamespaceValidator { public function is_valid( $filename ) { return ( 0 === strpos( $filename, 'Tutsplus_Namespace_Demo' ) ); } }
If the file does in fact belong to our namespace, then we can assume it's safe to load our file.
FileInvestigator
This class is doing quite a bit of work, though part of it is done via very simple, very small helper methods. During the course of execution, it looks at the type of file that it's passed.
It then retrieves the fully-qualified filename for the type of file.
<?php class FileInvestigator { public function get_filetype( $filename ) { $filepath = ''; for ( $i = 1; $i < count( $file_parts ); $i++ ) { $current = strtolower( $file_parts[ $i ] ); $current = str_ireplace( '_', '-', $current ); $filepath = $this->get_file_name( $file_parts, $current, $i ); if ( count( $file_parts ) - 1 !== $i ) { $filepath = trailingslashit( $filepath ); } } return $filepath; } private function get_file_name( $file_parts, $current, $i ) { $filename = ''; if ( count( $file_parts ) - 1 === $i ) { if ( $this->is_interface( $file_parts ) ) { $filename = $this->get_interface_name( $file_parts ); } else { $filename = $this->get_class_name( $current ); } } else { $filename = $this->get_namespace_name( $current ); } return $filename; } private function is_interface( $file_parts ) { return strpos( strtolower( $file_parts[ count( $file_parts ) - 1 ] ), 'interface' ); } private function get_interface_name( $file_parts ) { $interface_name = explode( '_', $file_parts[ count( $file_parts ) - 1 ] ); $interface_name = $interface_name[0]; return "interface-$interface_name.php"; } private function get_class_name( $current ) { return "class-$current.php"; } private function get_namespace_name( $current ) { return '/' . $current; } }
If there's a file that can be refactored a bit more, then this is it. After all, it attempts to determine if we're working with a class, an interface, or a class. A simple factory might be better suited to this.
When it comes time to implement our code, perhaps we'll refactor this further. Until then, this is a preliminary design that may work well enough.
FileRegistry
This will use the fully-qualified file path and include the file; otherwise, it will use the WordPress API to display an error message.
class FileRegistry { private $investigator; public function __construct() { $this->investigator = new FileInvestigator(); } public function load( $filepath ) { $filepath = $this->investigator->get_filetype( $filepath ); $filepath = rtrim( plugin_dir_path( dirname( __FILE__ ) ), '/' ) . $filepath; if ( file_exists( $filepath ) ) { include_once( $filepath ); } else { wp_die( esc_html( 'The specified file does not exist.' ) ); } } }
Another alternative to using the WordPress API would be to throw a custom Exception message. That way, we'd be able to completely separate or decouple our code from WordPress.
Once again, this code is a carry-over from the initial autoloader. During implementation, we may change this, as well.
Conclusion
Alright, so we've looked at the existing code for our autoloader, and then we've stubbed out some potential code that we can use based on some object-oriented analysis and design.
Is the solution that we're working toward more maintainable than what we have? Absolutely. Is this going to work within the context of WordPress and our existing plugin? We won't know until we begin to hook this up into our plugin.
As previously mentioned, there are still some areas in which we could possibly refactor this code. If we hit these type of issues when implementing our code in the final version of our plugin, we'll take a look at doing exactly that.
Whatever the case, the code that we have now should be more readable (though we still have DocBlocks and some inline comments to introduce) and more maintainable and even more testable.
With all of that said, I hope that this has given you an idea as to how to take a long method and break it into more purpose-driven classes. Sure, having multiple classes might feel weird at first, but that doesn't mean it's a bad thing. Have more files (and thus classes) with less code than one file with a lot of code is better.
Embrace the counterintuitive nature of object-oriented programming in this regard. In the next tutorial, we're going to be returning to our plugin and will be working on implementing a variation of the code above. We'll likely be debugging some of it as well. After all, rarely do we get it right the first time
Until then, if you're interested in reading more about object-oriented programming in the context of WordPress, you can find all of my previous tutorials on my profile page. Feel free to follow my on my blog or follow me on Twitter where I frequently talk about both.
Resources
- Object-Oriented Autoloading in WordPress, Part 1
- Namespaces
- Autoloading
- Interfaces
- The WordPress Plugin API
- Single Responsibility Principle
No comments:
Post a Comment