该实验面向可复用性进行设计,需要学习掌握一定的设计模式针对不同的应用进行设计。而我本人选择的三个应用:StellarSystem(太阳系恒星模型)、AtomStructure(原子结构模型)、SocialNetworkCircle(社交网络)。
可复用啊!!! L,E。
不同对象的面向复用的设计(Reusability)
CircularOrbit<L,E>
Mutable的类型。
设计新的接口CircularOrbit<L,E>,其中L和E分别代表多轨道系统的中心点物体和轨道物体。可有的操作:
- 创建一个空的 CircularOrbit 对象
- 增加一条轨道、去除一条轨道
- 增加中心点物体
- 向特定轨道上增加一个物体(不考虑物理位置)
- 增加中心点物体和一个轨道物体之间的关系
- 增加两个轨道物体之间的关系
- 从外部文件读取数据构造轨道系统对象
Track
Immutable的ADT。
需要设计rep:1.半径
L(CircularOrbit<L,E>中的L)
所设计的表征中心点物体的类。
- StellarSystem:中心点物体为恒星
- AtomStructure:中心点物体为原子核
- SocialNetworkCircle:中心点物体为人
PhysicalObject
Immutable的ADT。
实现 ConcreteCircularOrbit<L,E>中的 E,即代表分布在不同轨道上的物 体类 PhysicalObject。可以是接口,也可以是抽象类。 考虑到不同应用中包含不同类型的轨道物体,需从 PhysicalObject 派生子 类型,通过 override 实现 PhysicalObject 中的各个接口方法或抽象方法,也可 根据应用需求来增加子类的特有属性和方法。
- StellarSystem:轨道物体为行星
- AtomStructure:轨道物体为电子
- SocialNetworkCircle:轨道物体为人
可复用的API设计
API-Application Programming Interface. 何为API→点此了解。
- 计算多轨道系统中各轨道上物体分布的熵值。
double getObjectDistributionEntropy(CircularOrbit c)
- 计算任意两个物体之间的最短逻辑距离。
int getLogicalDistance (CircularOrbit c, E e1, E e2)
若两物体无联系,距离无穷大。(Lab1中P3已实现该功能) - 计算任意两个物体之间的物理距离。(若有具体位置,可在直角坐标系中计算)
double getPhysicalDistance (CircularOrbit c, E e1, E e2)
- //TODO 计算两个多轨道系统之间的差异
第三方API复用
实现可视化
设计模式应用
(1) 构造 Track、PhysicalObject 等对象时,请使用 factory method 设计模式。
(2) 构造 ConcreteCircularOrbit 对象时,针对不同应用中所需的不同类型的 L 和 E,使用 abstract factory 或 builder 设计模式。
(3) Iterator设计模式,在遍历各PhysicalObject对象时使用。
(4) 可复用API时遵循façade 设计模式,将所有 API 放置在 helper类CircularOrbitAPIs当中
(5) StellarSystem 应用中使用decorator设计模式
(6) AtomStructure 应用中,请使用 state 和 memento 设计模式管理电子跃迁 的状态,并可进行状态的恢复。意即:可保存电子每次跃迁前后的轨道信息。
实验涉及知识
继承和委托
继承inheritance.可能涉及到override,较易理解。下面重点分析delegation(委托)
简而言之,delegation是一个对象需要依赖其他的对象所实现功能的行为。
分为Explicit和Implicit两种。
-e.g. theSorter
is delegating functionality to some Comparator
LSP(Liskov替换原则)
- Subtypes can add, but not remove methods
子类型可以增加方法,但不可删。 - Concrete class must implement all undefined methods
子类型需要实现抽象类型中的所有未实现方法。 - Overriding method must return same type or subtype
子类型中重写的方法必须有相同或子类型的返回值。 - Overriding method must accept the same parameter types
子类型中重写的方法必须使用同样类型的参数。 - Overriding method may not throw additional exceptions
子类型中重写的方法不能抛出额外的异常。
而且满足下列条件: - Same or stronger invariants 更强的不变量
- Same or weaker preconditions 更弱的前置条件
- Same or stronger postconditions 更强的后置条件
抽象类?
抽象类与多态有关,了解抽象类->为什么使用抽象类?
OO Design Principles:SOLID
- (SRP) The Single Responsibility Principle 单一责任原则
- (OCP) The Open-Closed Principle 开放-封闭原则
- (LSP) The Liskov Substitution Principle Liskov替换原则
- (DIP) The Dependency Inversion Principle 依赖转置原则
- (ISP) The Interface Segregation Principle 接口聚合原则
SRP
一个类,一个责任。(下图为反例)
OCP
对扩展性的开放,对修改的封闭。
- 模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化
- 但模块自身的代码是不应被修改的
- 扩展模块行为的一般途径是修改模块的内部实现
- 如果一个模块不能被修改,那么它通常被认为是具有固定的行为
关键技术:抽象
LSP
前文
ISP 接口隔离
不能强迫客户端依赖于它们 不需要的接口:只提供必需的接口
DIP 依赖转置
- 抽象的模块不应依赖于具体的模块
- 具体应依赖于抽象
关键词:隔离
Design Patterns
一览Design Patterns的框架
Decorator
为什么要这种模式:动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
Problem:如何为对象增加不同侧面的特性。
Solution:对每一个特性构造子类,通过委派机制增加到对象上。
- 抽象构件类(Component):给出一个抽象的接口,用以规范准备接收附加责任的对象
- 具体构件类(ConcreteComponent):定义一个具体的准备接受附加责任的类,其必须实现Component接口。
- 装饰者类(Decorator):有一个构件(Conponent)对象的成员变量,并定义一个和抽象构件一致的接口(我们就是通过这个一致的接口中的东西来加东西)。
- 具体装饰者类(Concrete Decoratator):定义给构件对象“贴上”附加责任。
每次想给具体构建类的某个实例加上一点东西的时候,我们做出要加上的东西对应的具体装饰者类,以具体构建类作为构造器的输入参数,然后调用装饰类的方法输出装饰后的结果。注意上述所有的变量的声明都是他们的公共接口。
看decorator模式.其中关键的decorator为material里面添加的内容。如:
1 | class Beef extends Material{ |
facade(外观模式)
外观模式是为了解决类与类之间的依赖关系的,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个Facade类中,降低了类类之间的耦合度,该模式中没有涉及到接口。
应用于小的接口,目的是提供一个进行相似工作的接口。详细理解外观模式
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class SystemA {
public void operationA(){
System.out.println("operation a...");
}
}
public class SystemB {
public void operationB() {
System.out.println("operation b...");
}
}
public class SystemC {
public void operationC() {
System.out.println("operation c...");
}
}
facade
1 | public class Facade { |
客户端
1 | public class Client { |
strategy模式
- 问题描述:对于一个特定问题存在不同的算法,客户能够在动态运行期间(at run time)在这些算法之间切换。
- e.g. Sorting a list of customers(bubble sort, mergesort, quicksort)
- 解决: 创造一个接口,对算法中每个variant创建一个实现类。(implementing class)
state模式
用一句话来表述,状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。
状态模式所涉及到的角色:
- 环境(Context)角色,也成上下文:定义客户端所感兴趣的接口,并且保留一个具体状态类的实例。这个具体状态类的实例给出此环境对象的现有状态。
- 抽象状态(State)角色:定义一个接口,用以封装环境(Context)对象的一个特定的状态所对应的行为。
- 具体状态(ConcreteState)角色:每一个具体状态类都实现了环境(Context)的一个状态所对应的行为。
详细阅读→状态模式
Iterator
- 问题所在:客户需要访问容器内的所有元素,无论该容器的类型如何
- 解决方案:A strategy pattern for iteration
- 应用场景:(1)访问一个聚合对象而不需暴露对象内部表示;(2)支持对聚合对象的多种遍历;(3)对遍历不同的对象,提供统一的接口。
具体示例iterator迭代器设计模式和迭代器模式
Factory Method pattern
Also known as “Virtual Constructor”. 工厂方法案例1和工厂方法案例2
应用场景:当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体 创建的实例时,用工厂方法。 定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
- 抽象产品角色(Product):定义产品的接口
- 具体产品角色(ConcreteProduct) :实现接口Product的具体产品类
- 抽象工厂角色(Creator) :声明工厂方法(FactoryMethod),返回一个产品
- 真实的工厂(ConcreteCreator):实现FactoryMethod工厂方法,由客户调用,返回一个产品的实例
开工厂方法就是不想把子类名字暴露,把创建和返回隔离开。在子类实现过程当中不会暴露。
1 | public Track getTrack(int inputRadius) { |
Abstract Factory
创建的不是一个完整产品,而是“产品族”(遵循 固定搭配规则的多类产品的实例),得到的结果是:多个不同产品的 object,各产品创建过程对client可见,但“搭配”不能改变。
本质上,Abstract Factory是把多类产品的factory method组合在一起1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91abstract class AbstractProductA { // A产品
public abstract void productMethod();
};
abstract class AbstractProductB { // B产品
public abstract void productMethod();
};
abstract class AbstracFactory { // 产品族的抽象工厂
public abstract AbstractProductA createProductA();
public abstract AbstractProductB createProductB();
};
class ProductA1 extends AbstractProductA {
public void productMethod() {
System.out.println("ProductA1");
}
};
class ProductB1 extends AbstractProductB {
public void productMethod() {
System.out.println("Product B1");
}
};
class ProductA2 extends AbstractProductA {
public void productMethod() {
System.out.println("Product A2");
}
};
class ProductB2 extends AbstractProductB {
public void productMethod() {
System.out.println("Product B2");
}
};
class Factory1 extends AbstracFactory {
public AbstractProductA createProductA() {
System.out.println("Factory1");
return new ProductA1();
}
public AbstractProductB createProductB() {
System.out.println("Factory1");
return new ProductB1();
}
};
class Factory2 extends AbstracFactory {
public AbstractProductA createProductA() {
System.out.println("Factory2");
return new ProductA2();
}
public AbstractProductB createProductB() {
System.out.println("Factory2");
return new ProductB2();
}
};
public class Client {
public static void main(String arg[]){
AbstracFactory FAC;
AbstractProductA PA;
AbstractProductB PB;
FAC = new Factory1();
PA = FAC.createProductA();
PA.productMethod();
PB = FAC.createProductB();
PB.productMethod();
System.out.println("----------------");
FAC = new Factory2();
PA = FAC.createProductA();
PA.productMethod();
PB = FAC.createProductB();
PB.productMethod();
}
};
builder
- Builder: 为创建一个Product对象的各个部件指定抽象接口。
- ConcreteBuilder: 实现Builder的接口以构造和装配该产品的各个部件;定义并明确它所创建的表示;提供一个获取产品的接口。
- Director: 构造一个使用Builder接口的对象。
- Product: 表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程 。包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
举例:去必胜客点pizza。服务员(director)推荐给你套餐(builder),套餐一:甜甜圈,辣酱和海鲜披萨(concretebuilder),套餐二:薯条,番茄酱和水果披萨(concretebuilder)……而具体的套餐内产品(product)要进行制作(构造):
1 | private String sauce; |
配上一系列set操作,叮,做好了!
memento(备忘录模式)
设计模式之美:备忘录模式和《java与模式》备忘录模式
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
结构:
橙子说:
Originator发起人角色:记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据
Memento备忘录角色:负责存储Originator发起人对象的内部状态,在需要的时候提供发起人需要的内部状态。
Caretaker备忘录管理员角色:对备忘录进行管理、保存和提供备忘录。
实践出真知
Track
构造当中用极复杂的方式实现本来很简单的数据结构。但是便于扩展应用。(后来想想我就是傻,一个Track有啥扩展的?)
test测试小插曲
assertEquals对double类型测试出错?
查阅资料-看异常中的提醒Use assertEquals(expected, actual, delta) to compare floating-point numbers, 即按照“预期值,真实值,误差值”的格式,只要在这个范围内就算正确。
后来改成这个assertEquals( 2.2, stellarTrack.getRadius(),0.0);
科学记数法处理
在实验中涉及到对number的处理
大于 10000 的数字按科学记数法表示(例如 1.9885e30 表示1.9885 ∗ 1030,但 e 之前数字的整数部分必 须在 1 到 9 的范围内,e 之后的数字只能是大于 3 的正整 数),小于 10000 的数字直接给出(例如 5912,103.193), 不能用科学计数法。小数点位数不限制。
以本人设计的Stellar(恒星系的中心物体)为例,域如下
1 | private final String name; |
如何根据要求转化为double类型的呢?橙子又说,BigDecimal!this.radius = new BigDecimal(inputRadius).doubleValue(); //转化为科学记数法
为什么轨道上放不了物体?
发现如果直接用placeObject(Track inputtrack, E object)
就直接能加上物体,而如果先加入轨道的话就不能。
1 | public boolean placeObject(Track t, E object) { |
关键在于我的addTrack:
1 | public boolean addTrack(Track inputTrack) { |
第一开始并没有在objectsOnTrack.put(inputTrack, objects);
和List<E> objects = new ArrayList<E>();
,如果不事先放入一个集合进objectsOnTrack这个map的话,会返回空指针。
子类成员变量和父类
在我的SocialNetworkCircle和其父类ConcreteCircularOrbit中都有private final List<Track> tracks; //人际关系,其中radius表示好友所在的轨道级数
然而我在子类方法中调用父类的placeObject,其中会加入新轨道。父类placeObject
代码:
1 | public boolean placeObject(Track t, E object) { |
给的样例中,和中心人物有关的有四对关系,但其中只有两级轨道。一级好友和两级好友。
子类中
1 | Track t = new SocialTrackFactory().getTrack(String.valueOf(distance)); |
第3行为关键,如果注释掉的话每次都会new一个新的track,但是如果加上,只会new两个。猜测这个加入到父类的tracks中而非子类的。
证据:
后来发现objectsOnTrack这个map也有问题,同样的操作会直接对父类的这个成员变量产生影响,唉,索性让我重写父类的方法再ctrl +c/+v。
续:后来得知我的子类的重复定义了父类的成员变量,然后调用一系列方法的时候会被父类成员变量覆盖。
保留两位小数
1 | BigDecimal rounded = new BigDecimal(entropy); |
其中entrop
是计算熵的时候用的。
何方异常是也?ConcurrentModificationException?
在写星体转动的时候,在position中进行了这样的操作。
1 |
|
就会报错。
表示暴露实例分析
纪念第一次通宵写程序。在获取轨道变化前的map时我的画风
1 | for(Track track : objectsOnTrack.keySet()) { |
想着直接把List
1 | for(Track track : objectsOnTrack.keySet()) { |
如此安好!
实验三算过去了,总结的也算一般般吧。一点点进步吧,加油!