面向对象分析设计考试复习【历年卷】

对象识别与职责划分,是OO设计永恒的主题。

(以下非标准答案,仅代表个人理解,欢迎批评指正)

填空题

面向对象基本知识

  • 面向对象理论认为对象比函数更稳定,更适合作为程序的基本构成单位
  • OOP分析方法在做领域分析时,不仅要梳理业务流程,更是要发现对象
  • 从软件的角度来看,对象是一个完备的软件模块,因为其内部包括了数据处理这些数据的方法(函数)
  • OOP中,父类指针可以指向子类,这体现了IS-A这个检测继承关系的准则
    • IS-A代表的是类之间的继承关系
    • HAS-A代表的是对象和它的成员的从属关系
  • 类之间的层次关系主要是继承组合/聚合
    • 组合是指不可或缺的,比如轮胎、发动机之于汽车
    • 聚合是指非不可或缺的,比如手臂之于人,人没了手臂依然可以生活
  • OOP中实现功能复用的两个基本方法是继承组合/聚合(A、B卷都考了)
  • 描述对象间往返的消息:函数名函数参数函数返回值
  • 可以在同类不同对象间共享的是函数静态变量
  • 通过类可以直接访问的是静态成员
  • 类主要通过公有接口与其他类交互
  • 相互独立的两个类之间可以具有的关系是依赖关系没有关系

(解释一下依赖关系,比如A依赖B,并不一定是A中有B,比如说A有方法void f(B b)需要B类参数,这就是依赖)

  • 类之间的继承关系,根据父类的个数可以分为多继承单继承两种
  • 一个类成为不变类的充要条件是状态不变或不包含属性

设计原则(SOLID)

  • 依赖倒转原则认为,应该让具体依赖于抽象
  • 开闭原则认为,应该对扩展开放,对修改关闭

设计模式

  • 抽象工厂模式在什么情况下不支持开闭原则:新增产品结构

(工厂方法模式就完全支持开闭原则,因为具体工厂类都有共同的接口,或者都有共同的抽象父类因为具体工厂类都有共同的接口,或者都有共同的抽象父类,所以它也叫多态性工厂)

  • 单例类可以看作是工厂模式中的工厂角色和产品角色的合并
  • 对于对象A和它的克隆对象B,A==B应该返回
  • 为安全的实现对象的拷贝,要做到深拷贝(主要是为了防止同一块内存释放两次导致内存泄漏)
  • 之所以有对象的浅拷贝问题,主要是对象内含有对象引用(对象引用)
  • 可以描述树状结构的是合成模式
  • 代理模式中,代理类与主体类(被代理类)应是兄弟(同一类不同子类)关系
  • 命令模式中,除了命令发起者外,还有接收命令抽象命令具体命令角色

看似多余的解释

依赖关系

我们举个例子:

class Person {
    public void fly(Plane p){
        p.fly();
    }
}

class Plane {
    public void fly(){
        // 飞的操作
    }
}

这样我们就可以说Person类依赖Plane类,当然了,这样也是依赖,我们也经常这么做(这叫委派原则):

class Person {
    private Plane p;
    public void fly(){
        p.fly();
    }
}

设计模式

设计模式是一套解决软件开发过程中某些常见问题的通用解决方案,是已被反复使用且证明其有效性的设计经验的总结。

其中包含三种模式:

  • 创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式

  • 结构型模式,共7种:适配器模式、装饰模式、代理模式、外观模式、桥接(梁)模式、合成模式、享元模式

  • 行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

难啃的骨头

迪米特原则

又称为最少知识原则,一个对象应当对其他对象尽可能少的了解。

如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。(比如中介者模式)

但这样会导致系统内造出大量的小方法,散落在系统的各个角落,并且与系统中的业务逻辑无关,那咋办嘛?

破解!——依赖倒置原则

让原来的A不依赖朋友B来与C通信,而是依赖于抽象陌生人C,也就是和抽象的C做朋友,而不是具体C

迪米特法则的主要用意是控制信息的过载,在运用迪米特法则到系统的设计中时,要注意以下几点:

  • 在类的划分上,应当创建有弱耦合的类.类之间的耦合越弱,就越有利于复用。

  • 在类的结构设计上,每一个类都应当尽量降低成员的访问权限。

  • 在类的设计上,只要可能,一个类应当设计成不变类。

  • 在对其他类的引用上,一个对象对其他对象的引用应降到最低。

  • 尽量限制局部变量的有效范围。

合成模式

合成模式属于对象的结构模式,有时又叫做部分——整体模式。合成模式将对象组织到树状结构中,可以用来描述整体与部分的关系。合成模式可以使客户端将单纯元素与复合元素同等看待。最经典的应用就是“文件系统”。

合成模式有两种:安全式和透明式。

安全式:从客户端使用合成模式上看是否更安全,如果是安全的,那么就不会有发生误操作的可能,能访问的方法都是被支持的。

在这里插入图片描述

透明式:从客户端使用合成模式上,是否需要区分到底是“树枝对象”还是“树叶对象”。如果是透明的,那就不用区分,对于客户而言,都是Compoent对象,具体的类型对于客户端而言是透明的,是无须关心的。

在这里插入图片描述

对于合成模式而言,在安全性式和透明性上,会更看重透明式,毕竟合成模式的目的是:

让客户端不再区分操作的是树枝对象还是树叶对象,而是以一个统一的方式来操作。

而且对于安全式的实现,需要区分是树枝对象还是树叶对象。有时候,需要将对象进行类型转换,却发现类型信息丢失了,只好强行转换,这种类型转换必然是不够安全的。

因此在使用合成模式的时候,建议多采用透明式的实现方式。

门面模式
  • 为复杂子系统提供一个简单接口
  • 提高子系统独立性
  • 形成层次化结构,定义层与层之间的接口
  • 迪米特法则的最好应用(朋友越少越好,也只与这个朋友交流)
  • eg.医院里的导诊
访问者模式
  • 倾斜的可扩展性(类结构稳定),若数据结构变化频繁,则不适合这个模式
  • 违背“开-闭原则”
  • 方法集合的可扩展性,类集合的不可扩展性(允许节点新方法,不允许新类)
  • eg.老板、会计(访问者)查公司账本(对象结构)里的账单:支出/收入(被访问者)
中介者模式
  • 把系统网状结构变成星型结构,对象间不直接相互作用,而是通过中介
  • 遵守迪米特法则,把系统中有关的对象所引用其他的对象降到最少(朋友越少越好,也只与这个朋友交流)
  • 避免了同事间的过度耦合,多对多的结构也变成了一对多,易于维护、理解
  • eg.登录窗口需要输入账户密码,到达一定位数后,才能按登录按钮

相关文章推荐


简答题

什么是多态?请举例其优点。

  • 定义:多态是与继承相关的概念,从共同的超类派生出不同的子类,不同子类对象呈现出多种形态。

  • 优点:(1)模拟现实世界的多态特性;(2)提高程序灵活性;(3)降低类之间的耦合

  • 举例:

// 比如我们人去借书
人.借书(书);
书.出借(人);
// 根据人、书不同,结果也不同

请说明什么是模块化设计,在面向对象中,可从哪些方面实现模块化设计?

  • 模块是一组能完成某个完整功能的代码逻辑集合,有完整的输入输出

  • 模块化设计就是将系统划分为若干个模块,每个模块完成一个特定的功能,做到高内聚、低耦合,最后所有模块汇聚起来组成一个整体

  • 用封装的方法、访问控制符来限制模块间的交互,可以使用单一职责原则、迪米特原则等


为什么面向对象设计方法提倡把类的属性都设计为私有的?

  • 信息隐藏

    • 阻止外界直接对类的状态信息的访问,仅提供方法用以访问和改变它的状态,提高类的安全性

    • 提高对象的独立性,有利于灵活地局部修改,提升了程序的可维护性

一个模块设计得好坏的一个重要的标志就是该模块在多大的程度上将自己的内部数据与实现有关的细节隐藏起来

  • 信息隐藏的重要性

它可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化,使用、阅读以及修改


请简要分析为什么要提供“组合/聚合”?并以示意性代码说明。

说到了“组合/聚合”,我们自然要比较“继承”和“组合/聚合”。

  • 继承

    • 白盒复用
    • 静态强关联
  • 组合/聚合

    • 黑盒复用
    • 能更好的实现模块化设计
    • 动态低耦合,可替换

正因如此,“组合/聚合”可以做到“多身份”,而继承不行

举个例子:

// 继承只能同时做到一个身份了
class Person{
    public void work(){}
}

class Student extends Person{
    @Override
    public void work(){}
}

class Worker extends Person{
    @Override
    public void work(){}
}

class Client{    
    public static void main(String args[]){
        Person p1 = new Student();
        Person p2 = new Worker();
        // 做研究的活让学生干
        p1.work();
        // 搬砖的活让工人干
        p2.work();
        // 只能这样子一对一
    }
}

// 而组合/聚合则可以多身份(类似桥梁模式)
public interface Person{
    void work(){}
}

class Student implements Person{
    @Override
    public void work(){
        // 学习
        study();
    }
    private void study(){
        // ...
    }
}

class Worker implements Person{
    @Override
    public void work(){
        // 奔波
        hustle();
    }
    private void hustle(){
        // ...
    }
} 

abstract class Job{
    Person p;
    public void setPerson(Person p){
        this.p = p;
    }
    abstract public void doJob(){}
}

class StudentJob{
    public void doJob(){
        p.work();
    }
}

class WorkerJob{
    public void doJob(){
        p.work();
    }
}

class Client{    
    public static void main(String args[]){        
        Job studentJob = new StudentJob();
        Job workerJob = new WorkerJob();
        Person p = new Student();
        // 做研究的活让学生干
        studentJob.setPerson(p);        
        studentJob.doJob();
        // 突然学生想打兼职了,那就让他干搬砖的活
        p = new Worker();
        workerJob.setPerson(p);
        workerJob.doJob();
    }
}

请画出状态模式的类图,并说明图中各个类所承担的角色。(A、B卷都考了)

在这里插入图片描述

  • State:抽象状态角色
  • ConcreteState:具体状态角色
  • Context:环境角色

请简述桥梁模式和策略模式的区别。

  • 桥接模式属于结构型模式,而策略模式属于行为型模式
  • 策略模式对算法单独封装,只考虑算法的替换,而不考虑context;而桥接不仅Implementor具有变化,Abstraction也能变化,两者独立封装,考虑的是不同平台下调用不同的算法工具,动态性更强
  • 桥接要突出的是接口隔离的设计原则,使两个体系分割开来,做到解耦,使他们可以松散的组合;而策略模式仅仅只是一个算法的层次,没达到体系

请说明为什么要保持类的职责单一。

  • 根本上,是为了实现模块化,即高内聚、低耦合

  • 是任何程序设计方法都遵循的原则,如面向过程方法中的函数设计

    • 一个函数只实现一种功能

    • 一个函数的代码行数不宜过长

  • 降低类的复杂度,提高类的可读性

  • 控制变更影响的范围

  • 提高系统的可维护性


某系统内包含有n个类,每两个类之间存在依赖关系,请问可以采用什么方法,降低该系统的复杂度?

  • 对于系统外部而言,可以使用门面模式,为该系统提供一个与其他系统通信的公共接口,做到高耦合、低内聚
  • 对于系统内部而言,可以使用中介者模式,通过增加一个中介角色作为原来同事之间消息通信的中介,从而解除每两个类之间存在依赖

扩展

OOP的三大特性的作用

  • 提高系统的灵活性
  • 降低类之间的耦合性

程序分析题

以下程序将实现书的销售功能,它将根据书的类型执行不同的销售方案,请指出当前设计存在的问题,并重构该程序。

abstract class Book{
    public int type;
    public float discountRate;
}

class NovelBook extends Book{
    public int type = 1;
    public float discountRate = 0.1;
}

class SienceBook extends Book{
    public int type = 2;
    public float discountRate = 0.15;
}

class MusicBook extends Book{
    public int type = 3;
    public float discountRate = 0.2;
}

class Client{
    public void sell(Book book){
        if(book.type == 1){
            // plan 1
        }else if(book.type == 2){
            // plan 2
        }else if(book.type == 3){
            // plan 3
        }
    }
}

问题:

  • Book类对成员变量没有封装,直接把成员暴露给客户端

  • Book类没利用到OOP的多态,导致一长串的if-else,不利于维护,违反了“开闭原则”

  • 仍旧是一个”面向过程“的编程思维

修正后的代码如下:

abstract class Book{
    protected float discountRate;
    abstract public void sell(){} 
}

class NovelBook extends Book{
    private float discountRate = 0.1;
    public void sell(){
        // plan 1
    } 
}

class SienceBook extends Book{
    private float discountRate = 0.15;
    public void sell(){
        // plan 2
    } 
}

class MusicBook extends Book{
    private float discountRate = 0.2;
    public void sell(){
        // plan 3
    } 
}

class Client{
    public void sell(Book book){
        book.sell();
    }
}

有内味了吧~


请在不修改以下代码的情况下,补充新的类,是①处的程序语句可以调用Adaptee类的sampleOperation1函数。

public interface Target{
    void sampleOperation1();
    void sampleOperation2();
}

public class Adaptee{
    public void sampleOperation1();
}

public class Client{
    public void fun(Target t){
        t.sampleOperation1(); // ①
        t.sampleOperation2();
    }
}

根据题意以及代码的类命名我们可以明确,这是个适配器模式

  • 已有

    • 目标角色(Target)
    • 源角色(Adaptee)
  • 还缺

    • 适配器(Adapter)

那我们就来写这个Adapter!只要让它继承Adaptee,又实现Target接口,就能完成适配了!

public class Adapter extends Adaptee implements Target {
    /** 由于源类没有方法sampleOperation2,因此适配器类补上这个方法 */
    public void sampleOperation2() {
        // Write your code here
    }
}

请指出以下程序设计存在的问题,并给出改进方案。

public class Foo{
    // 以下函数包含上千行代码
    public void bigMethod(){
        /*
         * 代码块1
         */
        /*
         * 代码块2
         */
        /*
         * 代码块3
         */
        if(...){
            /*
             * 代码块4
             */
        } else {
            /*
             * 代码块5
             */
        }        
    }
}

其中存在的问题:

  • 复杂
  • 不稳定
  • 可读性差

我们应该用面向对象的思维来重构这个代码,比如通过一些类来做方法实现,更关键的是我们要把它”模块化“

对于这样一个这么长的方法实现,且其中确实存在因不同功能而存在区分的代码块,那么我们可以考虑使用模板模式,用不同的类实现不同的部分

这样一来,代码自然会清爽很多~


请阅读以下表示搜索引擎的类图,指出其中存在的问题,并画出修改后的类图

在这里插入图片描述

一个接口不应该有这么多方法,这会造成接口污染,遵循【单一职责原则】,我们需要将这个接口拆分


程序设计题

二叉树是每个节点最多有两个子节点的树结构,如以下示意图1所示,假设每个节点可存储一个字符数据,每个节点拥有其子节点的信息,但不存储父节点的信息。树支持添加、删除单个节点操作,以及遍历所有节点的操作。

(1)请以面向对象的方式表达二叉树,以UML类图和程序代码或伪代码描述。(不需要函数具体实现)

(2)如果(1)中的二叉树改为n叉树,又该如何设计?(n>=2)

(3)在(1)的二叉树中,令节点存储一个整数。各树枝节点存储的是以其为根的子树的所有叶子节点的整数和,如下图2所示。请实现当更改叶节点存储的整数值时,可同时更新其父节点及所有祖先节点的整数值。

# 这是个简略的图1
   A
  / \
  B  E
 /\   \
C  D   F

# 这是个简略的图2
   9
  / \
  5  4
 /\   \
3  2   4
# 更新2为8
   15
  /  \
 11   4
 /\    \
3  8    4

(1)树状结构,很显然,这是个【合成模式】

在这里插入图片描述
代码如下:

class Node {
    private Node left;
    private Node right;
    private char val;
    // 省略getter和setter
}

class Tree {
    private Node root;
    // 省略getter和setter
    public void add(Node node){}
    public void delete(Node node){}
    public void traversal{}
}

(2)因为子节点数量变成不固定了,我们就需要一个不定长的数据结构来维护节点表

在这里插入图片描述

相关节点的代码如下:

class Node {
    private char val;
    // 省略getter和setter
}

// 二叉树
class BinNode extends Node{
    private List<Node> nodes;
    // 省略getter和setter
}

// 二叉树
class BinNode extends Node{
    private List<Node> nodes;
    // 省略getter和setter
}

// 三叉树
class TriNode extends Node{
    private List<Node> nodes;
    // 省略getter和setter
}

// 四叉树
class QuoNode extends Node{
    private List<Node> nodes;
    // 省略getter和setter
}

// ...

// n叉树
class NNode extends Node{
    private List<Node> nodes;
    // 省略getter和setter
}

(3)由题意我们知道,叶子节点的改动,会造成父节点的改动,一个对象的改动会影响其他对象,这正是观察者模式!观察者为父节点,被观察者为子节点本身,逐层传递。

在这里插入图片描述

class Node {
    private Node parent;
    private int val;
    // 省略getter和setter
    public void updateVal(int newVal){
        if(newVal != val){
            int diff = newVal - val;
            this.val = newVal;
            if(parent != null){
                parent.updateVal(parent.getVal() + diff);
            }
        }
    }
}

还有一些比较个人化的题目我就没往上写了,xdm加油!

posted @ 2020-11-19 16:45  王帅真  阅读(990)  评论(0编辑  收藏  举报