📜 ⬆️ ⬇️

Application of the principle of poka-yoke in programming using the example of PHP


Hello! I'm Alexei Grezov, server team badoo developer. We at Badoo always try to make our code easy to maintain, develop and reuse, because it depends on these parameters how quickly and efficiently we can implement a feature. One way to achieve this goal is to write such code that simply does not allow you to make a mistake. The most rigorous interface will not make a mistake with the order of its call. The minimum number of internal states ensures the expected results. The other day I saw an article that describes how the use of these methods simplifies the life of developers. So, I offer you the translation of an article about the principle of "poka-yoke".


When working together with code in a medium or large team, sometimes there are difficulties in understanding and using someone else's code. There are various solutions to this problem. For example, you can agree to follow certain coding standards or use the framework known to the whole team. However, this is often not enough, especially when you need to correct the error or add a new function to the old code. It is hard to remember what specific classes were meant for and how they should work both individually and jointly. At such moments, you can accidentally add side effects or errors, even without realizing it.


These errors can be detected during testing , but there is a real chance that they will slip into production. And even if they are revealed, it can take quite a long time to roll back the code and fix it.


So how can we prevent this? Using the principle of "poka-yoke".


What is poka-yoke?


Poka-yoke is a Japanese term that translates into English roughly as “mistake-proofing” (error protection), and in the Russian version is better known as “fool protection” . This concept originated in lean manufacturing , where it refers to any mechanism that helps the equipment operator avoid mistakes.


In addition to production, poka-yoke is often used in consumer electronics. Take, for example, a SIM card, which, due to its asymmetric shape, can be inserted into the adapter only on the right side.



The opposite example (without using the poka-yoke principle) is the PS / 2 port, which has the same connector shape for both the keyboard and the mouse. They can only be distinguished by color and therefore easily confused.



Another concept poka-yoke can be used in programming. The idea is to make the public interfaces of our code as simple and clear as possible and generate errors as soon as the code is used incorrectly. This may seem obvious, but in fact we often come across code in which there is none.


Please note that poka-yoke is not intended to prevent intentional abuse. The goal is to avoid accidental errors, and not to protect the code from malicious use. Anyway, as long as someone has access to your code, he can always bypass the fuses, if he really wants it.


Before discussing specific measures to make the code more error-proof, it is important to know that poka-yoke mechanisms can be divided into two categories:



Error prevention mechanisms are useful for eliminating errors at an early stage. Having simplified interfaces and behavior as much as possible, we ensure that no one can accidentally use our code incorrectly (remember the example with a SIM card).


On the other hand, error detection mechanisms are outside our code. They monitor our applications to track possible errors and warn us about them. An example would be software that determines whether a device connected to a PS / 2 port has the correct type, and if not, tells the user why it is not working. Such software could not prevent an error, since the connectors are the same, but it can detect it and report it.


Next, we will look at several methods that can be used both to prevent and to detect errors in our applications. But keep in mind that this list is just a starting point. Depending on the specific application, additional measures may be taken to make the code more error proof. In addition, it is important to make sure that poka-yoke is incorporated into your project: depending on the complexity and size of your application, some measures may be too expensive compared to the potential cost of errors. Therefore, it is up to you and your team to decide which measures are best for you.


Error Prevention Examples


Type declaration


Previously known as Type Hinting in PHP 5, type declarations are an easy way to protect against errors when calling functions and methods in PHP 7. By assigning certain types to function arguments, it becomes harder to disturb the order of arguments when calling this function.


For example, let's consider a notification that we can send to the user:


<?php class Notification { private $userId; private $subject; private $message; public function __construct( $userId, $subject, $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() { return $this->userId; } public function getSubject() { return $this->subject; } public function getMessage() { return $this->message; } } 

Without a type declaration, you can accidentally pass variables of the wrong type, which can disrupt the application. For example, we can assume that $userId should be string , while in fact it can be int .


If we pass the wrong type to the constructor, the error is likely to go unnoticed until the application tries to do something with this notification. And at this moment, most likely, we will receive some mysterious error message, in which nothing will point to our code, where we pass string instead of int . Therefore, it is usually preferable to force the application to crash as soon as possible, in order to detect such errors as early as possible during development.


In this particular case, you can simply add a type declaration - PHP will stop and immediately warn us with a fatal error as soon as we try to pass a parameter of the wrong type:


 <?php declare(strict_types=1); class Notification { private $userId; private $subject; private $message; public function __construct( int $userId, string $subject, string $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : int { return $this->userId; } public function getSubject() : string { return $this->subject; } public function getMessage() : string { return $this->message; } } 

Note that by default, PHP will try to cast invalid arguments to their expected types. To prevent this from happening and a fatal error is generated, it is important to enable strict typing ( strict_types ). Because of this, scalar type declaration is not an ideal form of poka-yoke, but serves as a good starting point for reducing errors. Even with strong typing disabled, a type declaration can still serve as a hint as to what type is expected for the argument.


In addition, we declared return types for our methods. This makes it easier to determine what values ​​we can expect when calling a particular function.


Well-defined return types are also useful for avoiding a lot of switch statements when dealing with return values, because without explicitly declared return types, our methods can return different types. Therefore, someone using our methods will have to check which type was returned in a particular case. Obviously, you can forget about the switch , which will lead to errors that are difficult to detect. But they become much less common when declaring the return type of a function.


Value objects


The problem that a type declaration cannot solve is that having multiple function arguments makes it possible to confuse their order when called.


When arguments are of different types, PHP can warn us about the violation of the order of the arguments, but this does not work if we have several arguments with the same type.


To avoid errors in this case, we could wrap our arguments in value-object objects :


 class UserId { private $userId; public function __construct(int $userId) { $this->userId = $userId; } public function getValue() : int { return $this->userId; } } class Subject { private $subject; public function __construct(string $subject) { $this->subject = $subject; } public function getValue() : string { return $this->subject; } } class Message { private $message; public function __construct(string $message) { $this->message = $message; } public function getMessage() : string { return $this->message; } } class Notification { /* ... */ public function __construct( UserId $userId, Subject $subject, Message $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : UserId { /* ... */ } public function getSubject() : Subject { /* ... */ } public function getMessage() : Message { /* ... */ } } 

Since our arguments are now of a very specific type, they are almost impossible to confuse.


An additional advantage of using value objects compared to declaring scalar types is that we no longer need to include strong typing in each file. And if we do not need to remember this, then we will not be able to forget about it.


Validation


When working with value objects, we can encapsulate the logic of checking our data inside objects themselves. Thus, it is possible to prevent the creation of a value object with an invalid state, which may lead to problems in the future in other layers of our application.


For example, we may have a rule that any UserId should always be positive. We could obviously check it whenever we get a UserId as input, but, on the other hand, it can also be easily forgotten in one place or another. And even if this forgetfulness leads to an actual error in another layer of our application, it may be difficult to understand from the error message what actually went wrong, and this will complicate debugging.


To prevent such errors, we could add some validation to the UserId constructor:


 class UserId { private $userId; public function __construct($userId) { if (!is_int($userId) || $userId < 0) { throw new \InvalidArgumentException( 'UserId should be a positive integer.' ); } $this->userId = $userId; } public function getValue() : int { return $this->userId; } } 

Thus, we can always be sure that when working with the UserId object UserId it has the correct state. This will save us from having to constantly check the data at different levels of the application.


Note that here we could add a scalar type declaration instead of using is_int , but this will force us to include strong typing wherever UserId used. If this is not done, PHP will try to cast other types to int whenever they are passed as a UserId . This can be a problem, since we could, for example, pass a float , which may be an incorrect variable, since user IDs are usually not float . In other cases, when we could, for example, work with a Price object, disabling strong typing can lead to rounding errors, because PHP automatically converts float variables to int .


Immutability


By default, objects in PHP are passed by reference. This means that when we make changes to an object, it changes instantly in the entire application.


Although this approach has its advantages, it also has some drawbacks. Consider an example of a notification sent to the user via SMS and email:


 interface NotificationSenderInterface { public function send(Notification $notification); } class SMSNotificationSender implements NotificationSenderInterface { public function send(Notification $notification) { $this->cutNotificationLength($notification); // Send an SMS... } /** * Makes sure the notification does not exceed the length of an SMS. */ private function cutNotificationLength(Notification $notification) { $message = $notification->getMessage(); $messageString = substr($message->getValue(), 160); $notification->setMessage(new Message($messageString)); } } class EmailNotificationSender implements NotificationSenderInterface { public function send(Notification $notification) { // Send an e-mail ... } } $smsNotificationSender = new SMSNotificationSender(); $emailNotificationSender = new EmailNotificationSender(); $notification = new Notification( new UserId(17466), new Subject('Demo notification'), new Message('Very long message ... over 160 characters.') ); $smsNotificationSender->send($notification); $emailNotificationSender->send($notification); 

Since the Notification object is passed by reference, there is an unintended side effect. When reducing the length of a message in SMSNotificationSender associated Notification object was updated throughout the application, so the message was also cut off when it was later sent to EmailNotificationSender .


To fix this, make the Notification object immutable. Instead of providing set methods for making changes to it, add with-methods that make a copy of the original Notification before making these changes:


 class Notification { public function __construct( ... ) { /* ... */ } public function getUserId() : UserId { /* ... */ } public function withUserId(UserId $userId) : Notification { $c = clone $this; $c->userId = clone $userId; return $c; } public function getSubject() : Subject { /* ... */ } public function withSubject(Subject $subject) : Notification { $c = clone $this; $c->subject = clone $subject; return $c; } public function getMessage() : Message { /* ... */ } public function withMessage(Message $message) : Notification { $c = clone $this; $c->message = clone $message; return $c; } } 

Now, when we make changes to the Notification class, for example, by reducing the length of the message, they no longer apply to the entire application, which helps prevent various side effects.


However, note that in PHP it is very difficult (if not impossible) to make an object truly immutable. But in order to make our code more error-proof, it will be enough to add “immutable” with-methods instead of set-methods, since users of a class will no longer need to remember to clone an object before making changes.


Returning null objects


Sometimes we come across functions and methods that can return either a value or null . And these null return values ​​can be a problem, since it will almost always need to check for null values ​​before we can do something with them. This is again easy to forget.


To eliminate the need to test return values, we could return null objects instead. For example, we may have ShoppingCart with or without a discount:


 interface Discount { public function applyTo(int $total); } interface ShoppingCart { public function calculateTotal() : int; public function getDiscount() : ?Discount; } 

When calculating the final cost of ShoppingCart, before calling the applyTo method applyTo we now always need to check what the getDiscount(): null function returned getDiscount(): null or a discount:


  $total = $shoppingCart->calculateTotal(); if ($shoppingCart->getDiscount()) { $total = $shoppingCart->getDiscount()->applyTo($total); } 

If you do not perform this check, we will get a PHP warning and / or other side effects when getDiscount() returns null .


On the other hand, these checks can be avoided if we return a null object when the discount is not available:


 class ShoppingCart { public function getDiscount() : Discount { return !is_null($this->discount) ? $this->discount : new NoDiscount(); } } class NoDiscount implements Discount { public function applyTo(int $total) { return $total; } } 

Now, when we call getDiscount() , we always get a Discount object, even if there is no discount. Thus, we can apply a discount to the total amount, even if it is not, and we no longer need the if :


  $total = $shoppingCart->calculateTotal(); $totalWithDiscountApplied = $shoppingCart->getDiscount()->applyTo($total); 

Optional dependencies


For the same reasons that we want to avoid null return values, we want to get rid of optional dependencies by simply making all dependencies mandatory.


Take, for example, the following class:


 class SomeService implements LoggerAwareInterface { public function setLogger(LoggerInterface $logger) { /* ... */ } public function doSomething() { if ($this->logger) { $this->logger->debug('...'); } // do something if ($this->logger) { $this->logger->warning('...'); } // etc... } } 

There are two problems:


  1. We constantly need to check for the presence of the logger in our doSomething() method.
  2. When setting up the SomeService class in our service container, someone may forget to configure the logger, or it may not know at all that the class has the ability to do this.

We can simplify the code by making the LoggerInterface mandatory dependency:


 class SomeService { public function __construct(LoggerInterface $logger) { /* ... */ } public function doSomething() { $this->logger->debug('...'); // do something $this->logger->warning('...'); // etc... } } 

Now our public interface has become less cumbersome, and whenever someone creates a new instance of SomeService , he knows that the class requires an instance of the LoggerInterface , and therefore he can’t forget to specify it.


In addition, we got rid of the need to constantly check for a logger, which makes doSomething() easier to understand and less susceptible to errors whenever someone makes changes to it.


If we wanted to use SomeService without a logger, then we could apply the same logic as with returning a null object:


 $service = new SomeService(new NullLogger()); 

As a result, this approach has the same effect as using the optional setLogger() method, but it simplifies our code and reduces the likelihood of an error in the dependency embedding container.


Public methods


To make the code easier to use, it is better to limit the number of public methods in the classes. Then the code becomes less confusing, and we are less likely to abandon backward compatibility when refactoring.


The analogy with transactions will help to minimize the number of public methods to a minimum. Consider, for example, the transfer of money between two bank accounts:


 $account1->withdraw(100); $account2->deposit(100); 

Although a database using a transaction can cancel a withdrawal, if a deposit cannot be done (or vice versa), it cannot prevent us from forgetting to call either $account1->withdraw() or $account2->deposit() , which will to incorrect operation.


Fortunately, we can easily fix this by replacing two separate methods with one transactional:


 $account1->transfer(100, $account2); 

As a result, our code becomes more reliable, since it will be more difficult to make an error, completing the transaction partially.


Error Detection Examples


Error detection mechanisms are not intended to prevent them. They should only warn us about problems when they are discovered. Most of the time, they are outside of our application and check the code at regular intervals or after specific changes.


Unit tests


Unit tests can be a great way to make sure new code works correctly. They also help ensure that the code still works correctly after someone reorganizes part of the system.


Since someone may forget to conduct unit testing, it is recommended to automatically run tests when making changes using services such as Travis CI and GitLab CI . Thanks to them, developers receive notifications when something breaks, which also helps to make sure that the changes made work as intended.


In addition to error detection, unit tests are excellent examples of using specific parts of the code, which in turn prevents errors when someone else uses our code.


Code Coverage and Mutation Testing Reports


Since we can forget to write enough tests, it is useful when testing to automatically generate reports on code coverage by tests using services such as Coveralls . Whenever our code coverage drops, Coveralls sends us a notification, and we can add the missing tests. Thanks to Coveralls, we can also understand how code coverage changes over time.


Another way to make sure that we have enough unit tests is to use mutation tests, for example, using Humbug . As the name implies, they check whether our code is sufficiently covered with tests, slightly changing the source code and then running unit tests that should generate errors due to the changes made.


Using code coverage reports and mutation tests, we can make sure that our unit tests are sufficient to prevent errors.


Static code analyzers


Code analyzers can detect errors in our application at the beginning of the development process. For example, IDEs such as PhpStorm use code analyzers to warn us about errors and give hints when we write code. Errors can range from simple syntax to duplicate code.


In addition to the analyzers built into most IDEs, you can include third-party and even custom analyzers in the build process of our applications to identify specific problems. A partial list of analyzers suitable for PHP projects can be found on GitHub .


There are also online solutions, for example, SensioLabs Insights .


Logging


Unlike most other error detection mechanisms, logging can help detect errors in an application when it is running in production.


Of course, this requires that the code write to the log whenever something unexpected happens. Even when our code supports loggers, you can forget about them when setting up the application. Therefore, optional dependencies should be avoided (see above).


Although most applications are at least partially logged, the information that is recorded there becomes really interesting when it is analyzed and monitored using tools such as Kibana or Nagios . They can give an idea of ​​what errors and warnings occur in our application when people actively use it, and not when it is being tested.


Do not suppress errors


Even when logging errors, it happens that some of them are suppressed. PHP tends to continue working when a “recoverable” error occurs. However, errors can be useful in developing or testing new functions, as they may indicate errors in the code. This is why most code analyzers warn you when they find that you use @ for error suppression , as this may hide errors that will inevitably appear again as soon as the application is used.


As a rule, it is better to set the error_reporting PHP E_ALL level error_reporting PHP E_ALL in order to receive even the slightest warning message. However, do not forget to log these messages somewhere and hide them from users so that no confidential information about the architecture of your application or potential vulnerabilities is available to end users.


In addition to error_reporting , it is important to always include strict_types so that PHP does not automatically attempt to strict_types function arguments to their expected type, as this can lead to difficult-to-find errors (for example, rounding errors when converting float to int ).


Use outside PHP


Since poka-yoke is more of a concept than a specific technique, it can also be applied in areas not related to PHP.


Infrastructure


At the infrastructure level, many errors can be prevented by creating a common development environment, identical to the production environment, using tools such as Vagrant .


Automating the deployment of an application using build servers, such as Jenkins and GoCD , can help prevent errors when deploying changes to an application, since this process can involve many steps, some of which can be easily forgotten to be performed.


REST API


When creating a REST API you can implement poka-yoke to simplify the use of the API. For example, we can make sure that we return an error whenever an unknown parameter is passed in the URL or in the body of the request. , , , «» API-, , , , API, , .


, API color , -, API, colour . - , .


, API, , Building APIs You Won't Hate .



- . , . , color colour , , .


, . – , .



poka-yoke . , , , . .


Conclusion


poka-yoke , , , , . -, , , .


– , , , , , . , , .


, , public- .


')

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


All Articles