Você mencionou as portas COM, portanto, abordarei especificamente esse caso.
- Designing and structuring classes around hardware interfaces to maximize encapsulation.
Existem duas maneiras de utilizar as portas COM, Command / Response e Streaming. Como você projeta sua interface dependerá de como você planeja usar a porta. Você só estará lendo e gravando no dispositivo ocasionalmente ou estará abrindo um fluxo constante de dados de um dispositivo para outro?
Para o comando / resposta eu criaria uma interface assíncrona mais ou menos assim.
public interface IDevice : IDisposable
{
// Sends command without listening for a response
Task Send(Command command);
// Sends Command and waits for response
Task<Response> Send(Command command);
// Sends Command and listens for Response. Cancellable
Task<Response> Send(Command command, CancellationToken token);
}
Para o fluxo de dados, normalmente aproveito a classe Stream juntamente com um StreamWriter
ou StreamReader
personalizado.
Em ambos os casos, você quer ter certeza de que interfaces
ou abstract
classes dependem, porque você vai querer ridicularizar o hardware para testar outras partes do seu código.
- How to handle initialization and releasing/destroying of the hardware and their wrapping/related class.
Você tem basicamente duas opções de inicialização aqui. Você pode abrir o fluxo no construtor ou fornecer um método Open()
. Ou vai funcionar e ambos têm seus prós e contras. Abrir a conexão no ctor significa que você pode obter exceções do ctor e algumas pessoas não gostam disso. Por outro lado, ter um método que deve ser chamado antes de usar uma classe é um tipo de acoplamento temporal que muitas pessoas não se importam. É fácil esquecer de chamar o método para colocar a classe em um estado utilizável. Então, escolha aquele que se encaixa melhor com a sensibilidade da sua equipe.
Para o de struction, você desejará implementar a interface IDisposable
para poder fechar e liberar a porta de maneira controlada e confiável.
using( var device = new Device("COM5")
{
// do stuff with port
}
Quando saímos do bloco using
, o método Dispose()
que você implementou é chamado, liberando a porta.
- How to store hardware state (for example storing the state in the hardware wrapper-class, a context vs. always querying the hardware, when a state is requested).
Eu normalmente não armazeno muito estado. Normalmente, estamos consultando o hardware para qualquer estado atual e refutando isso.
- How to make this fit in with OO best practices, in particular dependency inversion.
Apenas trate o hardware como você trataria um banco de dados. Código para interfaces em vez de implementações. Criamos um interface
que representa nosso hardware e uma implementação dessa interface, portanto, qualquer código de cliente depende da nossa interface IDevice
. Você apenas construtor injetar a implementação como você faria com qualquer outra classe.
public class Foo
{
private readonly IDevice _device;
public Foo(IDevice device)
{
_device = device;
}
//...
}
Na verdade, eu normalmente injetamos uma fábrica em vez de uma instância quando estou lidando com IDisposable para tornar o teste mais fácil.