Jump to content

Order of dependency injection


NotionCommotion

Recommended Posts

I need to create an entity which models the connection between a PHP server and another device so that a web app can change the entity's properties which in turn will update the server DB as well as the other device.  A little background information for context is as follows:

When a socket clients connects to the PHP server, it will register by providing its GUID, and in turn the server will query its DB to identify the specific client.  The client has various other properties associated with the socket connection such as reconnect_timeout, response_timeout, etc, and the associated values will also be stored in the server's DB.

While all socket clients use the same approach to communicate to the PHP server, several "flavors" of socket clients exists which use a specific protocol (Modbus/RTU, Modbus/IP, ControlNet, BACnet/IP, BACnet/MSTP, KNX, DALI) to communicate to downstream devices.  Based on the used protocol, there will be different properties as well as different methods (i.e. some protocols allow one to query the device to retrieve a list of available data).

To create the entity using inheritance, I would likely do so where:

class ModbusRtuSocketClient extends ModbusSocketClient (which extends SocketClient)
class ModbusIpSocketClient extends ModbusSocketClient (which extends SocketClient)
class ControlNetSocketClient extends SocketClient
etc..

But instead of doing so by using inheritance, I am thinking that I should do so through injection.  For instance, I create an object called SocketClient which just deals with that scope of work and is protocol agnostic.  I also create a second object which deals with a given protocol such as ModbusRtuProtocol extends ModbusProtocol extends Protocol.  I then do one of the following:

$entity = new SocketClient(new ModbusRtuProtocol());
//or
$entity = new ModbusRtuProtocol(new SocketClient());

//and do
$entity->setSomeSocketClientProperty(123);
$entity->getProperty()->setSomeSpecificProtocolProperty(321);
$entity->updateEndDevice();
$entityManager->persist($entity);
$entityManager->flush();

//or do
$values=$entity->queryDevice();


The first approach "seems" right, but I am not sure.  One concern is I cannot create a standard interface to govern all protocols because the methods are so protocol specific (maybe I need to think harder how to make a standard interface?).   Any recommendations where I should be going on this?

PS.  If you think I shouldn't be doing so through injection but through inheritance, please advise.

Link to comment
Share on other sites

Inheritance and injection are two different things, and you should probably be using both.

Inheritance is what helps you write less code. If two different Modbus protocols would share a non-trivial amount of code, you might as well move that code into a superclass. And if many protocols would share code, you move that stuff up into a superclass as well.

Injection is how you pass stuff around. At some point the different protocol classes will get involved. Either you use injection by determining the right object ahead of time, or you use a factory so that the right object gets constructed when it's needed. Maybe a factory to create the object that will be injected.

Link to comment
Share on other sites

Thanks requinx,

Yes, I agree inheritance has its use regardless of what I do with injection.  For instance,  I showed ModbusRtuProtocol extends ModbusProtocol extends Protocol on the initial and was trying to imply that both ModbusProtocol and Protocol would be abstract superclasses.

Also, agree some sort of factory will likely be beneficial.

Do you agree that Protocol should likely be injected into ClientConnection instead of injecting ClientConnection into Protocol?  What is your thought pattern when making these decisions?

Link to comment
Share on other sites

13 hours ago, requinix said:

Depends. What "uses" what? I would expect the Connection uses a Protocol, so the Protocol should be injected into the Connection.

I suppose so, but it also seems like they use each other to communicate.  Cannot Phone, Email, and WalkieTalkie all be injected with Cantonese to utilize that language?

Link to comment
Share on other sites

2 hours ago, NotionCommotion said:

I suppose so, but it also seems like they use each other to communicate.  Cannot Phone, Email, and WalkieTalkie all be injected with Cantonese to utilize that language?

Oops.  I mean cannot CantoneseCommunicator be injected with WalkieTalkie to communicate over radio waves?

Link to comment
Share on other sites

4 hours ago, NotionCommotion said:

Oops.  I mean cannot CantoneseCommunicator be injected with WalkieTalkie to communicate over radio waves?

Yes, but now you're changing the nature of the object. Cantonese as a language is not the same thing as a Cantonese "communicator".

If WalkieTalkie was responsible for making sure information being sent was in the right language, it would need to know a language "protocol". That would be Cantonese. So you would inject Cantonese into the WalkieTalkie.

class WalkieTalkie {

	public function __construct(Language $lang) {
		$this->lang = $lang;
	}

}

new WalkieTalkie(new Cantonese());

If you have a CantoneseCommunicator, you would be specifically coupling Cantonese the language with whatever device.

class CantoneseCommunicator {

	public function __construct(Communicator $comm) {
		$this->lang = new Cantonese();
		$this->comm = $comm;
	}

}

new CantoneseCommunicator(new WalkieTalkie());

The best approach would be to inject the language and the communicator.

class LanguageCommunicator {

	public function __construct(Communicator $comm, Language $lang) {
		$this->comm = $comm;
		$this->lang = $lang;
	}

}

new LanguageCommunicator(new WalkieTalkie(), new Cantonese());

 

Link to comment
Share on other sites

As requinix suggested in the last post, I would probably just have your client object (or some other object) take both a socket and a protocol as arguments.  The protocol object is responsible for parsing/generating byte streams according to the rules of the protocol.  The socket object is responsible for transmitting and receiving those byte streams across the network.  The client object coordinates those two jobs.

<?php

class ProtocolFrame {
}

class Protocol {
    public function parseStream(string $bytes) : ProtocolFrame;
    public function serializeFrame(ProtocolFrame $frame) : string;
}

class SocketClient {
    public function __construct($host, $port);
    public function read() : string;
    public function write(string $bytes) : int;
}

class Client {
    public function __construct(SocketClient $socket, Protocol $protocol);
}

I'd have to know more about the protocols and such you have to deal with to make a better suggestion.   If you can't come up with a common interface that you can implement for each protocol it may make things more complicated. 

The above example makes a few assumptions, namely that your protocols are based on sending distinct packets/frames that can be divided up.  If not, adjust accordingly.  Instead of a generic serializeFrame method you might instead make more specific methods like sendGreeting() or changePosition() or whatever.

Having the protocol objects just generate frames/byte streams rather than work with a socket may allow for easier testing as well as you can just compare the generated data to some correct string constants or whatever rather than having to mock up a socket object.

On 3/18/2019 at 4:22 PM, NotionCommotion said:

One concern is I cannot create a standard interface to govern all protocols because the methods are so protocol specific (maybe I need to think harder how to make a standard interface?)

I think this is something that you should look at more.  Maybe your right in that a single interface isn't possible, but you definitely want to spend some time and think about it.  If your concern is some protocols taking longer to do something than others, maybe use something like react/promise to handle the difference in timing.  If it's an issue that one protocol needs extra details that others don't, maybe there is a way to get that information to the protocol via some other method (like a second interface maybe).

Perhaps post another topic about a specific case with details if you're stuck and want some help there.

 

 

Link to comment
Share on other sites

Thanks requinx,

Yes, I did "sneak" in the new nature of the object :)  Your rational on injecting both makes sense.  Often, it seems unnecessary at first, but then those dang details get a bit unruly.

Thanks kicken,

Actually, I really have three (and surely more) objects to deal with.  Thanks primarily to you, the parsing and generating of byte streams is working great.  Is your https://packagist.org/packages/react/promise significantly different than https://github.com/reactphp/promise?  When I referred to protocol, I was referring to the higher level logic needed to deal with various gateways which all do somewhat the same thing but utilize different APIs which is partially driven based on their native protocol.  

For instance, web-client requests web-server to command some 3rd party device, php-server sends to socket server which sends to given gateway, gateway uses its native protocol to communicate to applicable 3rd party device, gateway acknowledges back to PHP which allows the DB to be updated and in turn reply to web-client.  All of the communication between php and gateway is basically the same and uses json-rpc methods and results.  Based on the the 3rd party devices, however, some methods are just not applicable (one example is how some have discovery network wide, others only on a per device basis, and others not at all), address schema is not the same (some need an address offset, others need multiple property identifiers, etc), results are bundled differently, etc.  I cannot change each gateway to implement a common interface, but I could create drivers in PHP (not sure if this is the right word) which each know how to deal with each gateway.  The interface between the standard PHP scripts and this driver interface is what has got me in a quandary.

Thanks again for all your help both on this and many earlier topics.

Link to comment
Share on other sites

1 hour ago, NotionCommotion said:

They are the same.  Note the github url on the packagist page.

1 hour ago, NotionCommotion said:

The interface between the standard PHP scripts and this driver interface is what has got me in a quandary.

Generally speaking in these situations I start by designing the PHP interface I need and then try and code the necessary drivers to match that interface.  As drivers are built it may be necessary to change the interface to accommodate.  For example you may start with:

interface DeviceController {
    public function start() : Promise;
    public function stop() : Promise;
    public function open() : Promise;
    public function close() : Promise;
    //...
}

Then, foreach device you have to support make a driver that implements that interface, for example:


class ModbusRtuDevice implements DeviceController {
    private $socket;
    public function __construct(SocketClient $socket){
        $this->socket = $socket;
    }

    public function start() : Promise {
        return $this->send('start');
    }

    public function stop() : Promise {
        return $this->send('stop');
    }

    public function open() : Promise {
        return $this->send('open');
    }

    public function close() : Promise {
        return $this->send('close');
    }

    private function send($action) : Promise {
        $command = json_encode([
            'action' => $action
            , 'arguments' => []
        ]);

        return $this->socket->sendCommand($data);
    }
}

The just code the rest of your application according to the DeviceController interface.

I'd focus first on just creating an interface that you'd ideally want.  Then focus on implementing a driver for it for whatever your most common device type is.  Adjust the interface design where required as problems arise.  Once that is working, move on to implementing a driver for the next most common devices.  Again, adjust the interface as required but keep in mind your first driver.  If a change is needed and avoid just adding some driver specific method/parameter.  Adjust the interface to something that works for both devices and make the adjustments to the original driver as needed to accommodate the new interface design.

A lot of this is sometimes easier said than done, I know.  Sometimes it may take quite a few iterations to really get things in a good place.

 

Link to comment
Share on other sites

Agree strongly with the advice given previously.  There is almost always a way to design an interface once you think deeply about the general purpose of the task.  The internal specifics of each protocol just need to be hidden in the injected class(es), which is very much the point of Dependency Injection. 

Link to comment
Share on other sites

Thanks kicken,  Good advice on how to think through the general approach.  Agree with both you and gizmola that time is well spent thinking through the interface.  And agree some of this is easier said than done!  

I didn't realize the implications of Promise until seeing your sample script.  While I am using promises indirectly through some of the react socket classes, I am not doing so directly.  Looks like their use abstracts the eventual resulting outcomes which seems like a good thing.

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.