简体   繁体   中英

How to set up DI / IoC and/or Factory pattern design in C#

I have a design that requires an SSH connection in order to get to a command-line interface to run some system/feature level test automation. There are several different types of SSH clients possible:

  • Direct connections to a normal C/Bash shell
  • Tunneled/forwarded connections within a VM or through a gateway server, etc.
  • Connections that require a particular command-line debug interface to be launched from a shell after log-in
  • etc.

I'm using the SSH.NET open source library as a NuGet package under C# with Visual Studio, and have gotten everything working in a simple case of connecting, issuing a command and reading back the StdOut and StdErr responses.

My issue is, I'd like to implement a few Session/Command layers here, and am not sure how to properly use interfaces to implement them. I'd like to use DI/IoC to separate the SshSession and SshCommand layers from each other and from the SSH.NET SshClient class, but how do I get started with the design?

I've read a lot of explanations of DI/IoC and implemented some unit tests using IoC containers and such, but the core way to setup the DI is still not clicking. I would think I wouldn't need an IoC container to implement this, but might need to use the factory pattern or even some other patterns I'm still not familiar with.


For example, to create multiple client types, with different connection criteria, I was thinking I would need something like:

  • ISshSession interface
  • SshSessionBase : ISshSession as a base class
  • Then create custom classes that use SshSessionBase for each of the clients.

But then how does the DI actually work? Do the custom classes get injected into the constructor of the base class, as IsshSession objects? Or do I need another factory or executor class that consumes these custom class objects?

I might be mixing up topics here.. There's just something I'm missing to convert what I'm reading about into an actual implementation - appreciate any help!

I am not really sure if I understood your design, but the general approach could be the following:

  1. Think about the behavioural and structural elements all client connection handlers have in common and either extract a base class or interface. For example, there might be a Connect() or Send(ICommand) method.

  2. Think about the structural and behavioural elements of other components in your design, eg commands or loggers or whatever. Some sort of base class library will be the result.

  3. Decide how you want to instantiate concrete instances of connection types. If the decission about the connection type relies on a set of parameters, you can utilize a factory. The factory will take a set of parameters and return a connection type instance. The factory could fulfill an interface itself and thus be resolved by the IoC itself (Abstract Factory).

The factory itself could just instantiate concrete classes, but your connection classes may utilize constructor-based injection to get access to other components (loggers, helpers, whatever), thus your factory could rely on the IoC Container, something like:

Update : As Michael pointed out, passing the container to the factory might be convenient but hides the concrete dependencies the factory has. While it might be okay with smaller applications, a better approach is passing all connection-type dependencies to the Factory which instantiates concrete instances.

class StandardConnectionFactory : IConnectionFactory
{
    private readonly IContainer iocContainer;

    public StandardConnectionFactory(IContainer iocContainer) 
    {
        this.iocContainer = iocContainer;
    }

    public IConnection Create(string param1, int param2, ...)
    {
         if (...) return iocContainer.Resolve<IFancySshConnection>();
         else return iocContainer.Resolve<IAnotherConnection>();
    }
} 
  1. If the decission about which connection type to use rather relies on some simple enum or string (eg the user decides what connection to use by a command line param), some IoC Containers (eg Autofac) allow to register a class with a name and resolve by that name. In that case, you do not need a factory and might end up with something like:

(Pseudo-Code)

// Registration Process
containerBuilder.Register<FancySshConnection>().For<IConnection>().WithName("ssh");
containerBuilder.Register<FancyFtpConnection>().For<IConnection>().WitName("ftp");

// Resolve
var connection = container.ResolveByName("ftp")
  1. When you finished to implement a way to instantiate connections, either by an explicit factory (3) or some built-in instantiation (4), the dependencies of the connection type known to your IoC container will be injected automatically. For example, the above call to iocContainer.Resolve<IFancySshConnection>(); will instantiate the concrete connection type, look at its constructor, try to resolve each parameter, inject them, and so on. It will climb up the dependency tree as long as it finds dependencies that were registered before. So basically all you have to do is registering your base class components (2).

  2. If things get bigger and more complex, you may end up with alof of registrations. Some IoC containers (I stick to the Autofac example because I really like Autofac) provide an easy way to split registrations into modules. This allows to distribute the process of registration into different assemblies.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM