Design Concept
C++ 运行的载体
在 CPU 上和 GPU 上跑代码是不一样的。
在 Nvidia 的 GPU 上跑程序:CUDA(C-like)
异构 C++:程序员不需要考虑代码到底是在 CPU 上跑还是在 GPU 上跑,由编译器转化成对应指令。
AMD 和 Intel 有各自的异构 C++ 编译器(HCC、DPC++),各自收购了一家 FPGA 企业(Xilinx、Ultra)。
Lambda:将一段代码打包成数据,传递给某个计算队列(CPU / GPU / FPGA),根据队列构造出相应指令。
C++ 的场景:嵌入式(Embedded)和 高性能(HPC)
AI 编译:将一段代码(pytorch)理解形成计算图,优化后转化成 GPU 执行的语言,后端 LLVM。
代码设计
设计内容:类的声明、类的函数、类外的函数
问题:研究类的写法,使其有良好的可读性、可维护性、可重用性(understandable, maintainable, reusable)。
所有设计的目标:使未来的更新尽可能简单
软件的开发不是一蹴而就的,强大的软件要经历持续和多层的更新。
可能的更新:性能功能扩展、数据库迁移、移植、适配
现状:杭州的小公司,一个程序员的平均停留时间为 $2$ 年,一个产品平均每 $3$ 年替换一轮所有的开发人员。
责任驱动设计 Responsibility-Driven Design
对于数据而言,处理数据和存放数据的单位应该是同一个。
也即,一个类应该负责控制它存放的所有数据,同时不负责处理任何其他类的数据;
这样在功能更新、出错调试时更容易排查问题、解决问题。
责任驱动设计有助于实现松耦合。
耦合 Coupling
类之间的关联程度。
如果两个类的在实现中紧密联系,那么称为紧耦合。但两个类的联系不可能完全没有,所以我们想要松耦合。
越会被重用的类,它的耦合就应该越松。
效果
对松耦合的类,我们能不阅读其他内容而理解这个类的功能,在尽可能不影响其他类的情况下修改这个类的内容。
解耦合的方法
假设有一个 Button
和一个 Actor
,当 Button
被按下时 Actor
跳舞。
在代码层面,Button::onpressed()
时应该运行 Actor::dance()
。
如果在 Button
内部维护一个 Actor
的指针,同时在 Button::onpressed()
中添加 Actor::dance()
相关内容,这样 Button
编译必须依赖于 Actor
,耦合太紧。
注入反转 IoC
常用的方式,尤其是在相对紧密的模块内。
在 Button
中维护一个 listener
指针,通过 addlistener()
修改;
在 listener
中维护一个 action()
,在 Button::onpressed()
时调用;
Actor
继承一下 listener
,将 listener::action()
接入 Actor::dance()
。
class listener{
public:
virtual void action() = 0;
};
class Button{
public:
void addlistener(const listener *p){ listenptr = p;}
void onpressed(){ listenptr->action();}
private:
listener *listenptr;
};
class Actor: public listener{
public:
Actor(Button *buttonptr){ buttonptr->addlistener(this);}
void action(){ dance();}
private:
void dance();
};
这样 Button
只依赖于 listener
,而不同的类继承 listener
后可以方便的实现自己的 action()
。
现在 Actor
依赖于 Button
。
消息机制
上一个方案仍然存在依赖关系,尽管依赖关系比较符合逻辑。
在消息机制中,类之间并不认识,但都注册在一个中控系统中。
实现一
每个类有一个唯一的标识(字符串),A 对 B 操作通过向中控发送消息(字符串指令)完成。
这种情况下,中控并不知道信息对应的操作(函数)。
实现二
中控类有许多成员函数,分别指向不同类的成员函数。某个类需要交互的时候,直接调用中控的某个函数;中控的成员函数被调用后,会转接调用对应注册类的函数。功能的替换可以通过继承来实现。
这种情况下,中控需要调用具体的操作(函数)。由于减少了字符串传递和匹配的开销,这种方案的性能更好。
内聚 Cohesion
定义:一个单元(类、函数)负责任务的数量和种类。数量越少、范围越精确,内聚就越高。
效果
高内聚可以方便我们理解类的用途、以及重用类。
策略
一个函数只应该完成一个任务,一个类只应该代表一个东西。
重构 Refactoring
问题
代码在维护时,通常代码会越来越长。
如果一直下去,代码往往会因为零碎的更新而降低了内聚、提升了耦合。
重构的收益是延时的,对当下影响可能不大,但对未来的开发有益。
所以我们需要适时重构代码。
功能越单一的代码,修改的可能性也更小。
回归测试
在使用重构的代码前,先要用重构前的代码测试其功能是否一致。
可维护性 与 可扩展性:可扩展性指不经修改就能增加新的功能;可维护性指经少量修改就能增加新的功能。
总结
- 项目内容是持续更新的。
- 我们需要让更新尽可能简单。
- 好的代码应该有高可读性、可维护性、可重用性
- 代码的质量不仅仅要求把功能实现,更要求松耦合、高内聚,避免代码重复。
- 代码的质量将直接影响更新内容时的工作量。