为Raspberry Pi开发.NET应用程序:第2部分

目录

介绍

背景

问题描述

解决方案结构

关注点分离

重构

单元测试、模拟和依赖注入

运行代码

兴趣点


一个简单而现实的Raspberry Pi .NET解决方案,演示了项目布局,单元测试和最佳实践。开发是在能够运行Visual Studio Code的计算机上进行的,该计算机通过SSH远程连接到Raspberry Pi

介绍

可以在.NET中为Raspberry Pi编写应用程序,就像在任何其他环境中一样。编写良好的.NET(和C#)代码的优点是,可以实现更具伸缩性和可维护性的解决方案。在本文中,我将描述可用于创建此类解决方案的项目结构。这将包括单元测试,模拟和依赖注入。该应用程序将是一个简单的应用程序,但是解决方案结构将实现任何大型开发项目的两个关键目标:

  1. 该代码应尝试实现关注点分离
  2. 该代码应有助于重构

这两个原则将在本文后面进行演示。我将使用.NET Core IoT,这是Microsoft的官方解决方案,用于与Raspberry Pi等平台上的低级设备接口。

背景

第1部分:在Raspberry Pi上轻松设置.NET Core并使用VS代码进行远程调试

在上一篇文章中,我描述了如何设置开发环境,该环境将使使用自动化部署和调试的远程Raspberry Pi开发变得容易。那篇文章包括几个简单的程序。本文将以该文章为基础,并说明如何创建更现实,更大的解决方案。

问题描述

我为此示例解决方案选择了一个简单的问题。对此的要求很明确,但这是有意的。解决方案结构是重要的部分,并且通过解决一个简单的问题,这有望使焦点集中在解决方案上。

因此出现了问题:将有两个按钮开关和两个LED。我们将创建一个控制台应用程序,使每个LED都可以打开或关闭。这些按钮将识别短按的时间。我们还将有两种测试模式——一种持续使LED闪烁直到长按按钮2为止,另一种显示消息以表明已按下哪个按钮,长按按钮2也会终止该模式。

我已将开关和LED连接到以下GPIO引脚:

显然,这是一个人为的例子。一个更现实的解决方案将对输入和输出有所帮助,并且可能包括更复杂的设备。在撰写本文时,IOT项目包括70多个与温度,加速度计和光传感器等设备以及许多有线和无线设备的接口。

解决方案结构

顶层结构如下所示:

> .vscode
> Doc
> RpiBlinkButtonApp
> RpiBlinkButtonLib
> RpiBlinkButtonLibTests
> Scripts
.gitignore

有三个代码文件夹,RpiBlinkBut​​tonAppRpiBlinkBut​​tonLibRpiBlinkBut​​tonLibTests。在更大的解决方案中,可能会有更多的类库项目,并且每个类库都应该有自己的Tests项目。可能会有更多Application项目。另外,请注意,我没有为该App项目提供单元测试,但是对于真正的解决方案,也应该为此项目提供单元测试。

.vscode脚本文件夹是基于以前的文章,以及包括脚本和配置,以支持远程部署和调试。

关注点分离

本文中方法的主要目标之一是关注点分离。这对于较大的解决方案很重要,在这种解决方案中,通常不可能或不希望为了整个解决方案而理解整个解决方案。为此,需要有清晰的抽象层。显而易见的第一个抽象层是实际的物联网库本身。值得看一下该存储库中的代码,但理解所有内容以使用它并不是必需的。

我们的第一个抽象是拥有一个类库项目。在该项目中,我选择创建两个控制器类。该LED控制器类封装了两个LED——我选择了有红色和绿色LED。有两种方法可以打开或关闭红色或绿色LED按钮控制器有两个按钮,并且因为这些是稍微更复杂(例如,要求取消开关),控制器使用另一个类,GpioButton。但是,使用按钮控制器应该很容易,我决定提供一个ButtonPressed事件,该事件的EventArgs参数描述发生了什么类型的事件(短按或长按的按钮12)。

重构

该代码应该适合重构。与项目开始时一成不变的设计不同,现代代码应随着项目的进行不断地重构。包括VS Code在内的IDE提供了使此过程更容易的工具,但我们还需要确保代码的复杂性不会失控,因为一旦发生这种情况,就很难进行重构,并且在某些情况下变得不可能。

重构的另一个关键部分是确信重构没有破坏任何东西。这是进行良好单元测试的关键原因之一。

单元测试、模拟和依赖注入

库中的每个类都有其自己的测试类。理想情况下,我们只想测试该类并把对其他类的调用存根。最好的方法是模拟被调用的类。我已经使用了Moq,可以通过NuGet轻松安装。模拟的关键部分是使用依赖注入和控制反转。他们的主要思想是,我们将实例化的对象传递给我们的类,而不是将类创建对象本身。这样,我们就可以传递模拟对象而不是真实对象。这样的一个例子是在ButtonControllerTests中的CreateMockedObjects()

private MockButtonCollection CreateMockObjects()
{
    var mockObjects = new MockButtonCollection();

    mockObjects.Button1 = new Mock<GpioButton>(null);
    mockObjects.Button2 = new Mock<GpioButton>(null);
    mockObjects.GpioDriver = new Mock<GpioDriver>();

    // Mock the methods to setup a pin
    mockObjects.GpioDriver.Protected().Setup<bool>("IsPinModeSupported", ItExpr.IsAny<int>(),
                                                   ItExpr.IsAny<PinMode>()).Returns(true);

    // Create a ButtonController
    mockObjects.GpioController = new GpioController(PinNumberingScheme.Logical,
                                                      mockObjects.GpioDriver.Object);

    mockObjects.ButtonController = new ButtonController(mockObjects.GpioController,
                                      mockObjects.Button1.Object, mockObjects.Button2.Object);

    // Initialize the button controller
    mockObjects.ButtonController.Initialize();

    return mockObjects;
}

另一个例子是我们传递给GpioButton类的DateTimeProvider类。可以如下模拟:

var mockDateTime = new Mock<IDateTimeProvider>();

var dateNow = DateTime.UtcNow;

mockDateTime.SetupSequence(m => m.UtcNow)
  .Returns(dateNow)
  .Returns(dateNow.AddMilliseconds(20))
  .Returns(dateNow.AddMilliseconds(30));

这使我们可以返回特定的值,在这种情况下,第一个调用UtcNow()获取当前时间,随后的调用获取时间+ 20ms,然后是+ 30ms然后,我们可以在GpioButton中控制这样的代码:

else if ((_dateTime.UtcNow - PressedStart).TotalMilliseconds >= LONG_PRESS_DURATION)
{
  longReleaseAction();
}

依赖项注入的问题之一是使用不可模拟的第三方类。一个很好的例子是GpioController。为了使类成为可模拟的,它需要从接口派生(例如IList),或者需要具有虚拟方法。GpioController两者都不具备,因此我们无法创建模拟程序GpioController。幸运的是,GpioController构造函数确实将 GpioDriver作为参数。这使我们可以创建一个模拟GpioDriver。然后,我们可以调用GpioController,将调用我们的模拟程序GpioDriver。一个简单的例子就是LedControllerTests,在其中我们可以检查SetLed()方法实际写入正确的gpio pin

public void SetLed_ShouldCall_DriverWriteMethod(string method, int pin, bool on)
{
  // Given I have created a mock Gpio driver
  var mockDriver = new Mock<GpioDriver>();

  // And I have mocked the methods to write to a pin
  mockDriver.Protected().Setup<bool>("IsPinModeSupported", ItExpr.IsAny<int>(),
                                                  ItExpr.IsAny<PinMode>()).Returns(true);
  mockDriver.Protected().Setup<PinMode>("GetPinMode", ItExpr.IsAny<int>())
                        .Returns(PinMode.Output);

  // And created a LedController
  var gpioController = new GpioController(PinNumberingScheme.Logical, mockDriver.Object);
  var ledController = new LedController(gpioController);
  ledController.Initialize();

  // When I call the controller - e.g. led.SetRed(true)
  typeof(LedController).GetMethod(method).Invoke(ledController, new object[] { on });

  // Then I expect the pin to be written to
  mockDriver.Protected().Verify("Write", Times.Once(), ItExpr.Is<int>(p => p == pin),
                  ItExpr.Is<PinValue>(m => m == (on ? PinValue.High : PinValue.Low)));
}

运行代码

有两个步骤可以运行或调试程序。假设已按照这些文章的第1部分设置了Raspberry Pi,则需要两个步骤。这些步骤是:

  1. 设置Raspberry Pi名称(通过Ctrl Shift P
  2. 从菜单或活动栏(通常位于左侧)运行

兴趣点

使用Raspberry Pi C#代码与处理任何其他C#代码一样容易。我们拥有重构、智能感知等所有可用工具,这些工具使我们比在Raspberry Pi上尝试编写Raspberry Pi应用程序容易得多。

我在这里没有介绍它,但是很明显,我们可以包含其他任何.NET应用程序中都将包含的功能,例如实体框架,具有广泛支持的Web应用程序等。

.NET中为Raspberry Pi进行开发与在PythonJavaScriptC ++中进行开发(在使用此处描述的Raspberry Pi设置在VS Code中都应该可以进行开发)不同,在.NET中,C#代码在开发机器上,而对于其他语言,源通常位于目标计算机上。我想无论哪种方式都没关系(尽管您可能有自己的偏好),但是了解这一点可以使事情更清楚。

从理论上讲,我们可以在开发机器上本地运行代码(无论是LinuxMacOS还是Windows)。但是,gpio调用将不起作用(如果尝试使用,则会发现它们抛出不支持的异常)。但是应该可以创建与之通信的模拟器GpioDriver。但是,这超出了本文的范围!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章