软件构造学习笔记(4)

哈工大软件构造课程学习笔记


前言

本文主要包含三部分,对应哈工大软件构造课程第8、9、10章。包括ADT和OOP中的“等价性”、面向复用的软件构造技术、面向可维护性的构造技术。


一、ADT和OOP中的“等价性”

在Java中,判断相等有两种形式,分别是== 和 equals():

==:意为引用等价性,即判断比较的二者是否内存地址是否相等。

equals():意为对象等价性,即判断两个对象是否相等。

需要注意,默认情况,也就是 equals()的内部实现与==,等价,也就是说只有两个对象处于相同地址空间时才会返回true。

但是在更多情况下,我们定义的ADT做等价性比较时,只需要两个ADT内容相等即可,该怎么办呢?答案是要进行重写

这里将ADT分为两类,可变与不可变类型来对equals进行讨论:

对于不可变类型,由于不可变类型往往要进行临时拷贝,导致更改前后的对象处于不同的地址,这时使用equals()几乎都会返回false。所以我们要将euqlas()重写:

注意:

1.equals重写的是Objects类的方法,而不是自己类型的,如果重写的参数是本身类型,就属于重载,此时类中会存在两个equals方法,继承自Objects类的equals方法隐式存在,这样会造成错误。

2.在重写euqals()会常用 instanceof关键字和getclass()方法,作用分别是判断某个对象是不是特定类型(或其子类型) 和 返回对象的类型其中instanceof 动态类型检查,不是静态类型检查,需要注意的是,除了重写equals()之外,尽可能避免使用二者。

3.除了equals()方法之外,经常与之绑定的一个方法是hashcode()方法,hashcode()为对象在哈希表中产生一个哈希值,方便索引,哈希表的知识这里不是讲解重点。这就带来了一个问题,如果重写了equals()而没有重写hashcode(),会导致两个等价的对象的hashcode不同,会出现使用一个与对象等价的对象去索引对象的结果是找不到。所以要求我们只要重写equals()就要重写hashcode()方法。至于重写hashcode()的方式有很多,可以将所有对象hashcode都设置为同一个,但会导致性能下降;还是要根据对象的属性来编写。

对于可变类型,有两种判断等价关系的方式:1.观察等价性(在不改变状态的情况下,两个对象看起来一致)2.行为等价性(调用任何对象的方法都展示出一致的结果)

虽然Java中普遍对与可变类型采用观察等价性的方式(除了如StringBulider等),但是这种方式可能会导致bug,例子如下:

List<String> list = new ArrayList<>(); list.add("a");
Set<List<String>> set = new HashSet<List<String>>(); set.add(list);

此时的快照图如图:

但是如果我们修改这个存入的列表:

list.add("goodbye");

 会出现这种情况:

set.contains(list) → false!

原因是对象的hashcode变了,但是HashSet没有更新其在bucket的位置,查找时在新hashcode的位置找不到元素,hash table的RI遭到了破坏。

说明如果某个可变的对象包含在Set集合类中,当其放生改变时,集合类的行为会变得不确定。

总结,不可变类型必须重写equals()和hashcode();可变类型不用重写;对象之间比较等价性都要使用equals()(包括如Integer等的包装类型)。 
 

二、面向复用的软件构造技术

1.复用

基于复用编程就是利用已有的可复用软件搭建应用系统,实际上软件构造过程中的任何实体都可能被复用:代码层面上,可以复用方法属性等;模型层面,可以复用类或接口;库层面可以复用各种内库;结构层面上可以复用框架。

可分为, 白盒复用:源代码可见,可修改和扩展;黑盒复用:源代码不可见,不能修改

在系统级复用也就是架构复用时,如果想要拓展框架,也要分两种考虑:

白盒框架,通过代码层面的继承进行框架扩展

黑盒框架,通过实现特定接口/delegation进行框架扩展

2.LSP

LSP即 Liskov 替换原则,包含以下几条:

1. 子类型可以增加方法,但不可删

2.子类型需要实现抽象类型 (接口、抽象类)中所有未实现的方法 

3.子类型中重写的方法必须有相同或子类型的返回值或者符合co-variant(协边)的参数

4.子类型中重写的方法必须使用同样类型的参数或者符合contra-variant(反协变)的参数(此种情况Java目前按照重载overload处理)  

5.子类型中重写的方法不能抛出额外的异常,抛出的异常也要协变

总结而言,LSP要求子类对象想要替换父类时,子类必须满足1.前置条件不能强化2.后置条件不能弱化3.不变量要保持4.子类型方法参数:逆变5.子类型方法的返回值:协变6.异常类型:协变

注意:Java中数组支持协变, 泛型是类型不变的。

对于泛型

1.泛型信息只存在于编译阶段,在运行时会被”擦除”,无限定被设定为Objects,有限定,用第1个,getClass()只适用于原始类型

2.泛型想要实现协变需要使用通配符,通配符包含无限定通配符?,逻辑上时所有泛型的父类; <? super A> 下限通配符,逻辑上可以匹配所有A类及其父类; <? extends A> 上限通配符,逻辑上可以匹配所有A及A的子类。

3.委托和比较

比较器

Java中有两种比较器接口,分别是Comparator<T>和Comparable<T>,两者都可以在排序中使用,包含了排序算法。

相同点:都要重写各自的方法(compare()和compareTo())

区别:对于Comparator<T>,实现它的是具体的符合要求的新的比较类,使用时要先实例一个比较类,才能使用;对于Comparable<T>,实现它的是要被比较的类,并且使用时直接使用Collections.sort(对象)即可。

委托

委派/委托意为一个对象请求另一个对象的功能,是复用的一种常见形式

如果子类只需要复用父类中的一小部分方法 ,可以不需要使用继承,而是通过委派机制来实现,从而避免继承大量无用的方法

CRP原则:组合优先于继承,注:组合是委派的一种形式。

还需要注意的是,“委托”发生在object层面,而“继承”发生在class层面。

委托有四种分类:

1.Dependency: 临时性的delegation,通过方法的参数或者在方法的局部中使用发生
联系,被委托的类是在类外定义的,类内无这个被委托类的引用属性

2.Association: 永久性的delegation,类内会有一个被委托的类的对象,该对象的引用可以更改为外部对象

3.Composition(组合): 更强的association,但难以变化,类内会有一个被委托的类的对象,该对象的引用不可以改变

4.Aggregation:  更弱的association,可动态变化,类内会有一个被委托的类的对象的引用,但是实例是在外部创建的,并且可以控制引用指向

Dependency (A use one or multiple B)

Association (A has one or multiple B) 

Composition/aggregation (A owns one or multiple B) 

三、面向可维护性的构造技术

1.面向可维护性的构造技术

评价可维护性的指标,可以使用圈复杂度、代码行数、继承的层次数、类之间的耦合度、单元测试的覆盖度等,简单来讲就是代码的复杂程度。

可维护性的5个指标:

1.可分解性:将问题分解为各个可独立解决的子问题

2.可组合性:可容易的将模块组合起来形成新的系统

3.可理解性:每个子模块都可被系统设计者容易的理解

4.可持续性:规格说明小的变化将只影响一小部分模块,而不会影响整个体系结构

5.出现异常之后的保护 :运行时的不正常将局限于小范围模块内

面向可维护性编程的5个规则:

1.直接映射:模块的结构与现实世界中问题领域的结构保持一致

2.尽可能少的接口:模块应尽可能少的与其他模块通讯

3.尽可能小的接口:如果两个模块通讯,那么它们应交换尽可能少的信息

4.显式接口:当A与B通讯时,应明显的发 生在A与B的接口之间

5.信息隐藏:经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面

2.面向对象编程的准则:SOLID

▪ (SRP)  The Single Responsibility Principle  单一责任原则:ADT中不应该有多于1个原因让其发生变化,否则就拆分开

▪ (OCP) The Open-Closed Principle 开放-封闭原则:模块的 行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化,但模块自身的代码是不应被修改的

▪ (LSP)  The Liskov Substitution Principle  Liskov替换原则:子类型必须能够替换其基类型,派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异

▪ (ISP)   The Interface Segregation Principle  接口聚合原则:不能强迫客户端依赖于它们
不需要的接口:只提供必需的接口,不应该使接口太“胖”

▪ (DIP)  The Dependency Inversion Principle  依赖转置原则:高层模块不应该依赖于低层
模块,二者都应该依赖于抽象,抽象不应该依赖于实现细节,实现细节应该依赖于抽象  

该部分还涉及了正则表达式的相关学习,这里建议参考链接https://www.runoob.com/java/java-regular-expressions.html


总结

本文详细介绍了三个重要部分:ADT和OOP中的“等价性”、面向复用的软件构造技术、面向可维护性的构造技术,对于提高软件构造质量有重要作用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值