-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Question on coupling of static classes #178
Comments
DI is dependency injection. There is no injection of dependencies in your code. You initialize the dependency by the class name. Dependency injection would be if you had service final class Tcp implements CommunicationInterface
{
private SocketFunctionsInterface $socket;
public function __construct(SocketFunctionsInterface $socket)
{
$this->socket = $socket;
}
} I would implement it somehow like this: interface Stream
{
public function open(): void;
public function close(): void;
public function write(string $string): bool;
public function read(): string;
} final class SocketStream implements Stream
{
private string $hostname;
private int $port;
private int $timeout;
private ?resource $socket = null;
public function __construct(string $hostname, int $port, int $timeout)
{
$this->hostname = $hostname;
$this->port = $port;
$this->timeout = $timeout;
}
public function open(): void
{
if (is_resource($this->socket)) {
throw new StreamStateException('Stream already opened.');
}
$socket = fsockopen($this->hostname, $this->port, $errno, $errstr, $this->timeout);
if (!is_resource($socket)) {
throw new OpenStreamException($errstr, $errno);
}
$this->socket = $socket;
}
public function close(): void
{
if (is_resource($socket)) {
fclose($this->socket);
$this->socket = null;
}
}
public function write(string $string): bool
{
if (!is_resource($this->socket)) {
throw new StreamStateException('Stream not opened.');
}
return (bool) fwrite($this->socket, $string);
}
public function read(): string
{
if (!is_resource($this->socket)) {
throw new StreamStateException('Stream not opened.');
}
return (string) fgetc($this->socket);
}
public function __destruct()
{
$this->close();
}
} final class Tcp implements Communication
{
private Stream $stream;
public function __construct(Stream $stream)
{
$this->stream = $stream;
}
public function open(): void
{
// Infinite loop to try to establish a connection. I do not advise doing this
while (true) {
try {
$this->stream->open();
break;
} catch (OpenStreamException $e) {
// ignore connection errors
usleep(100000); // wait before next connection
}
}
}
} I prefer not to initialize the connection in the constructor and only initialize it when really needed. |
Thank you for the fast reply. I was a bit reluctant, when it came to opening the connection outside of the constructor. That way I would be able to rely, that the connection already has been established once I got an object, avoiding connection related exceptions and code handling the connection status outside of connection-related methods. However that reluctance finally lead to my dilemma (and a bug regarding the Socket constructor 🤦). Thanks for opening my eyes. My question has been answered. |
The main problem is that you want to retry the connection if the connection fails. This requires a method to explicitly make the connection, such as final class Tcp implements Communication
{
private Stream $stream;
public function __construct(Stream $stream)
{
$this->stream = $stream;
// Infinite loop to try to establish a connection
while (true) {
try {
$this->stream->open();
break;
} catch (OpenStreamException $e) {
// ignore connection errors
usleep(100000); // wait before next connection
}
}
}
} Or you can reconnect in the final class SocketStream implements Stream
{
private resource $socket;
public function __construct(string $hostname, int $port, int $timeout)
{
// Infinite loop to try to establish a connection
while (true) {
$socket = fsockopen($hostname, $port, $errno, $errstr, $timeout);
if (is_resource($socket)) {
$this->socket = $socket;
break;
}
// ignore connection errors
usleep(100000); // wait before next connection
}
}
// ...
} final class Tcp implements Communication
{
private Stream $stream;
public function __construct(Stream $stream)
{
// connection established
$this->stream = $stream;
}
} Or you can make a connection factory interface StreamFactory
{
public function open(): Stream;
} final class SocketStream implements Stream
{
private resource $socket;
public function __construct(string $hostname, int $port, int $timeout)
{
$socket = fsockopen($hostname, $port, $errno, $errstr, $timeout);
if (!is_resource($socket)) {
throw new OpenStreamException($errstr, $errno);
}
$this->socket = $socket;
}
// ...
} final class Tcp implements Communication
{
private Stream $stream;
public function __construct(StreamFactory $factory)
{
// Infinite loop to try to establish a connection
while (true) {
try {
$stream = $factory->open();
break;
} catch (OpenStreamException $e) {
// ignore connection errors
usleep(100000); // wait before next connection
}
}
$this->stream = $stream;
}
} I also recommend limiting the number of connection attempts. You can specify a negative number to make the loop endless. final class Tcp implements Communication
{
private Stream $stream;
public function __construct(Stream $stream, int $attempts)
{
$this->stream = $stream;
while ($attempts--) {
try {
$this->stream->open();
break;
} catch (OpenStreamException $e) {
// ignore connection errors
usleep(100000); // wait before next connection
}
}
}
} |
I wrote a Socket class implementing a Stream interface according to your example. You are mentioned as one of the authors, because the DIP solution was yours. I hope, that's ok. I solved the connection retry problem using a recursive method call in the SerialPort constructor (formerly known as Tcp class). Features:
final class SerialPort implements Communication
{
private const MICROSECONDS_PER_SECOND = 1000000;
public const DEFAULT_ATTEMPTS = 3;
public const DEFAULT_RETRY_WAIT = 1.0;
private $stream;
public function __construct(Stream $stream, int $attempts = null, float $retryWait = null)
{
$this->stream = $stream;
$attempts = $attempts ?: self::DEFAULT_ATTEMPTS;
$retryWait = $retryWait ?: self::DEFAULT_RETRY_WAIT;
$retryWait *= self::MICROSECONDS_PER_SECOND;
$this->connect($attempts, (int)$retryWait);
}
private function connect(int $attemptsLeft, int $retryWait): void
{
try {
$this->stream->open();
} catch (OpenStreamException $exception) {
--$attemptsLeft;
if ($attemptsLeft < 1) {
throw $exception;
}
usleep($retryWait);
$this->connect($attemptsLeft, $retryWait);
}
}
//...
} |
I recently stumbled across a problem applying the Dependency Inversion Principle (DIP) trying to write classes for communication with either local (
fopen()
) or remote (fsockopen()
) serial ports. (In the following example I'll stick to theTcp
class usingfsockopen()
and leave out the interfaces.)In order to test the code of the
Tcp
class, I movedfsockopen()
fgetc()
andfrwite()
into a separate classSocket
.Socket
mustn't contain any code, that would need testing. However, in order to create an instance of thatSocket
class,fsockopen()
needs to be called and that can't be done outside of theTcp
class.My concern is:
The case of injecting a static class before the constructor is called, in this case
self::$socketClass
, is not covered by DIP. The solution above doesn't look clean to me.What can I do?
The text was updated successfully, but these errors were encountered: