软件构造实验二

软件构造实验二


写在前面

  哈尔滨某高校大学生惨遭六名教授轮流布置作业和实验
  代码一小时,注释写一天,报告编一年

P1

  Eclipse的配置相关,直接点文字进去就有教程,安装中文插件(会影响一些内部代码显示)Eclipse开启全键触发代码补全去除空格触发补全问题,需要下载对应版本的Eclipse的SDK版本,百度一下EclipseSDK吧开启assert功能Eclipse各种小图标的意义,那个在去除空格触发补全问题的时候,要把生成的jar包塞到C:\users\.p2文件夹下,不知道为什么在安装文件夹\eclipse\plugins莫得用
  现在来解释为了写P1要搞清楚的一大坨概念

前置知识

  主要来源是神仙学长(点击神仙学长跳进去看神仙),下面都是我的通俗理解(仅针对实验的理解),严格定义请参考学长
使用者(client)/实现者
  实现者,即编写某个类的代码的人,下面文章中我们都是以实现者的视角来说明,使用者即客户端,即使用你的类的人,可能是其他根本不知道你写的类的内部实现内容的人,在这里ADT在Java中的具体表示就是一个类
创建者/生产者/观察者/改造者
  创建者就是java类中的构造函数,生产者则应该是根据外界要求返回与这个类相关的某个操作的结果(toString之类的),观察者是能获得内部成员变量的函数(getName之类的),改造者则应该是能修改成员变量的函数
不变量
  就是java中某个类能保护自己的成员变量不被除自己以外的类修改,private只是一种不完美的方法,如下的代码,由于传入的参数实际上是使用者提供的,使用者可以通过修改这些参数来操控内部成员变量

    /**
    * This immutable data type represents a tweet from Twitter.
    */
    public class Tweet {

        public String author;
        public String text;
        public Date timestamp;

        /**
         * Make a Tweet.
         * @param author    Twitter user who wrote the tweet
         * @param text      text of the tweet
         * @param timestamp date/time when the tweet was sent
         */
         public Tweet(String author, String text, Date timestamp) {
            this.author = author;
            this.text = text;
            this.timestamp = timestamp;
        }
    }

    /*一段使用者代码*/
    public static Tweet retweetLater(Tweet t) {
        Date d = t.getTimestamp();
        d.setHours(d.getHours()+1); // 操纵内部成员变量
        return new Tweet("rbmllr", t.getText(), d);
    }

  这就是一个典型的表示暴露 Rep exposure的例子,上述例子是在使用观察者时候造成的表示暴露,注意还可能会在使用构造者的时候引起表示暴露

表示暴露

  为了防止表示暴露,我们还需要防御性拷贝来防止使用者的修改,即在传入参数的时候拷贝一份下来,这样使用者的变量和自己的成员变量就是两个完全不同的对象了

    public Tweet(String author, String text, Date timestamp) {
        this.author = author;
        this.text = text;
        this.timestamp = new Date(timestamp.getTime()); // 构造者中的防御性拷贝
    }

  最后总的来说,不变量应该满足

  1. 被创建者或生产者创建;
  2. 被改造者和观察者保持;
  3. 没有表示暴露。

可变类型的不可变包装

Java的collections类提供了一种有趣的“折中”:不可变包装。
Collections.unmodifiableList() 会接收一个(可变)List然后将其包装为一个不可变对象——它的 set(), add(), remove(),等操作都会抛出异常。所以你可以将一个List包装为不可变对象(记得将以前对于List的索引丢掉),然后将它传入其他地方使用。
这种方法的缺点就是你只能在运行时获得不可变性,而不是编译时。Java不会在编译的时候对你对“不可变”列表的修改提出警告。但是这总比什么都不做好,所以使用不可变的列表、映射、和集合也是减少bug的好方法。

表示不变量和抽象函数
  先介绍抽线域(A)和表示域(R),简单一点说抽象域就是使用者想要的某种抽象的全体,如下图,使用者想要集合这个抽象概念,那么所有集合就是抽象域。表示域是具体实现的,即用来代表使用者想要的抽象概念的具体变量,如下图用字符串“abc”代表集合{a,b,c},“abc”就是表示域中的一个元素

抽象域和表示域

  抽象函数(AF)就是把表示域中的元素对应到抽象域的函数,表示不变量(RI)就是表示域中那些在有抽象函数值的那些元素组成的集合(当然可以有元素没有抽象函数值,像上图的String “abbc”)

AF & RI

例如上图中,CharSet这种类型的实现禁止有重复字符,所以 RI(“a”) = true, RI(“ac”) = true, RI(“acb”) = true, 但是 RI(“aa”) = false, RI(“abbc”) = false.其中为假的集合用红色区域表示,合法的(为真)的字符串集合用绿色表示。

  再和实验相关一点,AF就是某个成员变量代表的抽象概念,RI就是对这个成员变量的限制,如下面代码,RI强制要求字符串s中的字符按从小到大来排列以表示集合AF(s),由于AF和RI一般与使用者没有关系,因此多用//来注释,而不是用javadoc来注释(/* /)

    public class CharSet {
        private String s;
        // Rep invariant:
        //   s[0] <= s[1] <= ... <= s[s.length()-1]
        // Abstraction function:
        //   AF(s) = {s[i] | 0 <= i < s.length()}
        ...
    }

CheckRep
  理解了RI就很好理解,这个函数就是用来检查当前成员变量是否满足限制条件的

你应该在每一个创建或者改变表示数据的操作后调用 checkRep() 检查不变量,换句话说,就是在使用创建者、生产者以及改造者之后。
虽然说观察者通常不需要使用 checkRep() 进行检查,但这也是一个不错的主意。为什么?因为在每一个操作中调用 checkRep() 检查不变量更能够帮助你捕捉因为表示暴露而带来的bug。
  尽量不要在表示用使用null

// Check that the rep invariant is true
// *** Warning: this does nothing unless you turn on assertion checking
// by passing -enableassertions to Java
private void checkRep() {
    assert denominator > 0;
    assert gcd(Math.abs(numerator), denominator) == 1;
}

表示暴露的安全性注解
  即说明一下你用了什么方法来防止表示暴露,下面是一个完整的注释的例子

// Immutable type representing a tweet.
public class Tweet {

    private final String author;
    private final String text;
    private final Date timestamp;

    // Rep invariant:
    //   author is a Twitter username (a nonempty string of letters, digits, underscores)
    //   text.length <= 140
    // Abstraction function:
    //   AF(author, text, timestamp) = a tweet posted by author, with content text,
    //                                 at time timestamp
    // Safety from rep exposure:
    //   All fields are private;
    //   author and text are Strings, so are guaranteed immutable;
    //   timestamp is a mutable Date, so Tweet() constructor and getTimestamp()
    //        make defensive copies to avoid sharing the rep's Date object with clients.

    // Operations (specs and method bodies omitted to save space)
    public Tweet(String author, String text, Date timestamp) { ... }
    public String getAuthor() { ... }
    public String getText() { ... }
    public Date getTimestamp() { ... }
}

ADT的注解到底应该写什么

如上图所示,规格说明在使用者和实现者之间构建起了一道“防火墙”。抽象类型的规格说明(包含操作的说明)应该只关注使用者可见的部分,这包括参数、返回值、可能抛出的异常。例如规格说明需要引用T的值时,它应该是抽象域的值而非表示域。
规格说明不应该谈论具体的表示/实现细节,例如表示域里面的值。它应该认为表示本身(私有区域)对于使用者是不可见的,就像是方法里面的局部变量对外部不可见。这也是为什么我们在注解表示不变量和抽象函数的时候使用的是”\“注释而非典型的Javadoc格式。如果我们使用Javadoc注释的话,内部的实现细节会出现在规格说明中,而这会影响表示独立性以及信息隐藏。
总结一下上面文章的三个目标
1.远离bug. 一个好的ADT会确保它的不变量为真,因此它们不会被使用者代码中的bug所影响。同时,通过显式的声明和动态检查不变量,我们可以尽早的发现bug,而不是让错误的行为继续下去。
2.易于理解. 表示不变量和抽象函数详细的表述了抽象类型中表示的意义,以及它们是如何联系到抽象值的。
3.可改动. 抽象数据类型分离了抽象域和表示域,这使得实现者可以改动具体实现而不影响使用者的代码。

ADT注解

我终于开始写实验了

  P1的网址戳我
  先来看实验要求,可以知道我们是要实现一个带权有向可变的图,带权就是每条边都有权值,有向就是有向边,可变是指可以随时连接起图中两个点,也可以删除一条边或者一个点,也可以加入一个点。注意图中可以包含自环,不能有重边。

实验要求

  实际上problem1,2要求写的是顶点标签为String类型的图(就是每个顶点就是一个String),在problem3中才将String改为泛型。在介绍problem之前,我们有必要梳理一下整个P1的结构。Graph是一个抽象类,而两个子类都是Graph的具体实现。MIT在Graph中规定了一系列需要在子类中实现的方法(函数),我们就要在两个子类中实现。GraphStaticTest是一个可执行的JUnit测试,针对Graph中的静态方法进行测试。GraphInstanceTest则是一个抽象的JUnit类,他不能被运行,因为里面含有抽象方法emptyInstance(大概在源代码第40行可以看到声明),他针对Graph中的实例方法进行测试(就是非静态方法),这个JUnit只有被继承并且实现了emptyInstance方法之后才运行,而继承了这个JUnit的子JUnit也将拥有GraphInstanceTest的测试,所以当运行子JUnit的时候会发现GraphInstanceTest的测试用例也被运行了。下图右边两个子类则是对GraphInstanceTest的实现,这两个子类也是JUnit,而两个子类因为已经继承了GraphInstanceTest,所以不用对实例方法进行测试了,只需测试两个不同的实现下所包含的Edge类或者Vertex类。
P1结构
  problem1中的实际上是不需要你实现Graph中的emptyInstance方法的,你只需要写针对Graph中的静态方法的测试(也可以不写,因为MIT已经帮你写了),和针对Graph中的非静态方法的测试。

P1problem

  其中我写的一些测试用例如下面所示,test strategy主要是根据这个方法在哪些情况下应该返回哪些值来编写,还有一种写测试用例的方法就是面向覆盖度写测试。

@Test
public void testSourcesCase1() {
    Graph<String> graph = emptyInstance();
    HashMap<String, Integer> tmp = new HashMap<String, Integer>();
    assertTrue("expected add right", graph.add("fzh"));
    assertTrue("expected add right", graph.add("evan"));
    assertTrue("expected add right", graph.add("shawn"));

    assertEquals("expected right size", 3, graph.vertices().size());
    assertEquals("expected right set", 0, graph.set("fzh", "evan", 3));
    assertEquals("expected right set", 0, graph.set("evan", "fzh", 3));
    assertEquals("expected right set", 0, graph.set("fzh", "shawn", 4));
    assertEquals("expected right set", 0, graph.set("fzh", "vampire", 10));
    assertEquals("expected right set", 0, graph.set("FZHvampre", "vampire", 0));
    assertEquals("expected right set", 0, graph.set("shawn", "evan", 0));

    tmp.put("evan", 3);
    assertTrue("expected right judgement", graph.sources("fzh").equals(tmp));
}

  然后是problem2,实现两个子类ConcreEdgeGraph和ConcreVerticesGraph
  ConcreEdgeGraph中有一个类叫Edge,应该是对边的抽象,正好贴一些我的AF,RI和safe from exposure的写法

class Edge<L> {
    // 写博客的时候我已经写完了实验...如果你正在写problem2此时这里应该是Edge<String>
    private final L source; // 在problem2中这里应该都是String类型...
    private final L target; // String source,target;
    private final int weight;
    // TODO fields

    // Abstraction function:
    // TODO
    // AF(source) = this edge's start vertex
    // AF(target) = this edge's end vertex
        // AF(weight) = the weight of this edge
    // Representation invariant:
    // TODO
    // RI(source) = a valid String
    // RI(target) = a valid String
        // RI(weight) = a positive int
    // Safety from rep exposure:
    // TODO
    // use defensive method in constructor
    // use checkRep in constructor & getMethod
        // use final modify symbol

  这里其他具体的实现的要求可以看Graph中对应函数的SPEC辣,这里就不介绍了。有一点值得注意的是,在写set函数中,当我们要新建一条边加到边集里面去的时候,我们要检查当前边集中是否已经存在新建的边(就是起点和终点相同的边),我们可以用List的contains函数,而List的contains函数中用来比较两个Edge类是否相同的方法是调用equals方法,因此我们要在Edge中重写equals方法,具体如下,注意一定要有@override的标记,否则就没有重写成功


    // 在Edge中重写的equals方法
    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (obj.getClass() != Edge.class)
            return false;
        Edge<?> edgeObj = (Edge<?>) obj; // 如果你正在写problem2,这里应该是Edge<String>
        return edgeObj.source.equals(source) && edgeObj.target.equals(target);
    }

    //在ConcreEdgeGraph类中的set函数
    @Override
    public int set(L source, L target, int weight) { // 这里当然也不是problem2的写法...
        add(source); // 把所有的L换成String就是problem2中的正确姿势
        add(target);

        Edge<L> o = new Edge<L>(source, target, weight > 0 ? weight : 1);
        if (edges.contains(o) == false) {
            if (weight > 0)
                edges.add(o);
            checkRep();
            return 0;
        }
        int lastValue = edges.get(edges.indexOf(o)).getWeight();
        edges.remove(o);
        if (weight > 0)
            edges.add(o);
        checkRep();
        return lastValue;
    }

  ConcreVerticesGraph同理,不过这里面的Vertex类里面需要实现是不只是一个顶点,Vertex里面还要包括这个顶点为源链接到的顶点

class Vertex<L> {
    private final L label;
    private final Map<L, Integer> toVertex;
    // TODO fields

    // Abstraction function:
    // TODO
    // AF(label, toVertex) = a vertex in the graph & the edges from it
    // Representation invariant:
    // TODO
    // RI(vertex) = a valid String (for label can be any valid String)
    // RI(toVertex) = a Map whose key set reps the vertexs this vertex can goto,
    // along with the
    // value meaning the weight of this edge, attention that weight must be
    // positive, for a
    // edge with 0 weight can not be exist
    // Safety from rep exposure:
    // TODO
    // Label is private final type, can not be modified
    // for String is immutable, so no special operation for rep exposure
    // use defensive copy in constructor, getMethod, and set method
    // use checkRep to check if rep invariant is satisfied

  problem3中把所有出现String的地方改成泛型(L)就好了,如果有问题根据编译错误的那个小红叉叉提示的信息(或者直接在红色波浪线处按Ctrl + F1)修改就可以了
  problem4就是调用前面已经实现的Graph实现一个改变输入的程序,大致意思是输入一个txt文件(里面有一首或者多首诗),根据诗建立一个Graph,有向边连接两个相邻的词,然后根据这个图修改输入的字符串,具体的要求戳我
  我把Do not go gentle into that good night当作语料库输进去了!

P2

  其实吧,P2代码我真的一共只写了10min,JUnit的测试直接从Lab1粘贴过来就好

P3

  是时候上图了
P3
  Game是接口,MyChessAndGoGame是客户端代码,其他的都是类(泪)。
  大致思想是,游戏得有个棋盘吧(Board),棋盘上面得有位置(Position)吧,Position就是一个mutable的类型,里面只有一个成员变量就是棋子(Pieces),如果位置上莫得棋子pieces就是null,board和position构成了独立的一部分。然后一部分就是由玩家(Player)和棋子(Pieces)组成的一部分,每个棋子有一个玩家,每个玩家有名字和自己的日志(根据要求游戏结束时要输出日志)。Action中只有静态方法,没有实例方法或者成员变量,每次进行动作的时候把Board,Player,Pieces和坐标等参数塞进Actino的方法里面,Action里面的方法就能帮你改变棋盘的状态,增加玩家的日志,返回true(成功操作)或者false(不成功操作),如果是false还要输出错误信息

P3结构

  在具体实现的时候Game只是一个接口,gameChess和gameGo实现了Game,并且在各自的构造器中完成了初始化的工作。