大綱:
可複用構件與軟件複用的關係
1.可複用構件的定義及特徵
構件是指應用系統中可以明確辨識的有機構成成分,它具有相對獨立性、互換性和功能性的特徵。可複用構件則是指具有相對獨立的功能和可複用價值的構件。
可複用構件應具備以下特徵:
- 有用性,必須提供有用的功能;
- 可用性,必須易於理解和使用;
- 可靠性 ,構件自身及其變形必須能正確工作;
- 適應性,應易於通過參數化等方式在不同語境中進行配置;
- 可移植性,能在不同的硬件平臺和軟件環境中工作。
隨着對軟件複用理解的深入,構件的概念已經不再侷限於源代碼構件,而是延伸到系統和軟件的需求規約、系統和軟件的構架、文檔測試計劃、測試案例和數據,以及其他對開發活動有用的信息,這些信息都可以稱爲可複用軟件構件。
2.軟件複用的核心是可複用構件
可複用構件技術是支持軟件複用的核心技術,是近幾年來迅速發展並受到高度重視的一個學科分支。隨着軟件產業工業化的發展,也許未來會出現這樣一種產業模式:構件生產商專門生產構件,然後再由零售商等構件管理者將它們拿到市場上銷售。系統集成商則根據自己應用系統的需求進行構件的採購、集成和組裝。
可複用構件的主要技術流派
目前,軟件構件技術流派主要有三種:
1.COM
COM構件實現規範由微軟提出,它使開發人員可以利用其中的通信機制,組裝不同開發商的構件。COM的核心是一組應用程序調用接口,該接口提供了創建構件組建構件的功能。爲支持網絡環境,微軟對COM進行了擴充,這就是DCOM。COM規範具有以下特點:
- 構件間的互操作基於指針進行,依賴於操作系統的API;
- 對 Windows的依賴性強,對其他操作系統支持相對不足;
- 構件運行環境的提供者僅限於微軟,但支持COM規範的開發工具較多,如VC+ +、VB和Builder等。
2.JavaBean
JavaBean構件實現規範是由SUN公司在Java語言的基礎上提出的。由於Java是一種純對象式語言,因此JavaBean構件規範比較完備、簡潔。JavaHBean規範具有以下特點:
- 構件模型比較完備;
- 僅支持Java語言;
- 構件運行環境有SUN支持,其他廠商也可提供運行環境。支持該模型的開發工具較多,如Visual Cafe、Visual age for Java等。
3.CORBA
CORBA實現規範由OMG提出。OMG首先發布了OMA(對象管理體系結構) ,提出了構件互操作的軟總線——ORB (Object Request Broker) ,並將構件分爲三類:公共對象服務COS、面向領域的公共設施CF和應用對象AF。CORBA規範具有以下特點:
- 構件間的複用以 ORB爲中介,對處理機、操作系統、語言的異構性支持性強;
- 充分借鑑JavaBean的構件原型;
- 提供構件運行環境的廠商較多,對開發支持的工具相對較少,通常需要傳統開發工具的支持等。
設計可複用的類
1.子類型多態和利斯科夫替換原則
子類型多態:客戶端可用統一的方式處理不同類型的對象,例如:
Animal a = new Animal();
Animal c1 = new Cat();
Cat c2 = new Cat();
在可以使用a的場景,都可以用c1和c2代替而不會有任何問題(但反過來不行,即不能用父類來實例化一個子類引用)
- java靜態類型檢查中的規則:
- 子類型可以增加方法,但不可刪父類原有的方法
- 子類型需要實現抽象類型中的所有未實現方法
- 子類型中重寫的方法必須有相同或子類型的返回值
- 子類型中重寫的方法必須使用同樣類型的參數(或該類型的父類型,但java中不支持,會自動默認爲重載)
- 子類型中重寫的方法不能拋出額外的異常
- 規約上的準則:
- 更強的不變量
- 更弱的前置條件
- 更強的後置條件
- 里氏替換原則的主要作用就是規範繼承時子類的一些書寫規則。其主要目的就是保持父類方法不被覆蓋。
含義:
子類必須完全實現父類的方法
子類可以有自己的個性
覆蓋或實現父類的方法時輸入參數可以被放大
覆蓋或實現父類的方法時輸出結果可以被縮小
利斯科夫替換原則本質是強行爲子類型化,其要求爲:- 前置條件不能強化
- 後置條件不能弱化
- 不變量要保持
- 子類型方法參數:逆變
- 子類型方法的返回值:協變
- 異常類型:協變
- 協變:子類型方法的返回值是其父類型方法的返回值的子類型,即子類型及其方法的返回值在各自的繼承樹上的相對位置是協同的
- 逆變:子類型參數的類型是其父類型方法的返回值的父類型,即子類型及其方法的參數在各自的繼承樹上的相對位置是相反的
- 逆變與協變綜述:如果A、B表示類型,f(⋅)表示類型轉換,≤表示繼承關係(比如,A≤B表示A是由B派生出來的子類):
f(⋅)是逆變(contravariant)的,當A≤B時有f(B)≤f(A)成立;
f(⋅)是協變(covariant)的,當A≤B時有f(A)≤f(B)成立;
f(⋅)是不變(invariant)的,當A≤B時上述兩個式子均不成立,即f(A)與f(B)相互之間沒有繼承關係。 - 協變(Co-variance):
父類型->子類型:越來越具體(specific)。
在LSP中,返回值和異常的類型:不變或變得更具體 。
例子:
- 逆變(Contra-variance):
父類型->子類型:越來越抽象。
參數類型:要相反的變化,不變或越來越抽象。
例子:
但這在Java中是不允許的,因爲它會使重載規則複雜化。
總結:
(1.子類型(屬性、方法)關係;2.不變性,重寫方法;3.協變,方法返回值變具體;4.逆變,方法參數變抽象;5.協變,參數變的更具體,協變不安全)
- java遇到逆變時,當作overload(重載)處理
- 實例:數組類型
數組是協變的:一個數組T[ ] ,可能包含了T類型的實例或者T的任何子類型的實例
即子類型的數組可以賦予父類型的數組進行使用,但數組的類型實際爲子類型。
下面報錯的原因是myNumber指向的還是一個Integer[] 而不是Number[]
Number[] numbers = new Number[2];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; //run-time error!
- 泛型中的LSP原則
Java中泛型是不變的,但可以通過通配符"?"實現協變和逆變:
<? extends>實現了泛型的協變:
List<? extends Number> list = new ArrayList<Integer>();
<? super>實現了泛型的逆變:
List<? super Number> list = new ArrayList<Object>();
由於泛型的協變只能規定類的上界,逆變只能規定下界,使用時需要遵循PECS(producer--extends, consumer-super):
要從泛型類取數據時,用extends;
要往泛型類寫數據時,用super;
既要取又要寫,就不用通配符(即extends與super都不用)。
泛型聲明時存在類型擦除,即尖括號裏面的內容只在編譯時被考慮,運行時不考慮
實例:
ArrayList<String> 是List<String>的子類型
List<String>不是List<Object>的子類型
簡單來說,尖括號裏面的類型和其它尖括號裏面的類型即使有直接繼承關係,但對於整個泛型是沒有繼承關係的
可以用通配符來解決泛型繼承的問題<?>,<? extends AClass>,<? super BClass>
2.委託和組合
2.1 Java排序實例
如果ADT需要比較大小,或者要放入Collections或Arrays進行排序,可實現Comparator接口並override compare()函數。
另一種方法:讓ADT實現Comparable接口,然後overridecompareTo() 方法。與使用Comparator的區別:不需要構建新的Comparator類,比較代碼放在ADT內部。
2.2 委託(Delegation)
委派/委託:一個對象請求另一個對象的功能,是複用的一種常見形式。
顯性委派:將發送對象傳遞給接收對象;
隱性委派:由語言的成員查找規則。
委託可以被描述爲在實體之間共享代碼和數據的低級機制
委派設計模式:是一種用來實現委派的軟件設計模式;
委託依賴於動態綁定,因爲它要求給定的方法調用可以在運行時調用不同的代碼段。
委派的過程如下:
Receiver對象將操作委託給Delegate對象,同時Receiver對象確保客戶端不會濫用委託對象;
委託和繼承的比較:
- 繼承:通過新操作擴展基類或覆蓋操作。
- 委託:把基類拿過來作爲新的類的元素,可以用基類以實現的功能擴展新類的功能
- 委託可以替代繼承的情況:如果子類只需要複用父類中的一小部分方法,可以不需要使用繼承,而是通過委派機制來實現
2.3 組合繼承原則
類應該通過它們的組合實現多態行爲和代碼重用(通過包含實現所需功能的其他類的實例),而不是從基類或父類繼承。
組合原則:
- 使用接口定義系統必須對外展示的不同側面的行爲,例如,一隻鳥可以叫也可以飛,那麼可以定義兩個接口,quackable和flyable。
- 接口之間通過extends實現行爲的擴展(接口組合)。然後可以定義一個接口birdable同時繼承了上述兩個接口,這樣這個新的接口就有了上述兩個接口的全部功能。
- 類implements 組合接口,從而規避了複雜的繼承關係。接下來,我們定義的“鳥類”就可以實現birdable,使得活動頂層接口的功能(在構造實例的過程中,同時要delegation頂層功能的接口)
2.3.1 Dependency: 臨時性的delegation
Dependency:對象需要其他對象(供應商)實現的臨時關係。
本質上就是通過方法的參數列表把被委託方傳進來,或者通過在方法中定義局部變量來產生委託方。臨時要用被委託方的方法,在被委託方中的field中並不保存。
實例:
class Course { }
class CourseSchedule {
List<Course> courses;
void ass (Course c) {...}
void remove (Course c) {...}
}
2.3.2 Association: 永久性的delegation
Association:對象類之間的持久關係,允許一個對象實例使另一個對象實例代表它執行操作。
被委託方常爲委託方的屬性
實例:
class Teacher {
private Student [] students;
}
class Student {
private Teacher teacher;
}
class Course {}
class Student {
Course[] selectedCourses;
}
2.3.3 Composition: 更強的delegation
Composition是一種將簡單對象或數據類型組合成更復雜的對象的方法。
它的特點是,被委託方初始化是在委託方的內部
實例:
class Heart {}
class Person {
private Heart heart = new Heart();
public void operation () {
heart.operation();
}
}
2.3.4 Aggregation
對象存在於另一個之外,在外部創建,它作爲參數傳遞給construtor
它的特點是,當委託方的對象被刪除後,被委託方的實例仍然存在
實例:
class Student {}
class Course {
private Student[] students;
public addStudent (Student s) {
students.append(s);
}
}
設計可複用的庫與框架
之所以library和framework被稱爲系統層面的複用,是因爲它們不僅定義了1個可複用的接口/類,而是將某個完整系統中的所有可複用的接口/類都實現出來,並且定義了這些類之間的交互關係、調用關係,從而形成了系統整體 的“架構”。
相應術語:
API(Application Programming Interface):庫或框架的接口
Client(客戶端):使用API的代碼
Plugin(插件):客戶端定製框架的代碼
Extension Point:框架內預留的“空白”,開發者開發出符合接口要求的代碼( 即plugin) , 框架可調用,從而相當於開發者擴展了框架的功能
Protocol(協議):API與客戶端之間預期的交互序列。
Callback(反饋):框架將調用的插件方法來訪問定製的功能。
Lifecycle method:根據協議和插件的狀態,按順序調用的回調方法。
API和庫
API是程序員最重要的資產和“榮耀”,吸引外部用戶,提高聲譽。
建議:始終以開發API的標準面對任何開發任務;面向“複用”編程而不是面向“應用”編程。
難度:要有足夠良好的設計,一旦發佈就無法再自由改變。
編寫一個API需要考慮以下方面:
API應該做一件事,且做得很好
API應該儘可能小,但不能太小
Implementation不應該影響API
記錄文檔很重要
考慮性能後果
API必須與平臺和平共存
類的設計:儘量減少可變性,遵循LSP原則
方法的設計:不要讓客戶做任何模塊可以做的事情,及時報錯
框架
框架(Framework)是整個或部分系統的可重用設計,表現爲一組抽象構件及構件實例間交互的方法;另一種定義認爲,框架是可被應用開發者定製的應用骨架。前者是從應用方面而後者是從目的方面給出的定義。
爲了增加代碼的複用性,可以使用委派和繼承機制。同時,在使用這兩種機制增加代碼複用的過程中,我們也相應地在不同的類之間增加了關係(委派或繼承關係)。而對於一個項目而言,各個不同類之間的依賴關係就可以看做爲一個框架。一個大規模的項目可能由許多不同的框架組合而成。
框架與設計模式:
框架、設計模式這兩個概念總容易被混淆,其實它們之間還是有區別的。構件通常是代碼重用,而設計模式是設計重用,框架則介於兩者之間,部分代碼重用,部分設計重用,有時分析也可重用。在軟件生產中有三種級別的重用:內部重用,即在同一應用中能公共使用的抽象塊;代碼重用,即將通用模塊組合成庫或工具集,以便在多個應用和領域都能使用;應用框架的重用,即爲專用領域提供通用的或現成的基礎結構,以獲得最高級別的重用性。
框架與設計模式雖然相似,但卻有着根本的不同。設計模式是對在某種環境中反覆出現的問題以及解決該問題的方案的描述,它比框架更抽象;框架可以用代碼表示,也能直接執行或複用,而對模式而言只有實例才能用代碼表示;設計模式是比框架更小的元素,一個框架中往往含有一個或多個設計模式,框架總是針對某一特定應用領域,但同一模式卻可適用於各種應用。可以說,框架是軟件,而設計模式是軟件的知識。
框架分爲白盒框架和黑盒框架。
白盒框架:
白盒框架是基於面向對象的繼承機制。之所以說是白盒框架,是因爲在這種框架中,父類的方法對子類而言是可見的。子類可以通過繼承或重寫父類的方法來實現更具體的方法。
雖然層次結構比較清晰,但是這種方式也有其侷限性,父類中的方法子類一定擁有,要麼繼承,要麼重寫,不可能存在子類中不存在的方法而在父類中存在。
軟件構造課程中有關白盒框架的例子:
public abstract class PrintOnScreen {
public void print() {
JFrame frame = new JFrame();
JOptionPane.showMessageDialog(frame, textToShow());
frame.dispose();
}
protected abstract String textToShow();
}
public class MyApplication extends PrintOnScreen {
@Override protected String textToShow() {
return "printing this text on " + "screen using PrintOnScreen " + "white Box Framework";
}
}
通過子類化和重寫方法進行擴展(使用繼承);
通用設計模式:模板方法;
子類具有主要方法但對框架進行控制。
允許擴展每一個非私有方法
需要理解父類的實現
一次只進行一次擴展
通常被認爲是開發者框架
黑盒框架:
黑盒框架時基於委派的組合方式,是不同對象之間的組合。之所以是黑盒,是因爲不用去管對象中的方法是如何實現的,只需關心對象上擁有的方法。
這種方式較白盒框架更爲靈活,因爲可以在運行時動態地傳入不同對象,實現不同對象間的動態組合;而繼承機制在靜態編譯時就已經確定好。
黑盒框架與白盒框架之間可以相互轉換,具體例子可以看一下,軟件構造課程中有關黑盒框架的例子,更改上面的白盒框架爲黑盒框架:
public interface TextToShow {
String text();
}
public class MyTextToShow implements TextToShow {
@Override
public String text() {
return "Printing";
}
}
public final class PrintOnScreen {
TextToShow textToShow;
public PrintOnScreen(TextToShow tx) {
this.textToShow = tx;
}
public void print() {
JFrame frame = new JFrame();
JOptionPane.showMessageDialog(frame, textToShow.text());
frame.dispose();
}
}
通過實現插件接口進行擴展(使用組合/委派);
常用設計模式:Strategy, Observer ;
插件加載機制加載插件並對框架進行控制。
允許在接口中對public方法擴展
只需要理解接口
通常提供更多的模塊
通常被認爲是終端用戶框架,平臺
結束語
採用基於軟件構件的軟件複用技術,將使軟件設計、生產工廠化成爲可能,是未來軟件開發的發展方向。軟件開發的可複用構件技術必將極大地提高軟件開發的勞動生產率,縮短應用軟件的開發週期,不僅軟件質量會更好,軟件的維護也會更加容易。因此,該技術一定會有良好的發展前景。