65.9K
CodeProject 正在变化。 阅读更多。
Home

一个软件设计原则:不要强迫我使用你的设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (27投票s)

2014年11月4日

CPOL

6分钟阅读

viewsIcon

29763

让我使用你的功能,而不是你的设计。

或者为什么 C++ 中没有 SerialPort

在 C++ 中为机器人开发代码时,我经常遇到通过串口与机器人、传感器或其他设备通信的问题。C 和 C++ 是所谓的非常接近硬件的语言。此外,它们是最常见、最古老的两大主流编程语言。因此,以可移植的方式通过串口通信应该是很直接的。<!--more--> 可能和 Python 一样直接。要发送几个字节,只需进行一次基本的 Python 安装,然后在任何操作系统上,您需要做的就是:$ pip install pyserial 这会下载并设置十几二十个文件,仅仅几 KB。然后,编写您的代码。
import serial
ser = serial.Serial(0)
ser.write("hello")
ser.close()
就这样!现在我们用 C++ 来做。假设您想编写一个类似的应用程序,仅仅是为了通过串口发送一些字节。第一个困难是似乎没有“标准”或被广泛接受的东西,所以我第一次尝试使用 MRPT。MRPT 是一个包含大量出色驱动程序、算法、工具、接口… 的机器人库,是一个令人惊叹且非常流行的机器人工具包。我可以利用整个 MRPT,但假设我不想引入对它的依赖,因为它非常大(60MB 源代码,>5k 源文件)并且包含大量我不需要的东西。我只想获取 SerialPort 功能。深入代码,您会发现 CSerialPort_win.cppCSerialPort_lin.cpp 文件,它们都实现了同一个 CSerialPort 类,该类在 CSerialPort.h 头文件中声明。在实现文件中,您可以找到
void  CSerialPort::open( )
{
	MRPT_START
	… 
	// Open the serial port:
	if ( INVALID_HANDLE_VALUE == (
	            hCOM = CreateFileA( m_serialName.c_str(), // Serial Port name
而在头文件中
class HWDRIVERS_IMPEXP CSerialPort : public CStream
		{
			friend class PosixSignalDispatcherImpl;
		public:
就是这样,实现依赖于一个宏 MRPT_START(它允许进行性能分析和异常处理),这似乎很容易删除,但头文件继承自 CStream,一个流(网络、文件、输出等)的基类,而 CStream 又依赖于一个序列化框架(CSerializable, CObject)。虽然库的整体软件设计很棒,但很难分离出在我看来应该是一个非常独立且耦合度低的组件。请不要误解我的观点,MRPT 真的很棒。问题仅仅在于该库的设计初衷并非如此。我的猜测是,已经存在一个更通用、不与机器人技术相关的解决方案。于是我对此进行了一些搜索,发现在(至少在 Stack Overflow 上)最被接受的解决方案是使用 boost::asio。对我来说,依赖 500MB 的源代码来发送一些字节似乎有些夸张,所以我也尝试提取串口功能。但我发现了一些高级抽象,例如,在 basic_serial_port.hpp 文件中
template <typename SerialPortService = serial_port_service>
class basic_serial_port
  : public basic_io_object<SerialPortService>,
    public serial_port_base
{
public:
您必须深入 win_iocp_serial_port_service.ipp 才能实际找到在 Windows 中打开端口的代码
boost::system::error_code win_iocp_serial_port_service::open(
    win_iocp_serial_port_service::implementation_type& impl,
    const std::string& device, boost::system::error_code& ec)
{
  if (is_open(impl))
  {
    ec = boost::asio::error::already_open;
    return ec;
  }

  // Open a handle to the serial port.
  ::HANDLE handle = ::CreateFileA(name.c_str()
令人惊讶的是,功能分布在不同的文件中,例如,后者包含打开、关闭和设置参数的代码,但发送和接收字节的代码位于另一个文件中。基本上,结论是您无法单独使用串口,您被迫与 Asio 一起使用。我们又遇到了同样的问题:boost 非常棒,asio 也非常出色。当然,它们(MRPT 和 boost::asio)的串口实现比 Python 的实现更具可配置性和功能性。我个人希望有一天我的代码能写得像这些项目中的任何一个一样好。但我看到了一个模式,一个不允许拥有简单小巧的 SerialPort 功能来仅仅同步地通过串口发送和接收一些字节的模式。因此,我敢于陈述 IMHO 可以被视为一个设计原则

让我使用你的功能,而不是你的设计。

当然,这个原则并非什么新鲜事。它与许多众所周知的原则和模式密切相关。它与当前流行的函数式编程方法密切相关,并且可以被视为许多模式(如单一职责、低耦合、关注点分离、高内聚等)的结果。但我从未见过它以这种方式被表述,我认为这可能是一个需要考虑的视角。“不使用你的设计”我指的是架构设计,显然每一行代码本身都有一些设计。我如何看待 SerialPort 的例子应该如何解决?我看到的主要问题是未能将 SerialPort 识别为一个一级构建块,因此它应该拥有自己的“包”、“命名空间”、“库”…等等。有鉴于此,它们很容易构建到这些库中。例如,在 MRPT 的情况下,您可以让 SerialPort 和流及序列化框架彼此独立。然后,可以使用模板(为求清晰,仅为演示想法)非常轻松地将两者绑定在一起。
class SerialPort{
public:
	void write(char c);
};

class MyBaseStream{
	virtual void write(std::string str)=0;
	friend MyBaseStream& operator <<(MyBaseStream& mystream, std::string str){
		mystream.write(str);
		return mystream;
	}
};

template <typename T>
class Stream: public MyBaseStream{
public:
	Stream(T& _port): port(_port){}
	virtual void write(std::string str){
		for(auto c: str)
			port.write(c);
	}
private:
	T port;
};

//example client code of generic streams
void write2stream(MyBaseStream& stream, std::string str){
	stream<<str;
}

using SerialPortStream = Stream<SerialPort>;

int main(){
SerialPort serial;
    SerialPortStream serial_stream(serial);
    serial_stream<<"Hello World\n";
    write2stream(serial_stream, "Good Bye\n");
}
这样,SerialPort 和 Stream 类都成为我们项目中两个独立的、一流的公民。它们变得非常容易测试(和模拟)、理解、维护和扩展,并且也更容易使用它们来扩展和扩展整个项目。我知道我用这个软件设计提案并没有说出什么全新的东西,例如它类似于 Alexandrescu 的基于策略的设计,尽管最终目标可能不同。

遵循此原则的失败在 C/C++ 中更为常见

我在 C 和 C++ 项目中见过这种模式几次,但在其他语言(至少是我更多使用的 Java 和 Python)中很少见,我认为有一个原因,与软件设计无关:缺乏广泛使用的依赖项管理器。不,操作系统包管理器、安装程序等不足以解决这个问题。即使这些库的作者决定在其设计中解耦 SerialPort 的基本包装器功能,这样做也几乎没有好处,用户仍然需要手动提取这些文件并将其集成到他们的项目中,这听起来并不像合理的工程实践,并且肯定会产生维护问题和更新缺失。作者不太可能决定为 SerialPort 创建一个单独的项目/库,这不仅在短期内需要更多的工作,而且在中长期维护和使用起来也更困难。因此,开发人员只是按需推出他们的设计并填充功能。我也这样做过很多次。相反,如果存在依赖项管理器,很可能会出现一个简单、独立且健壮的 SerialPort 实现,并被广泛采用,而创建异步通信框架或机器人应用程序的人们将使用它,并需要编写的代码更少。
© . All rights reserved.