user656925
user656925

Reputation:

Dependency Injection Pattern seems extreme in Control Class

My Control Class basically picks which object / class to instantiate. Because this is basically what it does it naturally has many objects / classes it calls.

If I use dependency injection I will be injecting all of these objects. This seems bad for two reasons.

  1. I've heard that about 3 dependent objects / classes is normal to KISS ( Keep it Simple Smarty)

  2. Only one of the objects / classes will be used. So in a sense the others are instantiated for no reason.

How do I resolve these design considerations to satisfy - decoupled code, but simple and used code?

Upvotes: 2

Views: 236

Answers (2)

user656925
user656925

Reputation:

Solved:

By placing the dependency injection in the factory pattern ( Object Maker ), I can pull out all of the dependencies into one dependency - Object Maker - note below.

PHP Control

class Control
{
    public static function ajax($ajax_type)
    {
        Session::start();
        switch($ajax_type) 
        {
            case 'signin_control': // uses Message, Text, Database
                $Object = new ObjectMaker();
                $ObjectSignIn=$Object->makeSignIn();
                $ObjectSignIn->invoke();
                break; 
            case 'signup_control':// uses Message, Text, Database
                $Object = new ObjectMaker();
                $ObjectSignUp=$Object->makeSignUp();
                $ObjectSignUp->invoke(); 
                break;
            case 'tweet_control':// uses Message, Text, Database
                $Object = new ObjectMaker();
                $ObjectTweet=$Object->makeTweet();
                $ObjectTweet->add(); 
                break; 
            case 'ControlBookmark_add': // uses Message, Text, Database
                $Object = new ObjectMaker();
                $ObjectBookmark = $Object->makeBookmark();
                $ObjectBookmark->add();
                break; 
            case 'ControlBookmark_delete':// uses Database
                $Object = new ObjectMaker();
                $ObjectBookmark=$Object->makeBookmark();
                $ObjectBookmark->delete(); 
                break; 
            case 'ControlTryIt': // Why Not Session
                new ControlTryIt();
                break; 
            case 'ControlSignOut': 
                Session::finish();
                new ControlSignOut();
                break;
            default:
                throw new Exception('Invalid ajax_type');
        }
    }

ObjecMaker

class ObjectMaker
{
    public function makeSignUp()
    {
        $DatabaseObject = new Database();
        $TextObject = new Text();
        $MessageObject = new Message();

        $SignUpObject = new ControlSignUp();        
        $SignUpObject->setObjects($DatabaseObject, $TextObject, $MessageObject);
        return $SignUpObject;
    }
    public function makeSignIn()
    {
        $DatabaseObject = new Database();
        $TextObject = new Text();
        $MessageObject = new Message();

        $SignInObject = new ControlSignIn();
        $SignInObject->setObjects($DatabaseObject, $TextObject, $MessageObject);
        return $SignInObject;
    }
    public function makeTweet( $DatabaseObject = NULL, $TextObject = NULL, $MessageObject = NULL )
    {
        if( $DatabaseObject == 'small' )
        {
            $DatabaseObject = new Database();
        }
        else if( $DatabaseObject == NULL )
        {
            $DatabaseObject = new Database();
            $TextObject = new Text();
            $MessageObject = new Message();
        }
        $TweetObject = new ControlTweet();        
        $TweetObject->setObjects($DatabaseObject, $TextObject, $MessageObject);
        return $TweetObject;
    }
    public function makeBookmark( $DatabaseObject = NULL, $TextObject = NULL, $MessageObject = NULL )
    {
        if( $DatabaseObject == 'small' )
        {
            $DatabaseObject = new Database();
        }
        else if( $DatabaseObject == NULL )
        {
            $DatabaseObject = new Database();
            $TextObject = new Text();
            $MessageObject = new Message();
        }
        $BookmarkObject = new ControlBookmark();        
        $BookmarkObject->setObjects($DatabaseObject,$TextObject,$MessageObject);
        return $BookmarkObject;
    }
}

Upvotes: 0

hakre
hakre

Reputation: 198199

What you do is that you actually map some parameter onto some functionality, a so called script or action.

So in the end you only need a convention how to map that parameter or name onto some function. As functions can be somewhere (in some other object, in the global space, anonymous), you don't really need to inject many objects into your control class, but functions and the mapping.

If you would than even add some more differentiation with function name and parameters (or "modules" and "actions"), well then, you could drastically reduce your code and you can actually make the request inject the dependency:

Script or Action

 Mapping:                  Actions:                  

 "*"                       "_.Exception.Invalid ajax_type"                        
 "signin_control"          "A.SignIn.invoke"
 "signup_control"          "A.SignUp.invoke"  
 "tweet_control"           "A.Tweet.add"      
 "ControlBookmark_add"     "A.Bookmark.add"   
 "ControlBookmark_delete"  "A.Bookmark.delete"
 "ControlTryIt"            "B.ControlTryIt"   
 "ControlSignOut"          "C.SignOut"

 Implementation:

 $action = $map[isset($map[$ajax_type]) ? $ajax_type : '*'];

 Session::start();

 call_user_func_array(
     'call_user_func_array',
     explode('.', $action) + array(NULL, NULL, NULL)
 );

 function _($a, $b) {
     throw new $a($b);
 }

 function A($a, $b) {         
     $maker = new ObjectMaker();
     $maker->$a()->$b();
 }

 function B($a) {
     new $a();
 }

 function C($a) {
    Session::finish();
    B($a);
 }

This pseudo-code shows the actual business of your control class: Call some functions based on it's input. The concrete dependencies are:

  • ObjectMaker
  • Session
  • $map

As session is static, you should replace it with something that actually can be injected.

As $map is an array, it can be injected, but the logic of the mapping might need to become something more internal, so if $map is an ArrayAccess, this can happen already.

The non-concrete dependencies are hidden inside the actual $ajax_type, so these dependencies are dependent on that parameter through mapping, which is already a dependency. So the last dependency is:

  • $ajax_type

This dependency is related to both the control class and the mapping. So the control class itself could be made a dependency to the ajax type as well. But as you use a static global function, I'll simplify this inside a class function so actually dependencies can be passed into it. I put the factory into a global function and the ajax types are loaded from an ini-file:

function ajax_control_factory($inifile)
{
    $maker   = new ObjectMaker();
    $session = new SessionWrap();
    $types   = new AjaxTypesIni($inifile);
    return new AjaxControl($maker, $session, $types);
}

$control = ajax_control_factory($inifile);

printf("Call an nonexistent ajax type: ");
try {
    $control->invokeByType('blurb');
    printf(" - I so failed!\n");
} catch (Exception $e) {
    printf("Exception caught! All good!\n");
}

printf("Add me a bookmark: ");
$control->invokeByType("ControlBookmark_add");
printf("Done! Fine! Superb this works!\n");
printf("Do the two control functions: ");

$control->invokeByType("ControlTryIt");
$control->invokeByType("ControlSignOut");
printf("Done! Fine! Superb this works!\n");

Ini file:

*                       = _.Exception.Invalid ajax_type
signin_control          = A.SignIn.invoke
signup_control          = A.SignUp.invoke
tweet_control           = A.Tweet.add
ControlBookmark_add     = A.Bookmark.add
ControlBookmark_delete  = A.Bookmark.delete
ControlTryIt            = B.ControlTryIt
ControlSignOut          = C.SignOut

To have this work, this needs some stubs for mocking, which is easy with your example:

class Mock
{
    public $stub;

    public function __call($name, $args)
    {
        return class_exists($this->stub) ? new $this->stub() : $this->stub;
    }
}

class ObjectMaker extends Mock
{
    public $stub = 'Mock';
}

class ControlTryIt {}
class SignOut {}

class SessionWrap
{
    public function start()
    {
        // session::start();
    }

    public function stop()
    {
        // session::finish();
    }
}

Those are enough to run the code above which will give:

Call an nonexistent ajax type: Exception caught! All good!
Add me a bookmark: Done! Fine! Superb this works!
Do the two control functions: Done! Fine! Superb this works!

The ajax types:

class AjaxTypes extends ArrayObject
{
    private $default;
    private $types;

    public function __construct(array $types, $default)
    {
        parent::__construct($types);
        $this->default = $default;
    }

    public function offsetGet($index)
    {
        return parent::offsetExists($index) ? parent::offsetGet($index) : $this->default;
    }
}

class AjaxTypesIni extends AjaxTypes
{
    public function __construct($inifile)
    {
        $map = parse_ini_file($inifile);
        if (!isset($map['*'])) throw new UnexpectedValueException('No * entry found.');
        $default = $map['*'];
        unset($map['*']);
        parent::__construct($map, $default);
    }
}

And the ajax controler:

class AjaxControl
{
    private $types;
    private $maker;
    private $session;

    public function __construct(ObjectMaker $maker, SessionWrap $session, AjaxTypes $types)
    {
        $this->types   = $types;
        $this->maker   = $maker;
        $this->session = $session;
    }

    public function invokeByType($type)
    {
        $session = $this->session;
        $maker   = $this->maker;
        $invoke = function($action) use ($session, $maker)
        {
            $_ = function($a, $b)
            {
                throw new $a($b);
            };
            $A = function($a, $b) use ($maker)
            {
                $maker->$a()->$b();
            };

            $B = function ($a)
            {
                new $a();
            };

            $C = function ($a) use ($B, $session)
            {
                $session->stop();
                $B($a);
            };

            $args = explode('.', $action) + array(NULL, NULL, NULL);
            $func = array_shift($args);
            call_user_func_array(${$func}, $args);
        };
        $invoke($this->types[$type]);
        $this->session->start();
    }
}

This is just exemplary. There is no guarantee that this fits as a design for your needs, just for demonstrating purposes. What it shows is that your actual controller function is not normalized / modular enough. When you better analyze the dependencies that exist and you inject them instead that you hardencode them, you will automatically find the best way to design your system.

What this example shows as well is that you have a tons of hidden dependencies for the request and response. You really need to draw lines somewhere and define what you pass through and in which direction. Say goodbye to global static state. Always inject. You can even start with function that need everything as parameters if it helps.

Upvotes: 2

Related Questions