📜 ⬆️ ⬇️

Static class members. Do not let them ruin your code

I have long wanted to write on this topic. The first push was served by the article Miško Hevery " Static Methods are Death to Testability ". I wrote a response article, but never published it. But recently I saw something that could be called “Class-Oriented Programming”. This refreshed my interest in the subject and here is the result.

“Class-Oriented Programming” is when classes are used that consist only of static methods and properties, and an instance of the class is never created. In this article I will say that:

Although this article is about PHP, the concepts apply to other languages.


Dependencies


Usually, the code depends on another code. For example:
')
$foo = substr($bar, 42); 

This code depends on the $bar variable and the substr function. $bar is just a local variable, defined a little higher in the same file and in the same scope. substr is a PHP core function. Everything is simple here.

Now, such an example:

 $foo = normalizer_normalize($bar); 

normalizer_normalize is an Intl package feature that has been integrated into PHP since version 5.3 and can be installed separately for older versions. Here is a little more complicated - the performance of the code depends on the availability of a specific package.

Now, this option:

 class Foo { public static function bar() { return Database::fetchAll("SELECT * FROM `foo` WHERE `bar` = 'baz'"); } } 

This is a typical example of class-oriented programming. Foo is tied to the Database . And we assume that the Database class has already been initialized and the connection to the database (DB) has already been established. Presumably, the use of this code will be as follows:

 Database::connect('localhost', 'user', 'password'); $bar = Foo::bar(); 

Foo::bar implicitly depends on the availability of the Database and its internal state. You cannot use Foo without Database , and Database supposedly requires connecting to a database. How can you be sure that the connection to the database is already established when the call to Database::fetchAll ? One way looks like this:

 class Database { protected static $connection; public static function connect() { if (!self::$connection) { $credentials = include 'config/database.php'; self::$connection = some_database_adapter($credentials['host'], $credentials['user'], $credentials['password']); } } public static function fetchAll($query) { self::connect(); //  self::$connection... // here be dragons... return $data; } } 

When you call Database::fetchAll , we check the existence of the connection by calling the connect method, which, if necessary, retrieves the connection parameters from the config. This means that Database depends on the file config/database.php . If this file does not exist, it cannot function. We go further. The Database class is bound to a single database. If you need to transfer other connection parameters, it will be at least not easy. Kom is growing. Foo not only dependent on the availability of the Database , but also depends on its state. Database depends on a specific file in a specific folder. Those. implicitly, the Foo class depends on the file in the folder, although it is not visible from its code. Moreover, there are a lot of dependencies on the global state. Each piece depends on another piece, which must be in the desired state and nowhere is this clearly indicated.

Something familiar...


Doesn't that sound like a procedural approach? Let's try to rewrite this example in a procedural style:

 function database_connect() { global $database_connection; if (!$database_connection) { $credentials = include 'config/database.php'; $database_connection = some_database_adapter($credentials['host'], $credentials['user'], $credentials['password']); } } function database_fetch_all($query) { global $database_connection; database_connect(); //  $database_connection... // ... return $data; } function foo_bar() { return database_fetch_all("SELECT * FROM `foo` WHERE `bar` = 'baz'"); } 

Find the 10 differences ...
Hint: the only difference is the visibility of Database::$connection and $database_connection .

In a class-oriented example, the connection is available only for the Database class itself, and in the procedural code this variable is global. The code has the same dependencies, connections, problems and works the same. There is almost no difference between $database_connection and Database::$connection - it's just a different syntax for the same, both variables have a global state. The easy touch of the namespace, thanks to the use of classes, is certainly better than nothing, but does not seriously change anything.

Class-oriented programming is like buying a car, in order to sit in it, periodically open and close doors, jump on the seats, accidentally causing the airbags to activate, but never turn the ignition key and not budge. This is a complete misunderstanding of the essence.

Turn the ignition key


Now, let's try OOP. Let's start with the implementation of Foo :

 class Foo { protected $database; public function __construct(Database $database) { $this->database = $database; } public function bar() { return $this->database->fetchAll("SELECT * FROM `foo` WHERE `bar` = 'baz'"); } } 

Now Foo does not depend on a specific Database . When creating an instance of Foo , you need to pass some object that has the characteristics of a Database . This can be either a Database instance or its descendant. So we can use another implementation of the Database , which can get data from somewhere else. Or has a caching layer. Or it is a stub for tests, and not a real connection to the database. Now we need to create a Database instance, this means that we can use several different connections to different databases, with different parameters. Let's implement the Database :

 class Database { protected $connection; public function __construct($host, $user, $password) { $this->connection = some_database_adapter($host, $user, $password); if (!$this->connection) { throw new Exception("Couldn't connect to database"); } } public function fetchAll($query) { //  $this->connection ... // ... return $data; } } 

Notice how much simpler the implementation has become. In Database::fetchAll do not need to check the status of the connection. To call Database::fetchAll , you need to create an instance of the class. To create an instance of a class, you need to pass the connection parameters to the constructor. If the connection parameters are not valid or the connection cannot be established for other reasons, an exception will be thrown and the object will not be created. This all means that when you call Database::fetchAll , you are guaranteed to have a database connection. This means that Foo only needs to specify in the constructor that it needs a Database $database and it will have a connection to the database.

Without an instance of Foo , you cannot call Foo::bar . Without a Database instance, you cannot create an instance of Foo . Without valid connection parameters, you will not create a Database instance.

You simply can not use the code if at least one condition is not satisfied.

Compare this with a class-oriented code: you can call Foo::bar at any time, but an error will occur if the Database class is not ready. Database::fetchAll can call Database::fetchAll at any time, but an error will occur if there are problems with the config/database.php file. Database::connect sets a global state on which all other operations depend, but this dependency is not guaranteed.

Injection


Let's look at this from the side of the code that uses Foo . Procedural example:

 $bar = foo_bar(); 

You can write this line anywhere and it will be executed. Its behavior depends on the global state of the connection to the database. Although the code is not obvious. Add error handling:

 $bar = foo_bar(); if (!$bar) { // -    $bar,  ! } else { //  ,   } 

Due to the implicit dependencies of foo_bar , in case of an error, it will be hard to understand exactly what has broken.

For comparison, here is a class-oriented implementation:

 $bar = Foo::bar(); if (!$bar) { // -    $bar,  ! } else { //  ,   } 

No difference. Error handling is identical, i.e. it's also hard to find the source of the problems. This is all because calling a static method is just a function call that is no different from any other function call.

Now OOP:

 $foo = new Foo; $bar = $foo->bar(); 

PHP will fall with a fatal error when it comes to new Foo . We indicated that Foo needs a Database instance, but did not pass it.

 $db = new Database; $foo = new Foo($db); $bar = $foo->bar(); 

PHP will fall again, because we did not pass the database connection parameters that we specified in the Database::__construct .

 $db = new Database('localhost', 'user', 'password'); $foo = new Foo($db); $bar = $foo->bar(); 

Now we have satisfied all the dependencies that we promised, everything is ready for launch.

But let's imagine that the connection parameters to the database are incorrect or we have some problems with the database and the connection cannot be established. In this case, an exception will be thrown when new Database(...) executed. The following lines simply will not be executed. So, we don’t need to check the error after calling $foo->bar() (of course, you can check what you returned). If something goes wrong with any of the dependencies, the code will not be executed. And the thrown exception will contain information useful for debugging.

The object-oriented approach may seem more complicated. In our example, a procedural or class-oriented code is just one line, which calls foo_bar or Foo::bar , while the object-oriented approach takes three lines. It is important to capture the essence. We did not initialize the database in the procedural code, although we need to do this anyway. The procedural approach requires error handling after the fact and at every point in the process. Error handling is very confusing, because It is difficult to track which of the implicit dependencies caused the error. Hardcode hides dependencies. The sources of error are not obvious. It is not obvious what your code depends on for its normal functioning.

The object-oriented approach makes all dependencies obvious and obvious. Foo needs a Database instance, and Database instance needs connection parameters.

In a procedural approach, responsibility falls on functions. Call the method Foo::bar - now it must return the result to us. This method, in turn, delegates the Database::fetchAll . Now all the responsibility is on him and he is trying to connect to the database and return some data. And if something goes wrong at any point ... who knows what will be returned to you and from where.

The object-oriented approach shifts part of the responsibility to the calling code and that is its strength. Want to call Foo::bar ? OK, then give it a DB connection. What is the connection? It doesn't matter if it was a Database instance. This is the power of dependency injection. It makes the necessary dependencies explicit.

In the procedural code, you create a lot of hard dependencies and tie up different parts of the code with steel wire. It all depends on everything. You create a solid piece of software. I do not want to say that it will not work. I want to say that this is a very rigid structure, which is very difficult to disassemble. For small applications this may work well. For big ones, this turns into a horror of intricacies, which is impossible to test, extend and debug:



In object-oriented code with dependency injection, you create many small blocks, each of which is independent. Each block has a well-defined interface that other blocks can use. Each unit knows what it needs from others so that everything works. In procedural and class-oriented code, you associate Foo with the Database immediately while writing the code. In the object-oriented code, you indicate that Foo needs some Database , but leave room for maneuver, as it may be. When you want to use Foo , you will need to associate a specific instance of Foo with a specific Database instance:



The class-oriented approach looks deceptively simple, but firmly nails the code with nails of dependencies. The object-oriented approach leaves everything flexible and isolated until it is used, which may look more complicated, but it is more manageable.

Static members


Why do we need static properties and methods? They are useful for static data. For example, the data on which the instance depends, but which never change. Fully hypothetical example:

 class Database { protected static $types = array( 'int' => array('internalType' => 'Integer', 'precision' => 0, ...), 'string' => array('internalType' => 'String', 'encoding' => 'utf-8', ...), ... ) } 

Imagine that this class must associate data types from the database with internal types. For this you need a type map. This map is always the same for all Database instances and is used in several Database methods. Why not make the map a static property? Data never changes, but only reads. And it will save some memory, because data common to all Database instances. Since data access occurs only inside the class, it will not create any external dependencies. Static properties should never be accessible from the outside, because these are just global variables. And we have already seen what this leads to ...

Static properties can also be useful to cache some data that is identical for all instances. Static properties exist, for the most part, as an optimization technique; they should not be viewed as a programming philosophy. And static methods are useful as helper methods and alternative constructors.

The problem with static methods is that they create a hard dependency. When you call Foo::bar() , this line of code becomes associated with a specific class Foo . This can lead to problems.

The use of static methods is permissible under the following circumstances:

  1. Dependence is guaranteed to exist. If the call is internal or the dependency is part of the environment. For example:

     class Database { ... public function __construct($host, $user, $password) { $this->connection = new PDO(...); } ... } 

    Here Database depends on a specific class - PDO . But PDO is part of the platform, this is the database class provided by PHP. In any case, to work with the database will have to use some kind of API.

  2. Method for internal use. An example from the implementation of the Bloom filter :

     class BloomFilter { ... public function __construct($m, $k) { ... } public static function getK($m, $n) { return ceil(($m / $n) * log(2)); } ... } 

    This little helper function simply provides a wrapper for a particular algorithm that helps calculate a good number for the $k argument used in the constructor. Since it must be called before creating an instance of the class, it must be static. This algorithm has no external dependencies and is unlikely to be replaced. It is used like this:

     $m = 10000; $n = 2000; $b = new BloomFilter($m, BloomFilter::getK($m, $n)); 

    This does not create any additional dependencies. The class depends on itself.

  3. Alternative constructor. A good example is the DateTime class built into PHP. An instance of it can be created in two different ways:

     $date = new DateTime('2012-11-04'); $date = DateTime::createFromFormat('dm-Y', '04-11-2012'); 

    In both cases, the result will be a DateTime instance and in both cases the code is tied to the DateTime class anyway. The static DateTime::createFromFormat is an alternative object constructor that returns the same as new DateTime , but using additional functionality. Where you can write new Class , you can write Class::method() . No new dependencies arise.

The remaining uses of static methods affect the binding and may form implicit dependencies.

Word of abstraction


Why all this fuss with addictions? Ability to abstract! As your product grows, its complexity grows. And abstraction is the key to managing complexity.

For example, you have an Application class that represents your application. It communicates with the User class, which is the representation of the user. Which receives data from Database . The Database class needs a DatabaseDriver . DatabaseDriver needs connection options. And so on. If you simply call Application::start() statically, which causes User::getData() statically, which causes a database statically, and so on, in the hope that each layer will deal with its dependencies, you can get a terrible mess if something goes not this way. It is impossible to guess whether the call to Application::start() will work, because it is not at all obvious how the internal dependencies will behave. Even worse, the only way to influence the behavior of Application::start() is to change the source code of this class and the code of the classes it calls and the code of the classes that call those classes ... in the house that Jack built.

The most effective approach to creating complex applications is to create separate parts that can be relied upon. Parts that you can stop thinking in which you can be sure. For example, when calling static Database::fetchAll(...) , there are no guarantees that the connection to the database is already established or will be established.

 function (Database $database) { ... } 

If the code inside this function is executed, it means that the Database instance was successfully transferred, which means that the Database object instance was successfully created. If the Database class is designed correctly, then you can be sure that the presence of an instance of this class means the ability to perform queries to the database. If there is no class instance, the function body will not be executed. This means that the function should not care about the state of the database; the Database class will do it itself. This approach allows you to forget about dependencies and concentrate on solving problems.

Without the ability not to think about the dependencies and dependencies of these dependencies, it is almost impossible to write at least some complicated application. Database can be a small wrapper class or a giant multi-layered monster with a bunch of dependencies, it can start as a small wrapper and mutate into a giant monster with time, you can inherit the Database class and pass a descendant to the function, it's all not important for your function (Database $database) , until the public Database interface is changed. If your classes are properly separated from the rest of the application using dependency injection, you can test each of them using stubs instead of their dependencies. When you have tested a class enough to make sure that it works as it should, you can get rid of your head out of it, just knowing that you need to use a Database instance to work with the database.

Class-oriented programming is nonsense. Learn to use OOP.

Source: https://habr.com/ru/post/169301/


All Articles