A Sophisticated Broker
You're getting closer to perfection, but there is still room for improvement. So far, you've hard-coded the logic to instantiate the classes that will satisfy the interface dependencies. That is, you are specifying the interface implementations at compile time. Now you'll want to introduce some configuration options that would specify the classes to instantiate for each interface at run time. The Scanner class would "make a request" to the broker for an implementation for each dependent interface. The broker would read the configuration data, locate the linkage between the desired interface and the class that implemented that interface, and instantiate the specified class. At this point, you've achieved near-perfection with respect to decoupling the Scanner class from its dependencies.
This perfection comes at a cost, however. The logic to manage the configuration data and to instantiate the various classes can be challenging. You can easily generalize this logic to apply to any application, so you have a situation where you could create a standard component to hold this logic. Such components are typically called "inversion of control (IOC) containers." There are a number of these IOC containers available for use in your applications. Although my intent is not to delve into the details of each of these IOC containers, I will list a few that you might want to consider:
Usefulness and Loss of Control
Almost every class that is of some interest is dependent on external elements to accomplish its work. The usefulness of the class is inversely proportional to how concrete the specifications of these external dependencies are. The less control that the class has over its environment, the more useful the class can be in a variety of environments. One aspect of good design is to reduce—or even eliminate—the control that each class needs to have. This article covered a variety of different control extraction techniques, from introducing parameter values, to using delegates, to supporting pairs of interfaces and implementations of those interfaces. Each technique is useful for allowing classes to specify their needs in the most abstract way possible, and each technique is a valid response to the dependency issues in various situations.
After identifying a dependency, the next issue is how to satisfy the dependency. When using parameters and delegates, responsibility for satisfying the dependencies lies with the logic that invokes the class. When using interfaces, the invoking logic can satisfy the responsibility as well, but as the density of interface usage increases, you must look for other ways to reduce the "conceptual congestion." Specifying dependencies in terms of interfaces opens up the possibility of using a broker mechanism to resolve the dependencies. The broker can range from a hard-coded primitive broker to a configuration-driven sophisticated broker shareable across multiple applications.
Once again, this article is not intended to lead you to use a single approach, but rather to expose the underlying principles so that you can choose the approach that makes the most sense in your particular situation.