Development Dairy

开发日记

项目管理

  • 确定需求时直接判断是否大量复用,拆分成公用组件。
  • App版本号规范,MAJOR.MINOR.PATCH。

    版本迭代更新MINOR,服务器也至少通过MINOR判断对应服务器。PATCH更新不需要对应服务器。ios app每次提审后都需要更新PATCH版本号,因此如果使用PATCH对应服务器版本则提审后再次更新都需要更新对应服务器。

目录结构

定义类型声明

  • 通用类型声明最好放一块, 便于查找, 避免定义过多重复类型。
  • 单独类型放入各自目录下, 避免于通用类型声明混淆。
1
2
3
4
5
6
7
scripts/
├── interfaces/ -->接口
│ ├── IUser.ts
│ └── IReward.ts
├── activities/activity/ -->某单独活动
│ └── interfaces/
│ └── IActivity.ts

代码大全

软件构建中的设计

代码设计基本特性

  • 最小复杂度
  • 易于维护
  • 松散耦合
  • 可扩展性
  • 可重用性
  • 高扇入 (大量的类使用给定的类, 例如工具类)
  • 低扇出 (一个类适中地使用其他类)
  • 可移植性
  • 精简性
  • 层次性
  • 标准技术

规范子系统通信规则

设计原则

  • 信息隐藏 “我该隐藏什么?”;
  • 为测试而设计。如何便于测试, 各个系统能独立检查。

配置 Eslint

文件目录规范

避免语义耦合

  • Module2 在 Module1 修改全局数据后使用这个数据。需要确保数据在 M2 中是否可用, 并且这个参数在 M1 中被正常调用过。

    使用事件驱动或者状态管理。

  • M1 在调用 func()时会先调用 init(); M2 在使用 M1.func()时知道会提前调用 M1.init();

    1. M1 初始化时直接调用。
    2. func()中判断 initProperty 参数状态自动调用 init();
    3. func()中添加断言, 判断 init 是否被调用。
  • M1 传对象 obj 给 M2, 知道 M2 只需要部分 obj 的参数, 所以只初始化了 M2 需要的几个参数;

    对象接口抽象

  • M1 传类型 BaseObject 对象给 M2, M2 知道实际传的是 DerivedObject 类型, 所以把对象强制转换成了 DerivedObject 类型;

    直接传 DerivedObject 类型;

熟练掌握常用设计模式

经常比对需求判断其中的逻辑是否有合适的设计模式。

常用设计模式:

  • Abstract Factory 抽象工厂
  • Adapter 适配器
  • Bridge 桥接
  • Composite 组合
  • Decrorator 装饰器
  • Facade 外观
  • Factory Metho
  • Iterator 选代器
  • Observer 观察者
  • Singleton 单件
  • Strategy 策略
  • Template Method 模板方法

记录设计结果:

  • 设计文档插入到代码中

    代码开头写明关键设计决策。类似 JavaDoc 的工具, 直接提取注释生成 api 文档。

  • wiki 记录

    更全面, 更多文字格式, 可以贴图, 链接等。


开发程序任意一部分代码时, 能够安全地忽视程序中尽可能多的其余部分。类是实现这一目标的首要工具。

ADT (abstract data type)

抽象数据类型, 指一些数据以及对这些数据所进行的操作的集合。

抽象

如果阅读注释(文档)后仍然不知道接口的作用, 那说明接口抽象不够或者内聚性不强。

  • 目标一致, 功能与类紧密结合。
  • 抽象层次一致。一个类中实现单一 ADT(感觉不是很绝对)
  • 同时考虑抽象性和内聚性。

封装

  • 类的接口与类的实现隔离 PImpl(Pointer to Implementation)

    • 减少编译依赖, 只用重新编译 MyClass.cpp。否则所有引用 MyClass.h 的 cpp 都要重新编译。
    • 高封装, 高可维护。
    • ABI 兼容性(暂时接触不到)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // MyClass.h
    class MyClassImpl; // 只声明, 不引入具体实现
    class MyClass {
    private:
    MyClassImpl* pImpl; // 指向实现的指针
    public:
    MyClass();
    ~MyClass();
    void doSomething();
    };

    // MyClass.cpp
    void MyClass::doSomething() {
    pImpl->doSomething(); // 委托实现
    }

类的设计

太抽象的也想不明白, 只能说要先多想。想了之后画画之类的再实现。实现后觉得有问题多重构。实现太多后不好重构了再去想就晚了, 想不明白了。晚了之后也要总结点经验。

  • 接口是规范, 规定有什么。类是模版, 有什么, 可以做什么。
  • 7±2 法则。一个类超过约 7 个数据成员时考虑是否需要拆分。
  • Liskov 替换原则(LSP)。子类必须能够完全替换父类, 而不会影响程序正确性。必须语义完全相同才可以继承。派生类“是一个”特殊的“基类”。
  • 继承不得超过 2 层

    推荐是 2-3 层, 按理说超过两层复杂度已经很高了, 考虑通过不同抽象的方式减少层数。

    • 组合。继承是“是一个”, 组合是“有一个”。通过包含其他类的实例构建复杂功能。
    • 策略模式。是“有很多个”, 可以动态切换的时候使用策略模式。例如不同登录方法。

方法

命名

  • 描述所做的所有事情, 如果太长则可能需要拆分方法。
  • 含义清晰, 不要用 Handle, deal 等词。
  • 长度最多 15 个字符。
  • 命名时对返回值有所描述, 能看出返回结果。
  • 准确使用对仗词, 一组名词命名规范要相同。
  • 确定命名规范。

规范

  • 一个方法尽量不超过 200 行。
  • 参数 7 个以内

防御编程

  • 垃圾进, 什么都不出。
  • 垃圾进, 错误提示出。
  • 不许垃圾进。

断言

处理不应该发生的情况, 例如某参数为空。

错误

处理有可能出现错误的情况, 抛出异常并处理保证程序正常进行, 定位问题输出日志帮助解决问题。

debug 开发

  • 使用辅助调试代码, 方便开发。使用测试代码, 测试数据, 方便调用快速测试逻辑。需要判断环境避免在生产环境中使用。使用测试用例, 确保逻辑没有问题。
  • 进攻式编程, 考虑极端情况。

变量

Typescript

  • 变量直接初始化, 声明变量类型。

    基础类型直接使用默认值, 0、”” 等;对象使用 null;数组使用[]; 判断时使用强等, 避免隐式转换。

命名

  • 规范命名规则。
    • 别拼错。
    • 类名大写开头, 对象小写开头。(Unity 都是大写, 局部变量是小写)
    • 接口: IInterface 与类做区分。
    • 枚举: XXXType, XXXStatus。不单独使用, 直接调用即可, 不需要加前缀。
    • 全局变量全部大写 GLOBAL_XXXX。

规范

  • 减少无意义数字

    1
    2
    const duration = 0.2;
    cc.tween().to(duration,{});
  • bool 值不要再判断

    1
    2
    let a = true;
    a === true ???
  • 使用枚举

    1
    2
    3
    4
    5
    6
    7
    8
    enum Type{
    a = 1,
    b = 2,
    }
    if(n === Type.a){
    //
    }

  • 过度声明

    bool 值可以考虑使用数字类型, 后续开发有可能状态会变为多个的情况。

  • 禁止使用 var, 使用 const 和 let。

    eslint 配置自动将未重新赋值 let 改为 const。还能确保变量一定初始化。就是后续要再修改的时候还得回去改成 let 确实有点烦了。

软件质量

  • 代码特性之间的影响
    avatar

  • 代码缺陷检出

    代码检查比测试成本低效果好,要养成阅读代码的习惯,自检设计,自检代码。

    • 非正式设计复查 (小团队)
    • 正式设计检查 (强制流程审查)
    • 非正式代码复查 (code review)
    • 正式代码检查 (关键模块深度检查)
    • 建立模型或原型 (验证技术方案可行性)
    • 个人桌面代码检查 (自查)
    • 单元测试
    • 集成测试
    • 回归测试

开发者测试

  • 测试不会改善软件质量,改善软件质量需要的是更高质量的开发。
  • 测试目的是找到错误而不是完成流程。
  • 测试方法

    • 测试用例
    • 设计关注点测试
    • 基础测试+数据流测试
    • 检查表,记录所有犯错的类型
  • 测试注意事项

    • 尽可能多的满足测试覆盖率。(尽量保证每行代码都被执行了)
    • dirty tests 应该是 clean tests 的 25 倍。(只能说尽可能的多吧,测试用例都不一定能写 25 个)
      • clean tests: 预期的输入输出。 功能稳定后实现,自动化测试用例确保功能稳定运行。
      • dirty tests: 极端,错误数据验证系统处理能力。能够快速试错
    • 边界值分析
  • 结构化基础测试

    测试用例最少数量计算方法,遇到 if while for 等关键字计数+1. 感觉没必要这么多,视代码重要程度而定。算上个系数,一般游戏开发 N _ 0.1, 关键逻辑 N _ 0.5, 代码质量要求极高再直接 N 吧。

Debugger

科学调试

  1. 确定错误状态,触发错误的具体条件,尽量稳定复现,锁定错误范围。
  2. 收集错误相关数据
  3. 根据相关数据构造导致错误问题的假说
  4. 分析如何证明假说
  5. 实现假说证明是否能够修补,不能则重回 *3.
  6. 测试
  7. 判断是否还有类似错误

    比较关键,改 bug 谁都会改,能多想多思考发现更多问题才是更好的开发。反思问题出现的原因,优化代码逻辑代码结构。

  • 确定错误后可以添加单元测试避免再次出现相同或者类似的错误。
  • 实在找不到可以休息一下,说不定啥时候来灵感了。

修改缺陷

  • 修改代码前要确保理解问题和相关程序。
  • 修改后一定要测试,不管多小的改动。
  • 治本,理解错误最根本的,不要治标不治本

重构

需要代码重构的情况

  • 代码重复

    编写过程中就要避免,如果有重复代码就要抽象出来避免重复,哪怕只有两段重复)

  • 冗长的子程序

    编写过程中就要避免

    • 循环过长或嵌套过深