Search code examples
phpoopsolid-principles

Reducing manual object instantiation


I am trying to learn to the Dependency Inversion Principle. Currently my code is like this

class Example {
    public function __construct( $input, $output ) {
        $input_handler  = new InputHandler( $input );
        $output_handler = new OutputHandler( $output );

        $input_handler->doStuff();
        $output_handler->doOtherStuff();
    }
}

$input   = new Input();
$output  = new Output();
$example = new Example( $input, $output)

However, it seems using basic dependency injection, it should be more like this?

class Example {
    public function __construct( $input_handler, $output_handler ) {
        $input_handler->doStuff();
        $output_handler->doOtherStuff();
    }
}

$input          = new Input();
$output         = new Output();
$input_handler  = new InputHandler( $input );
$output_handler = new OutputHandler( $output);
$example        = new Example( $input_handler, $output_handler)

Is this is correct?

I want to let the programmer choose the type of input / output to use when running the program. So with dependency injection (as far as I understand) it would look like this;

$input          = new ConsoleInput();
$output         = new FileOutput();
$input_handler  = new ConsoleInputHandler( $input );
$output_handler = new FileOutputHandler( $output);
$example        = new Example( $input_handler, $output_handler);
$example->doStuffToOutput();

However, I would prefer to make the programmers life a little easier by only needing to pass in the type of input and output, and not need to worry about the classes handling them;

$input   = new ConsoleInput();
$output  = new FileOutput();
$example = new Example( $input, $output );
$example->doStuffToOutput();

or even

$example = new Example( new ConsoleInput(), new FileOutput() );
$example->doStuffToOutput();

How can I achieve this using DIP and not end up with my initial code block? Is this a good thing to do?


Solution

  • While I was reading your question I felt you have two main goals. Firstly to improve the readability of your code ('..ease the programmer's life') and secondly to decouple "Example" class from the I/O handlers. For my point of view, DI is just a principle to follow in order to reach your goals.

    Before I'm attaching any code, I want to emphasize that sometimes it is better to actually couple your code. Code must be coupled somehow. Do not use DI everywhere just because it has been said. Simplicity, as being described with the KISS and YAGNI principles, is always the winner.

    So the big question here is whether your second goal (decoupling with DI) is the smart thing to do. Is there a real reason for the InputHandler / OutputHandler in the "Exmaple" class to be changed? If "No" is your answer, I would recommend you to keep it inside this class intact. And "maybe in the distant future it will be profitable" doesn't really count.

    However, if your handlers should be unique for each type (file, console etc.), and your decoupling would help you and other programmers to extend the platform, you can take advantage of the Factory pattern. You have several ways to implement this pattern (static / abstract / simple / method factories). The main goal is to lessen the learning curve from the client, and make the "Example" class decoupled, so that adding more types or handlers would not affect this class.

    class HandlerFactory {
    
        protected static function createInputHandler(Input $input)
        {
            switch ($input)
            {
                case is_a($input, 'FileInput'):
                    return new FileInputHandler($input);
                case is_a($input, 'ConsoleInput'):
                    return new ConsoleInputHandler($input);
            }
    
            throw new \Exception('Missing Input handler');
        }
    
        protected static function createOutputHandler(Output $output)
        {
            switch ($output)
            {
                case is_a($output, 'FileOutput'):
                    return new FileOutputHandler($output);
                case is_a($output, 'ConsoleOutput'):
                    return new ConsoleOutputHandler($output);
            }
    
            throw new \Exception('Missing Output handler');
        }
    
        public static function createHandler($io)
        {
            switch ($io)
            {
                case is_a($io, 'Input'):
                    return self::createInputHandler($io);
                case is_a($io, 'Output'):
                    return self::createOutputHandler($io);
            }
    
            throw new \Exception('Missing I/O handler');
        }
    }
    

    Now your first code in your question is still relevant with a minor twist:

    class Example {
        public function __construct($input, $output) {
            $input_handler  = HandlerFactory::createHandler($input);
            $output_handler = HandlerFactory::createHandler($output);
    
            $input_handler->doStuff();
            $output_handler->doOtherStuff();
        }
    }
    
    $input   = new Input();
    $output  = new Output();
    $example = new Example($input, $output);