1. 程式人生 > 實用技巧 >面向物件分析設計考試複習【歷年卷】

面向物件分析設計考試複習【歷年卷】

物件識別與職責劃分,是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加油!