2

Before I begin:

My question was put on hold as primarily opinion-based. But I've done my best to reedit it in a more precise way again. In the hope, that its content will support its reopening.

So here is my question again:

It's about my HMVC project, a PHP framework, in which the M, V & C components are encapsulated in "independent" blocks (module directories). The project should not contain any static class members, static methods, singletons or service locators. I am using a dependency injection container, therefore beeing able to provide inversion of control (IoC).

In a class AbstractTemplate I assign the needed root paths of the template files as default values to the parameters of the class constructor:

abstract class AbstractTemplate {

    public function __construct(
        , $appLayoutsPath = '[app-root-path]/Layouts/Default'
        , $moduleLayoutsPath = '[module-root-path]/Templates/Layouts'
        , $moduleTemplatesPath = '[module-root-path]/Templates/Templates'
    ) {
        //...
    }

}

But in this way I couple the class to a hard-coded representation of the file system.

Therefore I considered passing the default values by using a separate class, which in turn holds the required values as class constants:

class Constants {

    const APP_LAYOUTS_PATH = '[app-root-path]/Layouts/Default';
    const MODULE_LAYOUTS_PATH = '[module-root-path]/Templates/Layouts';
    const MODULE_TEMPLATES_PATH = '[module-root-path]/Templates/Templates';

}

abstract class AbstractTemplate {

    public function __construct(
        , $appLayoutsPath = Constants::APP_LAYOUTS_PATH
        , $moduleLayoutsPath = Constants::MODULE_LAYOUTS_PATH
        , $moduleTemplatesPath = Constants::MODULE_TEMPLATES_PATH
    ) {
        //...
    }

}

This way I couple the abstract class to the concrete implementation Constants.

I'd like to ask you:

  1. Can the second option be tested without problems?

  2. Is there another concrete possibility to provide default values and in the mean time to preserve a good testability?

I appreciate your answers and thank you for your time.

  • there is no merit in spending time on this consideration. Use option 1. It's tried and tested and does what you want. You can still refactor this later *should* the necessity for a Constant class arise (which I think will not happen). – Gordon Jun 09 '17 at 13:53
  • 1
    It's about time you also stop using `abstract` and `extends` keywords at all! Prefer composition over inheritance. Define clean and simple interfaces and let the client to the rest. – Mike Doe Jun 09 '17 at 14:10
  • Not sure why you decided to edit your question to express thanks / talk about feedback to your question / etc. This just clutters your question. Just keep your questions "to the point." – David Makogon Jun 10 '17 at 15:30
  • @DavidMakogon Hi. Thanks for advise. I reedited. To answer to you: This question had a bounty on it earlier. In the moment, in which it was put "on hold", the bounty became invalid (at least I think so, because the reputations were reassigned to my account). But all the users who have taken their time to respond to my problem deserve to know, that their efforts were not in vain: that I try to reopen the question and the bounty. –  Jun 10 '17 at 16:09

4 Answers4

0

Clean Code

I fully agree with George Dryser in that option 2 is cleaner. I don't quite agree with him on his second point though:

I'd strongly recommend not to avoid entire functionality such as static class members purely for design or stylistic considerations, it's there for a reason.

The language features exist, but that doesn't necessarily mean that they should be used for this particular purpose. Also, the question isn't as much about style as much as it is about (de-)coupling.

Interdependence

To address your 3rd point:

... class Constants is introducing this interdependence/coupling ...

There's no interdependence here, since AbstractTemplate depends on Constants, but Constants has no dependencies. Your 2nd option can be tested, but it's not very flexible.

Coupling

In your 2nd point you say:

Is there a real issue, that each class using option 2 will depend on a Constants class?

The issue isn't that there's a dependency being introduced, but what kind of dependency. Reading values from a specific, named member of the application is tight coupling, which you should try to avoid. Instead, make the default values constant only to those classes that read the values:

Default Value Provider

How objects that implement IDefaultsProvider get their values does not concern the AbstractTemplate class at all.

Possible Solution

For the sake of being thorough, I'm going to reinvent the wheel here.

In PHP, the interface for IDefaultsProvider can be written like this:

interface IDefaultsProvider {
    /** Returns the value of a variable identified by `$name`. */
    public function read($name);
}

The interface is a contract that says: "When you have an object that implements IDefaultsProvider, you can read default values using its read() method and it will return the default value you requested".

I'll get to specific implementations of the interface further below. First, let's see how the code for AbstractTemplate might look like:

abstract class AbstractTemplate {
    private static function getDefaultsProvider() {
        // Let "someone else" decide what object to use as the provider.
        // Not `AbstractTemplate`'s job.
        return Defaults::getProvider();
    }

    private static function readDefaultValue($name) {
        return static::getDefaultsProvider()->read($name);
    }

    public function __construct(
        , $appLayoutsPath = static::readDefaultValue('app_layouts_path')
        , $moduleLayoutsPath = static::readDefaultValue('module_layouts_path')
        , $moduleTemplatesPath = static::readDefaultValue('module_templates_path')
    ) {
        //...
    }
}

We've gotten rid of Constants and its members (const APP_LAYOUTS_PATH etc). AbstractTemplate is now blissfully ignorant of where the default values come from. Now, AbstractTemplate and the default values are loosely coupled.

The implementation of AbstractTemplate only knows is how to get a IDefaultsProvider object (see method getDefaultsProvider()). In my example, I'm using the following class for that:

class Defaults {
    /** @var IDefaultsProvider $provider */
    private $provider;

    /** @returns IDefaultsProvider */
    public static function getProvider() {
        return static::$provider;
    }

    /**
     * Changes the defaults provider instance that is returned by `getProvider()`.
     */
    public static function useInstance(IDefaultsProvider $instance) {
        static::$instance = $instance;
    }
}

At this point, the reading part is complete, since AbstractTemplate can get a defaults provider using Defaults::getProvider(). Let's look at bootstrapping next. This is where we can start to address different scenarios like testing, development and production.

For testing, we might have a file called bootstrap.test.php that's included only when tests are being run. It needs to be included before AbstractTemplate, of course:

<?php
// bootsrap.test.php
include_once('Defaults.php');
include_once('TestingDefaultsProvider.php');
Defaults::useInstance(new TestingDefaultsProvider());

The other scenarios require their own bootstrapping as well.

<?php
// bootsrap.production.php
include_once('Defaults.php');
include_once('ProductionDefaultsProvider.php');
Defaults::useInstance(new ProductionDefaultsProvider());

... and so on.

What remains to be done are the implementations of IDefaultProvider. Let's start with TestingDefaultsProvider:

class TestingDefaultsProvider implements IDefaultsProvider {
    public function read($name) {
        return $this->values[$name];
    }

    private $values = [
        'app_layouts_path' => '[app-root-path]/Layouts/Default',
        'module_layouts_path' => '[module-root-path]/Templates/Layouts',
        'module_templates_path' => '[module-root-path]/Templates/Templates',
        // ... more defaults ...
    ];
}

It might actually be as simple as that.

Let's assume that, in production, we want the configuration data to reside in a configuration file:

// defaults.json
{
    "app_layouts_path": "[app-root-path]/Layouts/Default",
    "module_layouts_path": "[module-root-path]/Templates/Layouts",
    "module_templates_path": "[module-root-path]/Templates/Templates",
    // ... more defaults ...
}

In order to get to the defaults in the file, all we need to do is read it once, parse the JSON data and return the default values when requested. For the sake of this example, I'm gonna go with lazy reading & parsing.

class ProductionDefaultsProvider implements IDefaultsProvider {
    public function read($name) {
        $parsedContent = $this->getAllDefaults();
        return $parsedContent[$name];
    }

    private static $parsedContent = NULL;

    private static function getAllDefaults() {
        // only read & parse file content once:
        if (static::$parsedContent == NULL) {
            static::$parsedContent = static::readAndParseDefaults();
        }
        return static::$parsedContent;
    }

    private static readAndParseDefaults() {
        // just an example path:
        $content = file_get_contents('./config/defaults.json');
        return json_decode($content, true);
    }
}

Here's the whole shebang:

_

Conclusion

Is there a better alternative to provide default values?

Yes, provided that it's worth the effort. The key principle is inversion of control (also IoC). The purpose of my example was to show how IoC can be implemented. You can apply IoC to configuration data, complex object dependencies or, in your case, defaults.

If you only have a few default values in your application, it might be overkill to invert control. If there's plenty of default values in your application or if you cannot expect the number of defaults, configuration variables, etc to stay very low in the future though, you might want to look into dependency injection.

Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.

Martin Fowler

Also, this:

Basically, instead of having your objects creating a dependency or asking a factory object to make one for them, you pass the needed dependencies in to the object externally, and you make it somebody else's problem.

 — SO Answer to "What is Dependency Injection?" by wds

The good news is that there are plenty of DI frameworks around:

Community
  • 1
  • 1
Fabian Lauer
  • 8,891
  • 4
  • 26
  • 35
  • Why the downvote? – Fabian Lauer Jun 09 '17 at 13:59
  • I can agree to moving the default values into a config file, but the suggested implementation is complete overkill and adds needless complexity. I mean seriously? All that code for specifying default values? Option 1 is simple and straightforward. – Gordon Jun 09 '17 at 14:00
  • @Gordon Thank you for clarifying :-) I agree with you, it is overkill for just a few default values, but I wrote that in my answer: *If you only have a few default values in your application...*. I didn't make myself clear enough, sorry for that; I updated my answer. My message is that DI might be worth investigating if there's more than just a few default values. – Fabian Lauer Jun 09 '17 at 14:03
  • Yes, I've seen it. But it gets lost in all the other text. I think there is little value in explaining an overkill solution when the original problem is so simple and the OP shouldn't be using all that code in this case. But I've been guilty of that as well. I'll retract the dv because the answer is a very thorough effort. – Gordon Jun 09 '17 at 14:09
  • @Gordon You're right, it does. Thank you for the good criticism, it's much appreciated :-) – Fabian Lauer Jun 09 '17 at 14:11
  • Fabian, I want you to know that I'm very grateful to you for the very extensive answer! I read it with big attention. Thank you! I hope, that in the future I'll have the pleasure to read such answers from you again ;-) –  Jun 14 '17 at 15:46
0
  1. I will probably go with option #2. I like to separate things out (using Separation of Concerns principles), and code reuse (Don't Repeat Yourself principle). It wasn't immediately clear to me from the question if you intend to reuse the default values in multiple classes or not. If you do, Option #2 is even better, since you have to change the actual string default values only in one place.

  2. Not really. You are in a way creating a type that has default parameters. Imagine your Constants class is a type of int. Is there a real issue that your class depends on an integer type? Sometimes you have to have some variables in your class, and Constants is one of those variables.

  3. Your class will always depend on Constants, so you won't be able to easily swap in&out different constants. That can be an issue if you want to have a different set of constants for i.e. testing, or another environment (development, production, testing, etc)

  4. Personally I think I'd off-load the default values to a configuration text file, which can be different in different environments

Config file way

File called 'config/my-config.php';

/**
 * Config for default
 */
return array(
    'APP_LAYOUTS_PATH' => '[app-root-path]/Layouts/Default'
);

In your application:

$config = require 'config/my-config.php';
echo $config['APP_LAYOUTS_PATH'];  //'[app-root-path]/Layouts/Default';

If-then-else way (can be combined with config files)

if ($mySpecificCondition)
    $appLayoutsPath = '[app-root-path]/Layouts/Default';
else
    $appLayoutsPath = '[app-root-path]/Layouts/NonDefault';

Or

switch ($mySpecificCondition)
{
    case 'prod':
        $configFile= 'config_path/production.config.php';
        break;

    case 'devel':
        $configFile= 'config_path/development.config.php';
        break;

    case 'test':
        $configFile= 'config_path/test.config.php';
        break;
}

$config = require $configFile;

To clarify you may have a situation where you have the same file name with different contents on different environments. Or you may wish to have different parameters based on a condition. The above gives some ideas. In my case I use both methods for different things. i.e. I have the same file name with different contents for email/IP configuration for prod/development. But for something like operating system default font file folder placement, I use if/then/else. if (OS == WIN) $path = X; else $path = Y;

Also remember to use Keep It Simple principle. You can always refactor your design later. Think about how your design will play out in the future, of course, but don't make it overly-complicated until you have to.

Dennis
  • 7,907
  • 11
  • 65
  • 115
  • Dennis, thank you very much for your answer. I surely learned from it. I wanted you to know, that I really appreciate your effort! –  Jun 14 '17 at 15:42
0

You can argue for both and you will not be worng since it is just an example detatched from any "real" code. But in my opinion this should be your starting code:

abstract class AbstractTemplate {
    const DEFAULT_APP_LAYOUTS_PATH = '[app-root-path]/Layouts/Default';
    const DEFAULT_MODULE_LAYOUTS_PATH = '[module-root-path]/Templates/Layouts';
    const DEFAULT_MODULE_TEMPLATES_PATH = '[module-root-path]/Templates/Templates';

    public function __construct(
        $appLayoutsPath = AbstractTemplate::DEFAULT_APP_LAYOUTS_PATH, 
        $moduleLayoutsPath = AbstractTemplate::DEFAULT_MODULE_LAYOUTS_PATH, 
        $moduleTemplatesPath = AbstractTemplate::DEFAULT_MODULE_TEMPLATES_PATH
    ) {
        //...
    }

}

So you have your constants and you may reuse it (hopefully in the same class or in whatever class makes it "real", and not outside of it!)

You could argue that "class Constants" would be better if you are going to reuse the constants from it in many different classes. The problem with that approach is that it goes against very basic OOP principles. That is: You should have just one object that does sth for you with theese pathes. And if you need different things to be done you just reuse this one object in many different objects in many different ways...

Also Unit Testing or Dependency Injection and Inversion of Controll does not change anything here. But if you are providing code that you "know" that will only be used with IoC, you could argue if having any defaults is good idea at all and if it would not be better to inject everything from container...

smentek
  • 2,820
  • 1
  • 28
  • 32
  • 1
    Hi. First of all: thank you for your answer! I appreciate your effort. As you know, the question was put on hold since the 9th of June. Whatever I did, no matter how many times I reedited it, I couldn't manage to reopen it and, therefore, to reactivate the bounty of 100 reputations :-( But I find your answer the right one for my situation, e.g. for my project's constelation. The other users had great contributions too! –  Jun 14 '17 at 16:20
  • People make points matter not the other way around, so no worries ;D – smentek Jun 14 '17 at 16:28
  • Your general approach though - regarding the (breaking of) OOP principles and the one regarding the mandatory injection of the dependencies without the need of default values at all, made me decide that your answer should be the accepted one. Now, of course, I want to reward your vision on the subject with the bounties you deserve. But, since the question is on hold and probably will be closed tomorrow, I don't see how I can achieve this. Do have an answer to this... dilema, please? Thank you again and I'm waiting for your idea. –  Jun 14 '17 at 16:28
  • Oh, hi :-) I just wrote you in the same time. –  Jun 14 '17 at 16:30
  • @smentek, are you saying that NOT having a separate Constants class is better for OOP? I thought separation of concerns will speak for having a separate Constants class. I understood you that Constants should be not separate but embedded into the AbstractTemplate class and any other class that extends AbstractTemplate. Correct? If so, what about SOC? – Dennis Jun 14 '17 at 21:31
  • Yes. If we are about ideal code. Please check def of SOC on wikipedia, it says about enxapsulation and hidding data (so our constants) behind interfaces. SOC is 1st rule behind MVC pattern, but you do not apply it to artifficialy devide objects. You apply it in object that acctualy uses data, since it should be concern of that bject. – smentek Jun 16 '17 at 07:40
  • And using pathes should be separated from other objects so you can rewrite just one object in case of switching to other way of persistance. So it is real SOC here ;) – smentek Jun 16 '17 at 07:51
-1

Option 2 is much cleaner and easier maintainable code example I would go with 2, code-hinting applications like phpStorm or DW will understand option 2 better.

I'd strongly recommend not to avoid entire functionality such as static class members purely for design or stylistic considerations, it's there for a reason.

George Dryser
  • 322
  • 1
  • 7
  • how is op2 cleaner or more maintainable than keeping the default values right in the class that uses them? Can you expand on that opinion, please? And isn't a Constant class holding a random mixture of all kinds of constants effectively a registry with all the smells associated? – Gordon Jun 09 '17 at 13:50
  • The question was heavily edited since my answer, the answer was related to class constants in general vs hard-coded array of data – George Dryser Jun 11 '17 at 13:42
  • @Gordon Yes, George Dryser is fully right. Since the first version I heavily reedited the original question, because it was put on hold. Still without effect, I see. Anyway, George's answer was given in regard of the first version and was perfectly "on the subject". –  Jun 14 '17 at 14:35
  • George, thank you for your answer! I appreciate your effort! You have right: the second option would be the cleaner one. But I realised, that, if I'd use it in more than one classes (unrelated ones), I'd broke some basic OOP principles. So, I decided to use the "modified" option 1, as @smentek presented it. Thank you again! :-) –  Jun 14 '17 at 15:56
  • And I'm very sorry for the missunderstanding regarding the "question put on hold" and the many editings I had to make, in the hope that the question will be reopened and therefore the bounty will be revalidate again. –  Jun 14 '17 at 16:06