© Matthias Noback 2018
Matthias NobackPrinciples of Package Designhttps://doi.org/10.1007/978-1-4842-4119-6_7

7. The Common Reuse Principle

Matthias Noback1 
(1)
Zeist, The Netherlands
 

In Chapter 6, we discussed the Release/Reuse Equivalence principle. It’s the first principle of package cohesion: it tells an important part of the story about which classes belong together in a package, namely those that you can properly release and maintain as a package. You need to take care of delivering a package that is a true product.

If you follow all the advice given in the previous chapter, you will have a well-behaving package. It has great usability and it’s easily available, so it will be quickly adopted by other developers. But even when a package behaves well as a package, it may at the same time not be very useful.

Let’s say you have a nice collection of very useful classes, implementing several interesting features. When you group those classes into packages, there are two extremes that need to be avoided. If you release all the classes as one package, you force your users to pull the entire package into their project, even if they use just a very small part of it. This is quite a maintenance burden for them.

On the other hand, if you put every single class in a separate package, you will have to release a lot of packages. This increases your own maintenance burden. At the same time, users have a hard time managing their own list of dependencies and keeping track of all the new versions of those tiny packages.

In this chapter, we discuss the second package cohesion principle, which is called the Common Reuse principle. It helps you decide which classes should be put together in a package, and what’s more important, which classes should be moved to another package. When we’re selecting classes or interfaces for reuse, the Common Reuse principle tells us that1:

Classes that are used together are packaged together.

So when you design a package you should put classes in it that are going to be used together. This may seem a bit obvious; you wouldn’t put completely unrelated classes that are never used together in one package. But things become somewhat less obvious when we consider the other side of this principle—you should not put classes in a package that are not used together. This includes classes that are likely not to be used together (which leaves the user with irrelevant code imported into their project).

In this chapter, we look at some packages that obviously violate this rule: they contain all sorts of classes that are not used together—either because those classes implement isolated features or because they have different dependencies. Sometimes the package maintainer puts those classes in the same package because they have some conceptual similarity. Some may think it enhances the usability of the package for them or its users.

At the end of this chapter, we try to formulate the principle in a more positive way and we discuss some guiding questions that can be used to make your packages conform to the Common Reuse principle.

There are many signs, or “smells,” by which you can recognize a package that violates the Common Reuse principle. I’ll discuss some of these signs, using some real-world packages as examples. A quick word before we continue—in no way do I want to dispute the greatness of these packages. They are well-established packages, created by expert developers, and used by many people all over the world. However, I don’t agree with some of the package design choices that were made. So you should take my comments not as angry criticism, but as a gesture toward what I think is the ideal package design approach.

Feature Strata

The most important characteristic of packages that violate the Common Reuse principle is what I call “strata of features”. I really like the term strata , and this is the perfect time to use it. A stratum is:

A layer of material, naturally or artificially formed, often one of a number of parallel layers, one upon another. 2

I’d like to define “feature strata” as features existing together in the same package, but not dependent on each other. This means that you would be able to use feature A without feature B, but adding feature B is possible without disturbing feature A. It also means that afterward, disabling feature B is no problem and won’t cause feature A to break. Feature A and B don’t touch; they work in parallel.

In the context of packages, feature strata often manifest themselves as classes that belong together because they implement some specific feature. But then after one feature has been implemented, the maintainer of the package kept adding new features, consisting of conglomerates of classes to the same package. In most cases, this happens because the features are conceptually, but not materially, related.

Obvious Stratification

Sometimes you can recognize a stratified package by the fact that it literally contains a different namespace for each feature stratum. There are many examples of this, but let’s take a look at the symfony/security package,3 which contains the Symfony Security component.4 As you can see by looking at its directory/namespace tree, it has four major namespaces—Acl, Core, Csrf, and Http—each of them containing many classes (see Listing 7-1).
.
├── Acl
│   ├── Dbal
│   ├── Domain
│   ├── Exception
│   ├── Model
│   ├── Permission
├── Core
│   ├── Authentication
│   ├── Authorization
│   ├── Encoder
│   ├── Event
│   ├── Exception
│   ├── Role
│   ├── User
│   ├── Util
│   └── Validator
├── Csrf
└── Http
Listing 7-1

The Directory Tree of the symfony/security Package

Classes from these major namespaces don’t have to be used together at the same time: classes in Acl only depend on Core, classes in Http depend on classes in Core and optionally on classes in Csrf. Not the other way around. This means that if someone were to install this package to use the Csrf classes , they would only use a small subset of this package. So if we think about what the Common Reuse principle was all about (“Classes that are used together are packaged together”), then this is a clear violation of this principle.

As you can see, some of the major namespaces are split up into minor namespaces. And as it turns out, many of the classes from the minor namespaces can be used separately from classes in the other namespaces. This again indicates that the Common Reuse principle has been violated, and that the package should be split. Fortunately, the package maintainers already decided to split this package into several other packages, so now at least the major namespaces have their own package definition file and can be installed separately.

Obfuscated Stratification

In many other cases, a package has feature strata that aren’t so easy to spot. The classes that are grouped around a certain functionality are not separated by their namespace, but by some other principle of division, for instance by the type of the class. Take a look at the nelmio/security-bundle package,5 which contains NelmioSecurityBundle,6 which can be used in Symfony projects to add some specific security measures that are not provided by the framework itself. Because of the nature of the Symfony HttpKernel,7 everything related to security can be implemented as an event listener, which hooks into the process of converting a request to a response. Some security-related event listeners will prevent the kernel from further handling the current request (for instance, based on the protocol used), and some listeners modify the final response (e.g., encrypt cookies or session data).

By looking at the directory tree of this package, you can easily recognize the fact that most of the features are introduced as event listeners, some of which may use utility classes like Encrypter and Signer (see Listing 7-2).
.
├── ContentSecurityPolicy
│   └── ContentSecurityPolicyParser.php
├── Encrypter.php
├── EventListener
│   ├── ClickjackingListener.php
│   ├── ContentSecurityPolicyListener.php
│   ├── EncryptedCookieListener.php
│   ├── ExternalRedirectListener.php
│   ├── FlexibleSslListener.php
│   ├── ForcedSslListener.php
│   └── SignedCookieListener.php
├── Session
│   └── CookieSessionHandler.php
└── Signer.php
Listing 7-2

The Directory Structure of the nelmio/security-bundle Package

As you could’ve guessed by the names of the listeners, each listener has a particular functionality and each of the listeners can be used separately. This guess can be confirmed by looking at the available configuration options for this package (see Listing 7-3).
nelmio_security:
    # signs/verifies all cookies
    signed_cookie:
        names: ['*']
    # encrypt all cookies
    encrypted_cookie:
        names: ['*']
    # prevents framing of the entire site
    clickjacking:
        paths:
            '^/.*': DENY
    # prevents redirections outside the website's domain
    external_redirects:
        abort: true
        log: true
    # prevents inline scripts, unsafe eval, external
    # scripts/images/styles/frames, etc
    csp:
        default: [ self ]
    ...
Listing 7-3

The Available Configuration Options for the nelmio/security-bundle Package

It becomes clear that all of these listeners represent a different functionality that can be configured on its own and any of the listeners can be disabled, while the other one will still keep working. This forces us to conclude that when you use one class from this package, you will not always use all the other (nor even most of the other) classes inside this package. And thus, the package violates the Common Reuse principle in a very clear way. Somebody who wants to use only one of the features provided by this bundle (and they can!) still has to install the entire bundle.

It becomes slightly more interesting when we look at the remaining configuration options, where it appears that some parts of this package are even mutually exclusive: HTTPS handling can not be “forced” and “flexible” at the same time (see Listing 7-4).
nelmio_security:
    ...
    # forced HTTPS handling, don't combine with flexible mode
    # and make sure you have SSL working on your site before
    # enabling this
#    forced_ssl:
#        hsts_max_age: 2592000 # 30 days
#        hsts_subdomains: true
    # flexible HTTPS handling
#    flexible_ssl:
#        cookie_name: auth
#        unsecured_logout: false
Listing 7-4

HTTPS Handling Can’t Be Configured to be “Forced” and “Flexible” at the Same Time

This means that when you use one particular class in this package, i.e. the ForcedSslListener , you will definitely not use all other classes of this package. In fact, you will with certainty not use FlexibleSslListener. Of course, this definitely asks for a package split, so users would not have to be concerned with this exclusiveness.

Classes That Can Only Be Used When … Is Installed

It should be noted that the previous examples were about packages that provide separate feature strata, but each of those features has the same dependencies (in this case, other Symfony components or the entire Symfony framework). The following examples will be about feature strata within packages that have different dependencies themselves.

Let’s take a look at the monolog/monolog package,8 which contains the Monolog logger.9 The primary class of this package is the Logger class (obviously). However, the real handling of log messages is done by instances of HandlerInterface . By combining different handlers, activation strategies, formatters, and processors, every part of logging messages can be configured. The package tries to offer support for anything you can write log messages to, like a file, a logging server, a database, etc. This results in the list of files in Listing 7-5 (it’s quite big and still it’s shorter than the real list).
.
├── Formatter
│   ├── ChromePHPFormatter.php
│   ├── FormatterInterface.php
│   ├── GelfMessageFormatter.php
│   ├── JsonFormatter.php
│   ├── LogstashFormatter.php
│   └── WildfireFormatter.php
├── Handler
│   ├── AmqpHandler.php
│   ├── BufferHandler.php
│   ├── ChromePHPHandler.php
│   ├── CouchDBHandler.php
│   ├── CubeHandler.php
│   ├── DoctrineCouchDBHandler.php
│   ├── ErrorLogHandler.php
│   ├── FingersCrossedHandler.php
│   ├── FirePHPHandler.php
│   ├── GelfHandler.php
│   ├── HandlerInterface.php
│   ├── HipChatHandler.php
│   ├── MailHandler.php
│   ├── MongoDBHandler.php
│   ├── NativeMailerHandler.php
│   ├── NewRelicHandler.php
│   ├── NullHandler.php
│   ├── PushoverHandler.php
│   ├── RavenHandler.php
│   ├── RedisHandler.php
│   ├── RotatingFileHandler.php
│   ├── SocketHandler.php
│   ├── StreamHandler.php
│   ├── SwiftMailerHandler.php
│   ├── SyslogHandler.php
│   ├── TestHandler.php
│   └── ZendMonitorHandler.php
├── Logger.php
└── Processor
    ├── MemoryProcessor.php
    ├── ProcessIdProcessor.php
    └── PsrLogMessageProcessor.php
Listing 7-5

List of Files in the monolog/monolog Package (Abbreviated)

A developer can install this package and start instantiating handler classes for any storage facility that is already available in their development environment. As a user you don’t need to think about which package you should install; it’s always the main package. This would seem to have a high usability factor: isn’t this easy? Whatever your situation is, just install the monolog/monolog package and you can use it right away (this is known as the “batteries included” approach).

As we will see, this design choice complicates things a lot, for the user as well as for the package maintainer. The thing is, this package isn’t entirely honest about its dependencies. It contains code for all kinds of things, but all this code needs many different extra things to be able to function correctly. For instance, the MongoDBHandler needs the mongo PHP extension to be installed. Now when I install monolog/monolog, the package manager won’t verify that the extension is installed, because it’s listed as an optional dependency (ext-mongo under the suggest key) in the package definition file (see Listing 7-6).
{
  "name": "monolog/monolog",
  ...
  "require": {
    "php": ">=5.3.0",
    "psr/log": "~1.0"
  },
  ...
  "suggest": {
    "mlehner/gelf-php": "Send log messages to GrayLog2",
    "raven/raven": "Send log messages to Sentry",
    "doctrine/couchdb": "Send log messages to CouchDB",
    "ext-mongo": "Send log messages to MongoDB",
    "aws/aws-sdk-php": "Send log messages to AWS services"
    ...
  },
  ...
}
Listing 7-6

Optional Dependencies for the monolog/monolog Package

Why didn’t the maintainer of monolog/monolog add the ext-mongo dependency to the list of required packages? Well, imagine a developer who only wants to use the StreamHandler, which just appends log messages to a file on the local filesystem. They don’t need a Mongo database nor the mongo extension to be available. If ext-mongo would be a required dependency, installing the monolog/monolog package would force them to also install the mongo extension, even though the code that really needs the extension will never be executed in their environment. This is why ext-mongo is just a suggested dependency.

So if I want to use its MongoDBHandler for storing my log messages in a Mongo database, I have to manually add ext-mongo as a dependency to my own project (as is shown in Listing 7-7). This will make the package manager check if the mongo extension has indeed been installed.
{
    "require": {
        "ext-mongo": ...
    }
}
Listing 7-7

Manually Adding ext-mongo as a Project Dependency

The first issue for me is that I don’t know which version of the mongo PHP extension I need to install to be able to use the MongoDBHandler . There’s no way to find that out, other than to just try installing the latest stable version and hope that it’s supported by MongoDBHandler. I will only know this for sure if the MongoDBHandler comes with tests that fully specify its behavior. Then I could run the tests and see if they pass with the specific version of ext-mongo that I just installed. This is my first objection to the design choice of making the MongoDBHandler part of the core Monolog package.

The second objection to this approach is that it forces me to add the mongo extension to the list of dependencies of my own project. This is wrong, since it’s not a dependency of my project but of the monolog/monolog package. It’s a dependency of a class inside that package. My own project might not contain any code related to MongoDB at all, yet I have to require the mongo extension in my project because I want to use the MongoDBHandler class.

So the monolog/monolog package doesn’t handle its dependencies well, and I have to do this myself. But this doesn’t really match with the idea of a package manager, which I instruct to install each of my own dependencies and any dependencies required by these dependencies. I want all the packages I install to fully take care of their own dependencies. It’s not my responsibility to know the dependencies of each class inside a package and to guess which ones I need to manually install to be able to use them. Furthermore, I should not be the one who needs to find out which version of a dependency is compatible with the code in the package. This is the task of the package maintainer.

This reasoning applies to each of the specific handlers that the monolog/monolog package supplies. And nearly all handlers will be used exclusively by any particular user, which means that the package violates the Common Reuse principle an equal number of times. For any handler in the package, the other handlers and their optional dependencies will probably never be used at the same time.

Suggested Refactoring

The solution to this problem is easy—split the monolog/monolog package. Each handler should have its own package, with its own true dependencies. For example, the MongoDBHandler would be in the monolog/mongo-db-handler package, which can be defined as in Listing 7-8.
{
    "name": "monolog/mongo-db-handler",
    "require": {
        "php": ">=5.3.2",
        "monolog/monolog": "~1.6"
        "ext-mongo": ">=1.2.12,<1.6-dev"
    }
}
Listing 7-8

The Definition File for the monolog/mongo-db-handler Package

This way, each specific handler package can be explicit about its dependencies and the supported versions of those dependencies too, and there are no optional dependencies anymore (think about it: how can a dependency be “optional” really?). If I choose to install the monolog/mongo-db-handler, I can rest assured that every dependency will be checked for me and that after installing the package, every line of code inside it is executable in my development environment. Figure 7-1 shows what the dependency hierarchy looks like after this change.
../images/471891_1_En_7_Chapter/471891_1_En_7_Fig1_HTML.jpg
Figure 7-1

monolog packages with explicit dependencies

Previously, the package definition file of monolog/monolog contained some useful suggestions as to what other extensions and packages the user could install to unlock some of its features. Now that the ext-mongo dependency was moved to the monolog/mongo-db-handler package, how does a user know they can use a Mongo database to store log messages? Well, this monolog/mongo-db-handler package itself could be listed under the suggested dependencies for the monolog/monolog package, as shown in Listing 7-9.
{
  "name": "monolog/monolog",
  ...
  "suggest": {
    "monolog/mongo-db-handler": "Send log messages to MongoDB",
    "monolog/gelf-handler": "Send log messages to GrayLog2",
    "monolog/raven-handler": "Send log messages to Sentry",
    ...
  },
  ...
}
Listing 7-9

Suggested Dependencies for the monolog/monolog Package

A Package Should Be “Linkable”

Let’s take one last look at MongoDBHandler, as it’s currently still a part of the monolog/monolog package. We already concluded that after installing the package, you would not be able to use this class without also installing the mongo PHP extension first. If we don’t do this, and we try to use this specific handler, we would get all kinds of errors, in particular errors related to classes that were not found. The code inside the MongoDBHandler is syntactically correct, it just doesn’t work in the context of this project.

We need to make a conceptual division here when it comes to correctness, a division that is not often made by PHP developers. In many programming languages, problems with the code can occur at compile time or at link time. Compiling code means checking its syntax, building an abstract syntax tree, and converting the higher-level code to lower-level code. The result of the compile step are object files, which need to be linked together, in order to be executable. During the link process, references to classes and functions will be verified. If a function or class is not defined in any of the object files, the linker produces an error.

One of the characteristics of PHP is that it has no link process. It compiles code, yes, but if a class or a function does not exist, it will only be noticed at runtime, and even then in most cases, at the very last moment.

I strongly believe that even though PHP allows us to be very flexible in this regard, we must teach ourselves to think more in a “compiled language” way. We have to ask ourselves: would this code compile? It should, definitely, otherwise it would just be malformed code. And regarding every explicit, non-optional dependency of my package: would this code “link”? The answer would be “No” if the MongoDBHandler stays inside the monolog/monolog package, which has the mongo extension as a suggested dependency only. If we move the MongoDBHandler to its own monolog/mongo-db-handler package, which has an explicit dependency on ext-mongo, the answer will be “Yes,” as it should be.

Static Analysis Tools as Substitutes for a Real Compiler

Over the last few years, PHP as a programming language has moved further and further into the direction of being a statically typed language. PHP code will remain a dynamic language with compilation at runtime. But the language has better type checking with every release. Besides, PHP developers compensate for what the language doesn’t offer by using all kinds of static analysis tools. These tools become more and more popular, since they help catch a lot of potential issues with the code, before the code runs on some server.

In the context of our discussion about compiling and linking, PHPStan deserves a first mention here:

PHPStan moves PHP closer to compiled languages in the sense that the correctness of each line of the code can be checked before you run the actual line. 10

Comparable tools are Psalm11 and Phan.12

There’s another tool worth mentioning here. It’s called Composer Require Checker13 and it checks whether a package uses imported symbols (classes, interfaces, etc.) that aren't part of its direct dependencies. This is very useful, since it will prevent the scenario where your package uses a class that’s inside a package that is only indirectly a dependency of your package. In other words, if Package A depends on Package B and B depends on C, then if A also depends on C, it needs to make this dependency explicit. Otherwise, if one day B stops depending on C, this will break A. If you use PhpStorm as your IDE, the PHP Inspections plugin can also tell you about implicit dependencies.

Cleaner Releases

There’s one more characteristic of the monolog/monolog package that I’d like to discuss here, which again points us in the direction of creating separate packages for each of the specific handlers.

As we saw in the chapter about the Release/Reuse Equivalence principle, it’s important for a package to be a good software product. One of the important characteristics of good software is that new versions don’t cause backward compatibility breaks. However, the MongoDBHandler in the monolog/monolog package shows clear signs of a struggle for backward compatibility (see Listing 7-10).
class MongoDBHandler extends AbstractProcessingHandler
{
    // ...
    public function __construct(
        $mongo,
        $database,
        $collection,
        ...
    ) {
        if (!($mongo instanceof MongoClient
            || $mongo instanceof Mongo)) {
            throw new InvalidArgumentException('...');
        }
        // ...
    }
    // ...
}
Listing 7-10

The MongoDBHandler

The first constructor parameter $mongo has no explicit type. Instead, the validity of the argument is being checked inside the constructor and this validation step allows us to use two different kinds of $mongo objects. It should be either an instance of MongoClient or an instance of Mongo. Both are classes from the mongo PHP extension, but the Mongo class is deprecated since version 1.3.0 of the extension.

So now there’s an ugly if clause inside the constructor of this class, which prevents the $mongo argument from being strictly typed, even if I have the latest version of the mongo extension installed in my environment. This shouldn’t be necessary. I’d like the handler to look like the one in Listing 7-11 instead.
class MongoDBHandler extends AbstractProcessingHandler
{
    ...
    public function __construct(
        MongoClient $mongo,
        $database,
        ...
    ) {
        // no need for extra validation
        // ...
    }
    // ...
}
Listing 7-11

The MongoDBHandler with a Strictly Typed Constructor Argument

But if we remove the if clause, this class would be useless for people who have an older version of mongo installed on their system.

The only way to solve this dilemma is to create extra branches in the version control repository of the monolog/monolog package—a branch for version ranges of MongoDB that should receive special treatment, e.g. monolog/monolog@mongo_db_older_than_1_3_0 and monolog/monolog@mongo_db. Of course, this will soon end in a big mess. The monolog/monolog package has many more handlers that may require such treatment.

Let’s fast-forward to the already suggested solution of moving the MongoDBHandler to its own package, the definition file of which looks like the one in Listing 7-12.
{
    "name": "monolog/mongo-db-handler",
    "require": {
        "php": ">=5.3.2",
        "monolog/monolog": "~1.6"
        "ext-mongo": ">=1.2.12,<1.6-dev"
    }
}
Listing 7-12

The Definition File for the monolog/mongo-db-handler Package (Revisited)

This monolog/mongo-db-handler package is hosted inside a separate repository, so it doesn’t need to keep up with the versions of the core monolog/monolog package. This means it’s possible to add branches corresponding to different versions of the mongo extension. For instance, you could have a 1.2.x branch and a 1.3.x branch, corresponding to the version of the mongo extension that is supported. Then someone who has version 1.2 of the mongo extension installed could add version 1.2 of monolog/monolog-db-handler as a dependency to their project (see Listing 7-13).
{
    "require": {
        "monolog/monolog-db-handler": "1.2.*"
    }
}
Listing 7-13

Adding Version 1.2 of monolog/monolog-db-handler as a Dependency

Someone who already has the latest version of the mongo extension would simply choose "~1.3" as the version constraint.

Splitting the package based on its (optional) dependencies is advantageous not only to the user of the package. It will also help the maintainer a lot. They can let someone else maintain the MongoDB-specific handler package, someone who already keeps a close eye on the mongo extension releases. This person doesn’t have to be able to modify the main monolog/monolog package. This package automatically becomes a more stable package, because it has fewer reasons to change (see also Chapter 8 about the Common Closure principle). This is by itself a good thing for its users, who don’t need to keep track of every new version that is released because of a change in one of the handlers they don’t use.

The Cost of Splitting

When discussing the package design principles and applying them to real-world packages, the advice is usually to split the package into smaller ones. While we’re still in the middle of discussing all the reasons for doing so, it’s good to mention that there’s a trade-off involved in splitting packages. The smaller your packages are, the more you will have of them, the more work you have to put into making new releases, managing the repositories, their issues and pull requests, etc. The larger your packages are, the more complicated they are to work with from a user perspective. The package will need to be upgraded often, and the user will pull in lots of code and lots of dependencies they don’t need.

It's good to keep this in mind when you’re a package developer. You need to find that golden middle between too many small packages, and too few large packages.

A technical solution that could be helpful is the so-called “mono-repo”. It means that the code for all your packages will be hosted in one repository. Any change to any of the packages will be committed to that repository. To make it possible for users to install every package separately, there will be a read-only repository for each of the subpackages inside the mono-repo. These sub-repositories will be updated upon every change to the main repository. For Git, this process is called “subtree split”. There’s no need to manually set it up, since there are automated solutions, like Git Subtree Split as a Service.14

Bonus Features

We’ve looked at obvious and non-obvious feature strata and why these are characteristics of packages that violate the Common Reuse principle. Sometimes features are not really strata, but single classes that nevertheless don’t belong inside a package. Let’s take a look at the matthiasnoback/microsoft-translator package15 I created myself. This package contains the MicrosoftTranslator library,16 which can be used for translating text using the Microsoft (Bing) Translator API. The translator depends on the kriswallsmith/buzz package,17 which itself contains an HTTP client called Buzz.18 My package uses its Browser class to make HTTP requests to the Microsoft OAuth and Translator APIs, as you may guess by its (stripped) directory structure (see Listing 7-14).
.
├── Buzz
├── Exception
├── MicrosoftOAuth
└── MicrosoftTranslator
Listing 7-14

The Abbreviated Directory Structure of the matthiasnoback/microsoft-translator Package

While I was developing this library, I realized that my application might make many duplicate calls to the Microsoft Translator API. For instance, it would ask the translation service several times to translate the word “Submit” to Dutch. And even though this would trigger a new HTTP request every time, the response from the API would always be the same. In order to prevent these duplicate requests, I decided to add a caching layer to the package and I thought it would be a good idea to do this by wrapping the Buzz browser client in a CachedClient19 class. The CachedClient class would analyze each incoming request and look in the cache to see if it had made this request before. If so, the cached response would be returned; otherwise, the request would be forwarded to the real Buzz client, and afterwards the fresh response would be stored in the cache.

See Figure 7-2 for the dependency diagram of the microsoft-translator package.
../images/471891_1_En_7_Chapter/471891_1_En_7_Fig2_HTML.jpg
Figure 7-2

Dependency diagram of the microsoft-translator package

Though at the time I thought the design of this library was pretty good, I would now try to eliminate the dependency on Buzz. There’s nothing so special about Buzz that this package would really need it. In fact, all it needs is “some HTTP client”. Considering the need for abstraction, I would introduce an interface for HTTP clients (e.g., HttpClientInterface) and then create a separate package, called matthiasnoback/microsoft-translator-buzz, which would provide an implementation of my own HttpClientInterface that uses Buzz. Even better, I could rely on an existing abstraction for HTTP clients (e.g., HTTPlug20) or some otherwise standardized and widely supported interface, like the upcoming PHP Standards Recommendation (PSR) 18.21

But there’s some other thing that’s wrong with the design of this library: it contains this smart little CachedClient class. Since it’s indeed such a useful class, every user of this package will likely use this CachedClient class together with the other classes in the package. So there’s no immediate violation of the Common Reuse principle here. However, suppose that your project already depends on the kriswallsmith/buzz package and you need a cache implementation for the Buzz browser client. The matthiasnoback/microsoft-translator package contains such an implementation, so you would simply install it and use only its CachedClient class , and no other class from the same package. Now this makes it crystal-clear that the package does indeed violate the Common Reuse principle. If you use a class from this package, you will not use all the other classes.

Suggested Refactoring

As you may have guessed, the solution to this problem would be to extract the CachedClient class and put it in another package, matthiasnoback/cached-buzz-client, which has nothing to do with the Microsoft Translator and just depends on kriswallsmith/buzz . That way, anybody can install just this package in their project and use the cache layer for Buzz clients. Even better, this package could evolve separately from the microsoft-translator package. Other people may contribute to it by adding features or fixing bugs. These enhancements would become available for everyone who depends on the cached-buzz-client package. When installing the microsoft-translator package , the cached-buzz-client package could be a suggested, optional dependency (see Figure 7-3).
../images/471891_1_En_7_Chapter/471891_1_En_7_Fig3_HTML.jpg
Figure 7-3

Dependency diagram of the microsoft-translator package after moving the CachedClient class out

It’s really great that Buzz now has a way to cache HTTP responses, and this could indeed be interesting functionality for other people. However, if we would have thought more carefully before jumping in and extending the Buzz HTTP client itself, we could have realized that a much simpler solution was just around the corner. Rephrasing the functional requirements as “to be able to cache translation results,” all we need is to extend the functionality of the translator class itself. We could do so using the previously demonstrated technique of class decoration using composition. First, we need an interface for the translator class. Then we create a new class that implements that interface and receives an instance of that interface as a constructor argument. See Listing 7-15 for the result. The demonstrated technique of caching a function’s return value is called “memoization”.
interface TranslatorInterface
{
    public function translate(string $text, string $to): string;
}
final class MicrosoftTranslator implements TranslatorInterface
{
    public function translate(string $text, string $to): string
    {
        // make a call to the remote web service
    }
}
final class CachedTranslator implements TranslatorInterface
{
    private $results = [];
    public function __construct(TranslatorInterface $translator)
    {
        $this->translator = $translator;
    }
    public function translate(string $text, string $to): string
    {
        if (!isset($this->results[$text][$to])) {
           $result = $this->translator
                          ->translate($text, $to);
           $this->results[$text][$to] = $result;
        }
        return $this->results[$text][$to];
    }
}
$translator = new CachedTranslator(new MicrosoftTranslator());
// this call will hit the remote service:
$translator->translate('Submit', 'nl');
// the next call will use the cached result:
$translator->translate('Submit', 'nl');
Listing 7-15

Wrapping the Translator and Caching Return Values of the translate() Method

The CachedClient class we just discussed was a nice example of a “bonus feature” that consists of only a single class. It seems overkill to install an entire package with many classes in your project, just to use one class. But of course, this is a sliding scale. Which number or percentage of classes would be acceptable for a package to remain intact and not be split into multiple other packages?

Guiding Questions

These questions help you decide for each class whether or not it should be in the package you’re working on. In practice I tend to just create the class inside the package I’m already working on. Afterward, I may decide to move it to another package, based on the following guiding questions:
  • Does the class introduce a dependency?

    If it does, is it an optional/suggested dependency? Then you have to create a new package containing this class, explicitly mentioning its dependency.

  • Would the class be useful without the rest of the package?

    If it would, then you have to create a new package to enable users to pull in only the code they want to use.

Asking yourself these two questions, and following the advice they give, will automatically divide your packages according to their dependencies and the functionality they provide. These aspects are also the main reasons why people will select a certain package and not another one—whether or not it introduces too much or too little of the functionality they need, and whether or not its dependencies are compatible with the dependencies of their own projects. If either of these aspects don’t match with the requirements of the user, they won’t install the package.

When to Apply the Principle

You can apply the Common Reuse principle at different moments in the package development lifecycle, for instance when you’re creating a new package for existing classes. The first thing you will need to do is group the classes that are always used together and put them in a package. When you need class A and it always needs class B, it would be a bad idea to put these classes in different packages, package-a and package-b. Separating these classes would require a developer who would like to use class A to install both package-a and package-b.

But the Common Reuse principle should also constantly be applied when you’re adding new classes to an existing package. You need to check if the new class you’re about to add will always be used together with all the other classes in the package. Chances are that by adding a new class to a package you’re adding features to the package that aren’t used by everybody who would require the package as a dependency of their project.

When to Violate the Principle

As we see in the next chapter, the Common Reuse principle is a principle that can only be maximized. You cannot always follow it perfectly. There are times when you may choose to put two classes in one package that can be used separately. Strictly speaking, you would thereby violate the principle, but there may be good reasons to do so. First of all, for convenience. Every package you create needs some extra care. It requires time and energy from you as the package maintainer and there’s a limit to how many packages you can or want to maintain.

Another reason to violate the principle is that you may know all about your target audience. When you know that almost all of the developers who use your package are using it inside a project built using the Laravel framework, you can follow the Common Reuse principle less strictly and add some classes to your package, which may only make sense when someone uses the full-stack Laravel framework. These classes would normally belong in a separate package because they could in theory be used separately, but in practice they will always be used together, which allows you to put them in the same package.

Why Not to Violate the Principle

In most cases, however, there’s the following good reason to not violate the Common Reuse principle: every class that’s part of your package is susceptible to change. Maybe one of its methods contains a bug, or some of its functionality needs to be changed. Maybe its interface needs to be modified and a backward compatibility break will be introduced. As a user of the package, you need to follow all the changes and decide if and when to upgrade to a new version. This takes time, because after upgrading a package, you need to check all the parts of your project that use classes from the package. Maybe you have some automated tests for this, maybe not.

However, you can’t choose not to upgrade. You need to take care that your project depends on the latest stable versions of all packages, to prevent (future) problems like depending on deprecated or even abandoned packages. If the package you depend on is big, contains lots of classes, and is related to all kinds of things, upgrading it will be quite a problem. There are many points of contact between the code in your project and the code inside the package, which means that changes will have many side effects.

On the contrary, when the package you depend on is small, it will be easier to track the changes and less painful to upgrade the package. There will be fewer points of contact, and the chance that an upgrade will break your project is consequently much smaller.

So you greatly help the users of your package when you keep your package small and only put classes in it that they will actually use. Then they will be able to upgrade their dependencies fearlessly.

Conclusion

We have found out many things about the Common Reuse principle and by now it should be clear that there are some good reasons for splitting packages. Those reasons have advantages for both users and maintainers. A package that adheres to the Common Reuse principle has the following characteristics:
  • It’s coherent: All the classes it contains are about the same thing. Users don’t need to install a large package just to use one class or a small group of classes.

  • It has no “optional” dependencies: all its dependencies are true requirements; they are mentioned explicitly and have sensible version ranges. Users don’t need to manually add extra dependencies to their project.

  • They use dependency inversion to make dependencies abstract instead of concrete.

  • As an effect, they are open for extension and closed for modification. Adding or modifying an alternative implementation doesn’t mean opening the package, but creating an extra package.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.122.162