你想要一个香蕉,但得到的却是一个大猩猩拿着香蕉,而其还有整个丛林。” — Joe Armstrong(Erlang语言发明人)
架构原则:
- 代码封装:用类——类是源码的抽象原型
- 各司其职:数据单一职责严谨封装,行为单一职责及时销毁
- 结构依赖:扁平和垂直的继承依赖关系(包括配置文件的位置);上游端口和下游端口抽象暴露
- 任务轮询:状态变量;线程消息、协程事件、数据驱动
- 测试激励:
- 语法风格:主谓宾的管道语法
命名、排版、文档
编程技巧:资源的独立与共享
#单一职责原则 #接口隔离原则 #依赖倒置原则 #资源获取即初始化 #原子性 #面向切面
(48 封私信 / 80 条消息) 什么是面向切面编程? - 知乎
单一职责原则的思维:为什么你的代码总在“牵一发而动全身” - AI·NET极客圈 - 博客园
(57 封私信 / 80 条消息) 接口隔离原则(Interface Segregation Principle) - 知乎
集成电路,物理上是分隔的,不需要担心另一个元件的信号影响这一个元件的运行。
程序代码,前人栽树,后人乘凉,改代码时容易造成“牵一发而动全身”。
两个解决办法:
-
时间上用作用域划分不同步骤下变量的生命周期,用完的变量及时销毁,所以简便地,可以把不同代码块用大括号括起来,括号结束对象自动销毁,python则可以定义局部函数
-
空间上把不同的功能重新封装成不同的函数、结构体乃至类,不同步骤下完全互不干扰(具体实现上存在继承和嵌套两种做法)。
-
前者对应行为的职责单一。比如甲和乙洗澡的习惯不一样,甲先抹洗发露,乙先洗脸顺便把牙刷了;但是他们肯定都是脱了衣服进去,穿上衣服出来。
-
后者对应数据的职责单一。比如Robot类,包含关节定位、视觉定位、IMU辅助定位、执行器电机。视觉定位让关节位置信息从局部到全局,IMU辅助定位让视觉位置信息在无视觉时也能粗略更新。执行器,有直接PID控制法,有线性插补法,切面之间的核心算法可以彼此替换,最后的执行器类继承了所有特性。
独立而完备的原子性代码。真正原子性的不可分割的代码往往只是三四行聚集的“代码颗粒”。所以我们在描述类的时候,要怀有一点“这个类未来可能成为基类”,“这个类未来可能要被拆分”的意识,让后人维护代码更加轻松。
这样你就实现了:
- 开闭原则。对内不需要多少修改,对外可以很好的扩展。这不强求必须在类的外面扩展(继承、装饰器……),你当然可以修改类里面的方法。想要修改的时候,直接定位到对应的方法,在原型之外添加一点点代码就能够用了。
- 接口隔离原则。例如一个机器人有摄像头和激光雷达,MyRobot类就可以写成CameraRobot和LidarRobot两个类的继承(组合)。激光雷达SLAM相关的函数只需要对LidarRobot进行操作,视觉SLAM相关的函数只需要对CameraRobot进行操作。
- 里氏替换原则。如果把MyRobot放进激光雷达SLAM和视觉SLAM的函数,不应该导致函数的行为变化。至于MyRobot可能还有AI对话等功能,那是MyRobot自己实现的扩展。
所谓“切面”,可以理解为代码块之间耦合度较低的部分。当很多代码都在类似的位置低耦合,就形成了一个切面。
在面向对象的表达方式,
“洗澡”可以包装成一个类,或者最起码是个函数。
“进去洗澡”和“洗完出来”就是耦合度较低的环节
在面向切面的表达方式,
“洗澡”是核心关注点
“进去洗澡”和“洗完出来”是横切关注点
面向切面,就是要我们关注对象与对象之间定义的边界,这个边界越平整越便于维护。
切面,是为了我们“望闻问切”。在切面中看到数据流通的“血管”,也就可以把变量印出来打印“抽血”(在计算机语言随时printf可是可以,但是难以定位和统一控制;在verilog中暴露变量更加困难,面向切面更加有用了)
对于依赖中的“切面”,用抽象接口分隔出上下游的依赖,这就是依赖倒置原则。
编程技巧:状态模型示例
一个任务的状态应该分四个象限
| CPU在,RAM在 | |
|---|---|
| CPU不在,RAM在 | CPU不在,RAM不在 |
| “CPU在,RAM不在”显然是不能成立的(无法正常工作) | |
| 所以任务分为了 |
graph TD
A[运行:CPU在,RAM在] --暂停--> B[暂存:CPU不在,RAM在]
A --退出--> C[空闲:CPU不在,RAM不在]
B --继续--> A
C --进入--> A进一步还可以有
graph LR B[暂存:CPU不在,RAM在] --回收--> C[空闲:CPU不在,RAM不在] C --预备--> B
面对多任务,还可以把暂存的原因分为
- 阻塞:任务正在等待某个时序或外部中断,不在就绪列表中。(FreeRTOS的延时也算在里面了,因为不考虑手动延时)
- 待命:任务已经具备执行能力,等待调度器进行调度。(FreeRTOS的挂起也算在里面了,因为不考虑手动挂起)
编程技巧:C语言实现“面向对象”
基于结构体:虽然C的函数是静态的,无法实例化,但是结构体可以实例化。
基于指针:对象的方法、对象的父类、对象的子类,本质上都可以用指针表示,因为指针就是最灵活的组合。
开工!
在结构体中定义指针指向基类
在函数中传入指针指向对象
部分函数作为工厂方法也就是构造函数返回结构体指针。
这样就实现面向对象了。HAL库也是这样实现面向对象的。下面是一个比较有名的库
lw_oopc(C语言的面向对象) - robert_cai - 博客园
Akagi201/lw_oopc: Light Weight Object Oriented C macros
关于-LW_OOPC学习01_金永华 csdn-CSDN博客
C语言唯一做不到的,是省略对象自身的参数传入(省略self或者this的能力)
有了这些定义,用起来就跟简单c++的类一样了,只是定义一个类的实例时应采用如下一种格式:
Dog* dog = Dog_new();
dog->Init(dog);
调用自身的方法:
Bird* bird = Bird_new();
bird->Init(bird);
bird->Fly(bird);
调用继承的方法稍显麻烦:
((Animal*)bird)->Eat((Animal*)bird);
或者当EXTENDS语句在定义类时并不在第一句(下一篇将揭露各个宏的真实面目)时,最稳妥也显得繁琐一点的方式为:
SUPER_PTR(bird, Animal)->Eat(SUPER_PTR(bird, Animal));