2020春季学期哈工大软件构造学习心得四

探讨程序设计中函数、方法的规约重要性,强调规约在代码正确性、可读性和维护性中的作用。深入解析抽象数据类型(ADT)设计原则,包括表示独立性、抽象函数(AF)、表示不变量(RI)等关键概念,以及如何通过ADT避免表示泄露,确保程序的正确性和高效性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

程序设计语言中的函数和方法

参数类型是否匹配,在静态类型检查阶段完成
返回值类型是否匹配,也在静态类型检查阶段完成

规约:交流编程

Spec给“供需双方”都确定了责任,在调用的时候双方都要遵守;没规约,没法写程序;即使写出来,也不知道对错
很多bug来自于双方之间的误解,不写下来,那么不同开发者的理解就可能不同 没有规约,就难以定位错误。客户端无需阅读调用函数的代码,只需理解spec即可
规约可以隔离“变化”,无需通知客户端;规约也可以提高代码效率;规约:扮演“防火墙”角色
对象与其用户之间的协议:输入/输出的数据类型;功能和正确性;性能

静态类型声明是一种规约,可据此进行静态类型检查static checking。 方法前的注释也是一种规约,但需人工判定其是否满足(前置条件和后置条件在@param和@return说明完是什么后,再补充这后面的东西应该满足的条件)
除非在后置条件里声明过,否则方法内部不应该改变输入参数。尽量避免使用mutable的对象

行为等价性

站在客户端视角看行为等价性(根据规约判断是否行为等价)
1.单纯的看实现代码,并不足以判定不同的 implmentation是否是 “行为等价的”
2.需要根据代码的spec(开发者与client之间 形成的contract)判定行为等价性
3.在编写代码之前,需要弄清楚spec如何协商形成、如何撰写

前置条件:对客户端的约束,在使用方法时必须满足的条件
后置条件:对开发者的约束,方法结束时必须满足的条件
契约:如果前置条件满足了,后置条件必须满足
(前置条件不满足,则方法可做任何事情,任何结果都有可能出现)

程序中可 能有很多变量指向同一个可变对象(别名)。无法强迫类的实现体和客户端不保存可变变量的“别名”。避免使用可变的全局变量!

设计规约

如果对于两个规约S1,S2,S2相对于S1前置条件更弱,而且后置条件更强 ,则规约的强度S2>=S1,就可以用S2替代S1
越强的规约,意味着implementor的自由度和责任越重,而client的 责任越轻。

确定的规约:给定一个满足precondition的输入,其输出是唯一的、明确的
欠定的规约(Under-deterministic):同一个输入可以有多个输出
非确定的规约(Nondeterministic):同一个输入, 多次执行时得到的输出可能不同

操作式规约,例如:伪代码
声明式规约:没有内部实现的描述,放在代码实现体内部注释里呈现,只有 “初-终”状态
声明式规约更有价值

一个好的“方法”设计,并不是你的代码写的多么好,而是你对该方 法的spec设计得如何。一方面:client用着舒服;另一方面:开发者编着舒服
Spec描述的功能应单一、简单、易理解(内聚);不能让客户端产生理解的歧义(信息丰富);
应尽可能考虑各种特殊情况,在post-condition给出处理措施(健壮);
太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度(client当然非常高兴)
在规约里使用抽象类型,可 以给方法的实现体与客户端更大的自由度
不写Precondition,就要在代 码内部check;若代价太大, 在规约里加入precondition, 把责任交给client

是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围
如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check——责任交给内部client;
如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端 不满足则方法抛出异常。

抽象数据类型 (ADT)

抽象数据类型与表示独立性:如何设计良好的抽象数据结构,通过封装来避免客户端获取数据的内部表示(即“表示泄露”),避免潜在的bug——在client和implementer之间建立“防火墙”

ADT的特性:表示泄漏、抽象函数AF、表示不变量RI
基于数学的形式对ADT的这些核心特征进行描述并应用于设计中。
ADT是由操作定义的,与其内部 如何实现无关

数据抽象:由一组操作所刻画的数据类型
传统的类型定义:关注数据的具体表示
抽象类型:强调“作用于数据上的操作”,程序员和 client无需关心数据如何具体存储的,只需设计/使用操作即可。

可变类型的对象:提供了可改变其 内部数据的值的操作
不变数据类型: 其操作不改变内部值,而是构造新的对象

对抽象类型的操作进行分类
构造器,创建类型为的新对象
生产器,从类型的旧对象创建新对象
观察器,获取抽象类型的对象并返回不同类型的对象
变值器,改变对象属性的方法
在这里插入图片描述
构造器:可能实现为构造函数或静态函数
变值器:通常返回 void(如果返回值为void,则必然意 味着它改变了对象的某些内部状态
),也可能返回非空类型

设计ADT

1.设计简洁、一致的操作
2.要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低
3.要么抽象、要么具体,不要混合 — 要么针对抽象设计,要么针对具体应用的设计

表示独立性:client使用ADT时无需考虑其内部如何实 现,ADT内部表示的变化不应影响外部spec和客户端
除非ADT的操作指明了具体的pre和post-condition,否则不能改变ADT的内部表示——spec规定了 client和implementer之间的契约。

(1) 选择R和A;
(2) RI — 合法的表示值;
(3) 如何解释合法的表示值 —映射AF
做出具体的解释:每个rep value如何映射到abstract value

测试ADT

测试creators, producers, and mutators:调用observers来观察这些operations的结果是否满足spec
测试observers:调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。

保持不变量(不变量:在任何时候总是true,如,immutability就是一个典型的“不变量” ),由ADT来负责其不变量,与client端的任何行为无关
为什么需要不变量:保持程序的“正确性”,容易发现错误,如果没有这个不变性, 那么在所有使用String的地方,都要检查其是否改变了

抽象函数:R和A之间映射关系的函数,即如何去解释R中的每一个值为A中的每一个值。 AF : R → A
表示不变性RI:某个具体的“表示”是否是“合法的”
也可将RI看作:所有表示值的一个子集,包含了所有合法的表示值
也可将RI看作:一个条件,描述了什么是“合法”的表示值

选择某种特定的表示方式R,进而指定某个子集是“合 法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射 到抽象空间中的值。
即使是同样的R、同样的RI,也 可能有不同的AF,即“解释不同”。

在所有可能改变rep的方法内都要检查checkRep() ;Observer方法可以不用,但 建议也要检查,以防止你的“万一”

对immutable的ADT来说,它在A空间的abstract value应是不变的。
但其内部表示的R空间中的取值则可以是变化的。
这种mutation只是改变了R值,并未改变A值,对client来说是 immutable的
“AF并非单射”,从一个R值变成了另一个R值
但这并不代表在immutable的类中就可以随意出现mutator!

通过牺牲immutability的部分原则来换取“效率”和“性能”

记录AF、RI和Rep Exposure的安全性

private and final,防御性拷贝
unmodifiable包装,返回值是String、void等不可变类型

精确的记录RI:rep中的所有fields何为有效
精确记录AF:如何解释每一个R值

ADT的规约里只能使用client可见的内容来撰写,包括参数、返回值、异常等。
如果规约里需要提及“值”,只能使用A空间中的“值”。
ADT的规约里也不应谈及任何内部表示 的细节,以及R空间中的任何值,ADT的内部表示(私有属性)对外部都应严格不可见
故在代码中以注释的形式写出AF和RI而不能在Javadoc文档中,防止被外部看到而破坏表示独立性/信息隐藏

构造器和生产器在创建对象时要确保不变量为true ,变值器和观察器在执 行时必须保持不变性。 所以就可以在每个 方法return之前,用checkRep()检查不变量是否得以保持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值