從素數問題看對象思維方式


說明:這篇文章是很早之前就發表在CSDN上的原創作品(CSDN的博客好久不更新了),文章的創意最早來自於一位老教授給我的一個求素數問題的C++程序。以這個程序為基礎,我設計了如何用面向對象的思維來求解素數的設計方案,並將其作為我的研究生“面向對象分析與設計”課程的開篇,效果非常好,其授課內容也獲得了學院青年教師講課比賽的一等獎。后來在出版配套教材時,也作為該教材的開篇。其內容也共享出來,供大家討論和交流。
1. 引言
隨着C++、Java、C#等面向對象的編程語言的日益普及,面向對象概念已經深入人心;從面向對象的編程語言到面向對象軟件工程方法也日益得到廣泛的應用;關於面向對象的更進一步的應用如:面向構件、面向服務、面向模式等新思維也逐步發展起來,而這些新方法都是建立在面向對象的思維方式上的。由此可見,深入理解面向對象的思維方式不僅可以幫助我們理解目前面臨的應用模式,還是我們進一步學習和發展的必經之路。
我們很多人都是傳統的結構化思維方式中走過來的,本文即通過一個經典數據結構中的算法問題來探討如何走出傳統的思維模式,而通過對象思維方式來解決問題,進而理解到底什么是對象思維方式。從而為后續的對象課程熱身。
2. 傳統思維方式
我們所面臨的是一個求素數的問題,素數(也叫質數)是指除了1與本身之外,不能被其他正整數整除的數。按照習慣規定,1不算素數,最小的素數是2,其余的是3、5、7、11、13、17、19……等等。
根據上面的定義我們可以推導出判斷素數的算法:對於數n,從i=2,3,4,5…到 n-1 判斷 n能否被i整除,如果全部不能整除,則n是素數,只要有一個能除盡,則n不是素數;事實上,為了壓縮循環次數,可將判斷范圍從2 ~ n-1改為2 ~ sqrt(n)。
篩選法是一個經典的求素數的算法,它的作用並不是判定某個數是否是素數,而是求小於數n的所有素數,下面通過一個簡單的例子來說明篩選法的求解過程。
我們需要求解50以內的所有素數i(2<i<n),為此我們需要進行如下的篩選:
算法的執行過程是這樣的:
1)首先,拿當前最小的數(即2)作為因子,將后面所有可以被2整除的數去掉(因為它們肯定不是素數,參見圖中的第1行,去掉后面的4、6、8…,剩余結果見第2行);
2)之后,取剩余序列中第二小的數(即3)作為因子,將后面所有可以被3整除的數去掉(參見圖中的第2行,去掉后面的4、6、8…,剩余結果見第3行);
3)如此繼續,直到所取得最小數大於sqrt(n)(圖中第4行為最后一次篩選,此時的因子為7,因為下一個因子即為11大於sqrt(50));
4)剩余的序列即為n以內的所有素數()。
為了更清楚地描述該算法,我們可以采取傳統軟件工程中的流程圖來闡述上面的流程,下圖即為該算法的流程圖:
注意:上述的流程圖和前面描述的算法有所出入;在具體實現時,考慮到算法的執行效率並沒有將當前因子的倍數直接刪除(因為,如果采用數組來存儲當前數字序列,則刪除過程的算法復雜度都為O(n)),而是將相應的位置零,表明該位置已經沒有數據了。
設計出這樣一個算法后,后面的實現過程就是水到渠成了,我們可以隨便采用一種編程語言來實現,這里我們采用C語言來實現,其源代碼如下(PrimerNumber.c):

 

int main(){
    int *sieve, n;
  int iCounter=2, iMax, i;
  printf("Please input max number:");
  scanf(“%d", &n);
  sieve=(int *)malloc((n-1)*sizeof(int))
  for(i=0;i<n-1;i++) { sieve[i]=i+2; }
  iMax = sqrt(n);
  while (iCounter<=iMax) {
     for (i=2*iCounter-2; i<n-1; i+=iCounter)
        sieve[i] = 0;
      iCounter++;  
  }   
for(i=0; i<n-1; i++)    if (sieve[i]!=0) printf("%d ",sieve[i]); return 0; }

3. 是對象思維嗎?

在上面的問題中,我們可以很容易的從算法描述中構造目標程序,很自然,也似乎很符合我們的思維習慣;那么這種方法是面向對象的方法嗎?也許大家都會說不是!因為很明顯我們采用是C語言實現的,而C語言顯然是結構化的!
那么怎樣才算面向對象的方法呢?如果我現在需要大家用面向對象的方法去解決這個問題,那么我們又會怎么去做呢?
有人也許會說,這很簡單,Java是一門真正的面向對象的語言,我用Java去實現這個算法是不是就是面向對象呢?好,下面我們就看看該算法的Java實現(PrimerNumber.java):
import java.lang.Math;
public class PrimerNumber{
    public static void main(String args[]) {
        int n=50;
        int sieve[]=new int[n-1];
        int iCounter=2, iMax, i;
        for(i=0;i<n-1;i++) {sieve[i]=i+2;}
        iMax=(int)Math.sqrt(n);
        while(iCounter<=iMax){
            for (i=2*iCounter-2; i<n-1; i+=iCounter)
                sieve[i]=0;
            iCounter++;
        }
        for(i=0; i<n-1; i++)
        if (sieve[i]!=0) System.out.println(sieve[i]);
    }
}

 

 

在這個程序中,我們看到一個面向對象的關鍵特征:類;為了能夠使程序正確的通過編譯並運行,我們需要利用Java的關鍵字class定一個類,並為該類定義相應的成員函數(main函數),這不就是面向對象嗎?原來就這么簡單?!

4. 上升到對象思維
那么真的是這樣的嗎?我們再仔細看看程序的內部實現,怎么這么面熟?這不和前面的C程序很類似嗎?定義數組、利用for循環初始化,利用while循環控制因子;只不過將語法從C換成了Java,換湯不換葯;這不是面向對象,這只不過是披着“面向對象皮”的結構化程序,我把它稱為“偽面向對象”。
那么怎樣才算面向對象的思維方式呢?到底要怎樣去做才是一個面向對象的程序呢?在這里我先不討論關於面向對象的概念或理論(這些內容后續章節會陸續介紹),我們先來看看這個例子如何通過面向對象的方法來實現。
我們都知道,在面向對象的方法中,最終要的概念是類和對象,至於這些算法所要求的功能是通過各個對象來之間的交互實現的(這就像我們日常生活一樣,為了完成某一件事,我們需要和各種不同的人、物打交道,這些人和物就是對象,而打交道的過程就是交互)。因此在面向對象的思維方法中,我們並不是關注算法本身,而需要關注為了完成這個算法,我們需要怎么的“人”和“物”(即對象),之后再定義這些“人”和“物”之間的關系,從而明確它們是如何“打交道”的(即交互);把這個過程明確后,事就自然辦成了(即算法實現了)。
按照這種思維模式,我們再來看前面的篩選法求素數的問題是怎樣的一個過程。在這個算法中,我們看到了什么?
1)首先,我們看到了一堆需要處理的數字,這些數字構成數據源,這個數據源就是一個對象,針對這個對象即可抽象出一個類:篩子Seive(存儲數據源);
2)其次,我們看到了一個過濾因子,通過這個因子來篩選后面的數,這個因子就是一個對象,針對這個對象即可抽象出一個類:過濾器Factor(記錄當前的過濾因子);
3)此外,我們還看到一些東西,比如為了能夠對數據源進行遍歷,我們需要一個計數器來記錄當前正在訪問的數據值,這個計數器對象即可抽象成類:計數器Counter(記錄當前正在篩選的數據)。
具體的過程如下圖所示:
好了,到這我們基本上把所要求的對象都找出來啦,是不是工作就做完了?
沒有,如果僅到這一步還不是面向對象,這只不過是“基於對象”罷了,要做到真正的面向對象,還需要最關鍵的一步:抽象!這一步是面向對象領域最難的一步,這一步做好了我們程序(或軟件)才會獲得那些面向對象“廣告語”中所謂的面向對象各種好處(什么穩定性、復用性等等),否則這一切都是空談。至於這個例子如何進行抽象,這是一個非常復雜的問題,我們並不展開(關於抽象,后面會有專門的章節進行論述);我們直接看結果,下圖即為篩選法求素數問題的類圖:

 

在這個類圖中,除了前面所找出來的三個對象之外,我們還看到了一個新的類Item,這個類的類名是一個斜體字,表明它是一個抽象類;通過該抽象類為其成員函數out()提供了多態的特型。
通過上面的類圖即可描述我們所需要的類以及它們之間的關系;此外,為了能夠實現具體的算法,我們還需要通過UML中的交互圖描述它們之間的交互過程(關於交互過程的描述,后面章節會詳細論述),此處由於這個過程比較簡單,我們可以直接實現;下面是該算法的面向對象的實現(采用C++語法): 
//基類:Item
class Item{
public:
    Item* source;
    Item (Item* src) {source=src;}
    virtual int out() {return 0;}
};
//計數器類:Counter
class Counter: public Item{
    int value;
public:
    int out() {return value++;}
    Counter(int v):Item(0){value=v;}
};
//過濾器類:Filter
class Filter:public Item{
    int factor;
public:
    int out(){
        while(1){
            int n=source->out();
            if (n%factor) return n;
        }
    }
    Filter(Item *src, int f):Item(src) {factor=f;}
};
//篩子類:Sieve
class Sieve: public Item{
public:
    int out(){
        int n=source->out();
        source= new Filter(source, n);
        return n;
    }
    Sieve(Item *src):Item(src){}
};
//主函數,構造類的對象演繹應用場景
void main(){
    Counter c(2);
    Sieve s(&c);
    int next, n;
    cin>>n;
    while(1){
        next=s.out();//關鍵代碼只有一行,類知道自己的職責
        if(next>n) break;
        cout<<next<<" ";
    }
    cout<<endl;
}

5. 總結

看完面向對象的結果,我們有什么體會?
1)程序更復雜了!的確,原來40多行的程序,用面向對象的方法卻寫出了100多行。
2)程序更寫難了!原來的思路很清楚,但現在程序的結構卻不是簡簡單單就可以寫出來的,它需要我們經過一個復雜的分析和設計過程。
這是為什么呢?這樣做有必要嗎?我們的目標是什么?
軟件工程思想的出現是為了解決軟件危機,而軟件危機出現的原因並不是寫不出程序了,而是寫出來的程序無法修改、無法穩定運行!因為社會在進步,軟件的需求也在不斷的發展,這就要求我們的程序也能夠隨着需求的變化而變化,而傳統的方法是無法解決這些問題的。面向對象技術的出現就是為了解決變化的問題,使軟件能夠適應變化。而為了這個變化,就需要程序建立更合理、穩定的結構;程序也就不可避免的變得更加復雜;不過這種復雜性卻是很合理的,因為我們的現實世界本身就具有這種復雜性,面向對象在實現功能的時候,還在模擬着這個現實世界。
當然,用這樣一個算法問題來講解面向對象的優點是很愚蠢的!因為算法是很穩定的,它關注的是底層的實現,這些沒有必要也不會隨着需求而變化。我曾經問過計算機學院一位非常有名的數據結構老師,為什么到現在講解數據結構時還是用C語言來實現,而不是面向對象的數據結構呢?那位老師就說,因為數據結構關注的是底層算法,並不是程序的高層結構,用面向對象的方法使得程序更復雜,這樣必須投入更多地精力去關注程序結構,而不是數據結構,舍本逐末了。
正是因為面向對象技術有這種復雜性的存在,使得面向對象設計和開發的難度更大,因為我們將面臨着對象的識別、職責分配等一系列問題;而這就要求我們學習更多知識和技術,並掌握一系列面向對象的設計原則和模式,同時靈活利用各種圖形化工具(如UML)來幫助我們表達和交流設計思想,從而簡化設計和實現過程。

 by thbin
2006-10-18


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM