一个软件设计原则:不要强迫我使用你的设计
让我使用你的功能,而不是你的设计。
或者为什么 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.cpp
和 CSerialPort_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 的基于策略的设计,尽管最终目标可能不同。