1. 程式人生 > >子類的定義,與組合的比較

子類的定義,與組合的比較

在面向物件程式設計中,類B可以繼承自另外一個類A。我們將A稱為父類(superclass),將B稱為子類(subclass)。B的例項從A繼承了所有的例項方法。類B可以定義自己的例項方法,有些方法可以過載類A中的同名方法,如果B的方法過載了A中的方法,B中的過載方法可能會呼叫A中的過載方法,這種做法稱為“方法鏈”(method chaining)。同樣,子類的建構函式B()有時需要呼叫父類的建構函式A(),這種做法稱為“建構函式鏈”(constructor chaining)。子類還可以有子類,當涉及類的層次結構時,往往需要定義抽象類(abstract class)。抽象類中定義的方法沒有實現。抽象類中的抽象方法是在抽象類的具體子類中實現的。

在JavaScript中建立子類的關鍵之處在於,採用合適的方法對原型物件進行初始化。如果類B繼承自類A,B.prototype必須是A.prototype的後代。B的例項繼承自B.prototype,後者同樣也繼承自A.prototype。此外還會介紹類繼承的替代方案:“組合”(composition)。

  • 定義子類

JavaScript的物件可以從類的原型物件中繼承屬性(通常繼承的是方法)。如果O是類B的例項,B是A的子類,那麼O也一定從A中繼承了屬性。為此,首先要確保B的原型物件繼承自A的原型物件。通過inherit()函式,可以這樣來實現:

B.prototype=inherit(A.prototype);//子類派生自父類
B.prototype.constructor=B;//過載繼承來的constructor屬性

這兩行程式碼是在JavaScript中建立子類的關鍵。如果不這樣做,原型物件僅僅是一個普通物件,它只繼承自Object.prototype,這意味著你的類和所有的類一樣是Object的子類。如果將這兩行程式碼新增至defineClass()函式中,可以將它變成例9-11中的defineSubclass()函式和Function.prototype.extend()方法:

例9-11:定義子類

//用一個簡單的函式建立簡單的子類
function defineSubclass(superclass,//父類的建構函式
                        constructor,//新的子類的建構函式
                        methods,//例項方法:複製至原型中
                        statics)//類屬性:複製至建構函式中
{
//建立子類的原型物件
    constructor.prototype=inherit(superclass.prototype);
    constructor.prototype.constructor=constructor;//像對常規類一樣複製方法和類屬性
    if(methods)extend(constructor.prototype,methods);
    if(statics)extend(constructor,statics);//返回這個類
    return constructor;
}
//也可以通過父類建構函式的方法來做到這一點
Function.prototype.extend=function(constructor,methods,statics){
    return defineSubclass(this,constructor,methods,statics);
};

例9-12展示了不使用defineSubclass()函式如何“手動”實現子類。這裡定義了Set的子類SingletonSet。SingletonSet是一個特殊的集合,它是隻讀的,而且含有單獨的常量成員。

例9-12:SingletonSet:一個簡單的子類

//建構函式
function SingletonSet(member){
    this.member=member;//記住集合中這個唯一的成員
}
//建立一個原型物件,這個原型物件繼承自Set的原型
SingletonSet.prototype=inherit(Set.prototype);//給原型新增屬性
//如果有同名的屬性就覆蓋Set.prototype中的同名屬性
extend(SingletonSet.prototype,{//設定合適的constructor屬性
    constructor:SingletonSet,//這個集合是隻讀的:呼叫add()和remove()都會報錯
    add:function(){throw"read-only set";},
    remove:function(){throw"read-only set";},//SingletonSet的例項中永遠只有一個元素
    size:function(){return 1;},//這個方法只調用一次,傳入這個集合的唯一成員
    foreach:function(f,context){f.call(context,this.member);},//contains()方法非常簡單:
    只須檢查傳入的值是否匹配這個集合唯一的成員即可
    contains:function(x){return x===this.member;}
});

這裡的SingletonSet類是一個比較簡單的實現,它包含5個簡單的方法定義。它實現了5個核心的Set方法,但從它的父類中繼承了toString()、toArray()和equals()方法。定義子類就是為了繼承這些方法。比如,Set類的equals()方法用來對Set例項進行比較,只要Set的例項包含size()和foreach()方法,就可以通過equals()比較。因為SingletonSet是Set的子類,所以它自動繼承了equals()的實現,不用再實現一次。當然,如果想要最簡單的實現方式,那麼給SingletonSet類定義它自己的equals()版本會更高效一些:

SingletonSet.prototype.equals=function(that){
return that instanceof Set&&that.size()==1&&that.contains(this.member);
};

需要注意的是,SingletonSet不是將Set中的方法列表靜態地借用過來,而是動態地從Set類繼承方法。如果給Set.prototype新增新的方法,Set和SingletonSet的所有例項就會立即擁有這個方法(假定SingletonSet沒有定義與之同名的方法)。

  • 建構函式和方法鏈

最後一節的SingletonSet類定義了全新的集合實現,而且將它繼承自其父類的核心方法全部替換。然而定義子類時,我們往往希望對父類的行為進行修改或擴充,而不是完全替換掉它們。為了做到這一點,建構函式和子類的方法需要呼叫或連結到父類建構函式和父類方法。

例9-13對此做了展示。它定義了Set的子類NonNullSet,它不允許null和undefined作為它的成員。為了使用這種方式對成員做限制,NonNullSet需要在其add()方法中對null和undefined值做檢測。但它需要完全重新實現一個add()方法,因此它呼叫了父類中的這個方法。注意,NonNullSet()建構函式同樣不需要重新實現,它只須將它的引數傳入父類建構函式(作為函式來呼叫它,而不是通過建構函式來呼叫),通過父類的建構函式來初始化新建立的物件。

例9-13:在子類中呼叫父類的建構函式和方法

/*
*NonNullSet是Set的子類,它的成員不能是null和undefined
*/
function NonNullSet(){//僅連結到父類
//作為普通函式呼叫父類的建構函式來初始化通過該建構函式呼叫建立的物件
    Set.apply(this,arguments);
}
//將NonNullSet設定為Set的子類
NonNullSet.prototype=inherit(Set.prototype);
NonNullSet.prototype.constructor=NonNullSet;//為了將null和undefined排除在外,只須重寫add()方法
NonNullSet.prototype.add=function(){//檢查引數是不是null或undefined
    for(var i=0;i<arguments.length;i++)
    if(arguments[i]==null)
        throw new Error("Can't add null or undefined to a NonNullSet");//呼叫父類的add()方法以執行
    實際插入操作
    return Set.prototype.add.apply(this,arguments);
};

讓我們將這個非null集合的概念推而廣之,稱為“過濾後的集合”,這個集合中的成員必須首先傳入一個過濾函式再執行新增操作。為此,定義一個類工廠函式(類似例9-7中的enumeration()函式),傳入一個過濾函式,返回一個新的Set子類。實際上,可以對此做進一步的通用化的處理,定義一個可以接收兩個引數的類工廠:子類和用於add()方法的過濾函式。這個工廠方法稱為filteredsetSubclass(),並通過這樣的程式碼來使用它:

//定義一個只能儲存字串的"集合"類
var StringSet=filteredSetSubclass(Set,function(x){return typeof x==="string";});//這個集合
類的成員不能是null、undefined或函式
var MySet=filteredSetSubclass(NonNullSet,function(x){return typeof x!=="function";});

例9-14是這個類工廠函式的實現程式碼。注意,這個例子中的方法鏈和建構函式鏈和NonNullset中的實現是一樣的。

例9-14:類工廠和方法鏈

/*
*這個函式返回具體Set類的子類
*並重寫該類的add()方法用以對新增的元素做特殊的過濾
*/
function filteredSetSubclass(superclass,filter){
    var constructor=function(){//子類建構函式
        superclass.apply(this,arguments);//呼叫父類建構函式
    };
    var proto=constructor.prototype=inherit(superclass.prototype);
    proto.constructor=constructor;
    proto.add=function(){//在新增任何成員之前首先使用過濾器將所有引數進行過濾
        for(var i=0;i<arguments.length;i++){
            var v=arguments[i];
            if(!filter(v))throw("value"+v+"rejected by filter");
        }
//呼叫父類的add()方法
        superclass.prototype.add.apply(this,arguments);
    };
    return constructor;
}

例9-14中一個比較有趣的事情是,用一個函式將建立子類的程式碼包裝起來,這樣就可以在建構函式和方法鏈中使用父類的引數,而不是通過寫死某個父類的名字來使用它的引數。也就是說如果想修改父類,只須修改一處程式碼即可,而不必對每個用到父類類名的地方都做修改。已經有充足的理由證明這種技術的可行性,即使在不是定義類工廠的場景中,這種技術也是值得提倡使用的。比如,可以這樣使用包裝函式和例9-11的Function.prototype.extend()方法來重寫NonNullSet:

var NonNullSet=(function(){//定義並立即呼叫這個函式
    var superclass=Set;//僅指定父類
    return superclass.extend(
        function(){superclass.apply(this,arguments);},//建構函式
        {//方法
            add:function(){//檢查引數是否是null或undefined
                for(var i=0;i<arguments.length;i++)
                if(arguments[i]==null)
                    throw new Error("Can't add null or undefined");//呼叫父類的add()方法以執行實際插入操作
                return superclass.prototype.add.apply(this,arguments);
            }
        });
}());

最後,值得強調的是,類似這種建立類工廠的能力是JavaScript語言動態特性的一個體現,類工廠是一種非常強大和有用的特性,這在Java和C++等語言中是沒有的。

  • 組合vs子類

定義的集合可以根據特定的標準對集合成員做限制,而且使用了子類的技術來實現這種功能,所建立的自定義子類使用了特定的過濾函式來對集合中的成員做限制。父類和過濾函式的每個組合都需要建立一個新的類。

然而還有另一種更好的方法來完成這種需求,即面向物件程式設計中一條廣為人知的設計原則:“組合優於繼承”。這樣,可以利用組合的原理定義一個新的集合實現,它“包裝”了另外一個集合物件,在將受限制的成員過濾掉之後會用到這個(包裝的)集合物件。例9-15展示了其工作原理:

例9-15:使用組合代替繼承的集合的實現

/*
*實現一個FilteredSet,它包裝某個指定的"集合"物件,
*並對傳入add()方法的值應用了某種指定的過濾器
*"範圍"類中其他所有的核心方法延續到包裝後的例項中
*/
var FilteredSet=Set.extend(
    function FilteredSet(set,filter){//建構函式
        this.set=set;
        this.filter=filter;
    },
    {//例項方法
        add:function(){//如果已有過濾器,直接使用它
            if(this.filter){
                for(var i=0;i<arguments.length;i++){
                    var v=arguments[i];
                    if(!this.filter(v))
                        throw new Error("FilteredSet:value"+v+"rejected by filter");
                }
            }
//呼叫set中的add()方法
            this.set.add.apply(this.set,arguments);
            return this;
        },//剩下的方法都保持不變
        remove:function(){
            this.set.remove.apply(this.set,arguments);
            return this;
        },
        contains:function(v){return this.set.contains(v);},
        size:function(){return this.set.size();},
        foreach:function(f,c){this.set.foreach(f,c);}
    });


在這個例子中使用組合的一個好處是,只須建立一個單獨的FilteredSet子類即可。可以利用這個類的例項來建立任意帶有成員限制的集合例項。比如,不用上文中定義的NonNullSet類,可以這樣做:

var s=new FilteredSet(new Set(),function(x){return x!==null;});
甚至還可以對已經過濾後的集合進行過濾:

var t=new FilteredSet(s,{function(x){return!(x instanceof Set);}};

  • 類的層次結構和抽象類

之前給出了“組合優於繼承”的原則,但為了將這條原則闡述清楚,建立了Set的子類。這樣做的原因是最終得到的類是Set的例項,它會從Set繼承有用的輔助方法,比如toString()和equals()。儘管這是一個很實際的原因,但不用建立類似Set類這種具體類的子類也可以很好的用組合來實現“範圍”。例9-12中的SingletonSet類可以有另外一種類似的實現,這個類還是繼承自Set,因此它可以繼承很多輔助方法,但它的實現和其父類的實現完全不一樣。SingletonSet並不是Set類的專用版本,而是完全不同的另一種Set。在類層次結構中SingletonSet和Set應當是兄弟的關係,而非父子關係。

不管是在經典的面向物件程式語言中還是在JavaScript中,通行的解決辦法是“從實現中抽離出介面”。假定定義了一個AbstractSet類,其中定義了一些輔助方法比如toString(),但並沒有實現諸如foreach()的核心方法。這樣,實現的Set、SingletonSet和FilteredSet都是這個抽象類的子類,FilteredSet和SingletonSet都不必再實現為某個不相關的類的子類了。

例9-16在這個思路上更進一步,定義了一個層次結構的抽象的集合類。AbstractSet只定義了一個抽象方法:contains()。任何類只要“聲稱”自己是一個表示範圍的類,就必須至少定義這個contains()方法。然後,定義AbstractSet的子類AbstractEnumerableSet。這個類增加了抽象的size()和foreach()方法,而且定義了一些有用的非抽象方法(toString()、toArray()、equals()等),AbstractEnumerableSet並沒有定義add()和remove()方法,它只代表只讀集合。SingletonSet可以實現為非抽象子類。最後,定義了AbstractEnumerableSet的子類AbstractWritableSet。這個final抽象集合定義了抽象方法add()和remove(),並實現了諸如union()和intersection()等非具體方法,這兩個方法呼叫了add()和remove()。AbstractWritableSet是Set和FilteredSet類相應的父類。但這個例子中並沒有實現它,而是實現了一個新的名叫ArraySet的非抽象類。

例9-16中的程式碼很長,但還是應當完整地閱讀一遍。注意這裡用到了Function.prototype.extend()作為建立子類的快捷方式。

例9-16:抽象類和非抽象Set類的層次結構

//這個函式可以用做任何抽象方法,非常方便
function abstractmethod(){throw new Error("abstract method");}/*
*AbstractSet類定義了一個抽象方法:contains()
*/
function AbstractSet(){throw new Error("Can't instantiate abstract classes");}
AbstractSet.prototype.contains=abstractmethod;/*
*NotSet是AbstractSet的一個非抽象子類
*所有不在其他集合中的成員都在這個集合中
*因為它是在其他集合是不可寫的條件下定義的
*同時由於它的成員是無限個,因此它是不可列舉的
*我們只能用它來檢測元素成員的歸屬情況
*注意,我們使用了Function.prototype.extend()方法來定義這個子類
*/
var NotSet=AbstractSet.extend(
    function NotSet(set){this.set=set;},
    {
        contains:function(x){return!this.set.contains(x);},
        toString:function(x){return"~"+this.set.toString();},
        equals:function(that){
            return that instanceof NotSet&&this.set.equals(that.set);
        }
    }
);/*
*AbstractEnumerableSet是AbstractSet的一個抽象子類
*它定義了抽象方法size()和foreach()
*然後實現了非抽象方法isEmpty()、toArray()、to[Locale]String()和equals()方法
*子類實現了contains()、size()和foreach(),這三個方法可以很輕易地呼叫這5個非抽象方法
*/
var AbstractEnumerableSet=AbstractSet.extend(
    function(){throw new Error("Can't instantiate abstract classes");},
    {
        size:abstractmethod,
        foreach:abstractmethod,
        isEmpty:function(){return this.size()==0;},
        toString:function(){
            var s="{",i=0;
            this.foreach(function(v){
                if(i++>0)s+=",";
                s+=v;
            });
            return s+"}";
        },
        toLocaleString:function(){
            var s="{",i=0;
            this.foreach(function(v){
                if(i++>0)s+=",";
                if(v==null)s+=v;//null和undefined
                else s+=v.toLocaleString();//其他的情況
            });
            return s+"}";
        },
        toArray:function(){
            var a=[];
            this.foreach(function(v){a.push(v);});
            return a;
        },
        equals:function(that){
            if(!(that instanceof AbstractEnumerableSet))return false;//如果它們的大小不同,則它們不相等
            if(this.size()!=that.size())return false;//檢查每一個元素是否也在that中
            try{
                this.foreach(function(v){if(!that.contains(v))throw false;});
                return true;//所有的元素都匹配:集合相等
            }catch(x){
                if(x===false)return false;//集合不相等
                throw x;//發生了其他的異常:重新丟擲異常
            }
        }
    });/*
*SingletonSet是AbstractEnumerableSet的非抽象子類
*singleton集合是隻讀的,它只包含一個成員
*/
var SingletonSet=AbstractEnumerableSet.extend(
    function SingletonSet(member){this.member=member;},
    {
        contains:function(x){return x===this.member;},
        size:function(){return 1;},
        foreach:function(f,ctx){f.call(ctx,this.member);}
    }
);/*
*AbstractWritableSet是AbstractEnumerableSet的抽象子類
*它定義了抽象方法add()和remove()
*然後實現了非抽象方法union()、intersection()和difference()
*/
var AbstractWritableSet=AbstractEnumerableSet.extend(
    function(){throw new Error("Can't instantiate abstract classes");},
    {
        add:abstractmethod,
        remove:abstractmethod,
        union:function(that){
            var self=this;
            that.foreach(function(v){self.add(v);});
            return this;
        },
        intersection:function(that){
            var self=this;
            this.foreach(function(v){if(!that.contains(v))self.remove(v);});
            return this;
        },
        difference:function(that){
            var self=this;
            that.foreach(function(v){self.remove(v);});
            return this;
        }
    });/*
*ArraySet是AbstractWritableSet的非抽象子類
*它以陣列的形式表示集合中的元素
*對於它的contains()方法使用了陣列的線性查詢
*因為contains()方法的演算法複雜度是O(n)而不是O(1)
*它非常適用於相對小型的集合,注意,這裡的實現用到了ES5的陣列方法indexOf()和forEach()
*/
var ArraySet=AbstractWritableSet.extend(
    function ArraySet(){
        this.values=[];
        this.add.apply(this,arguments);
    },
    {
        contains:function(v){return this.values.indexOf(v)!=-1;},
        size:function(){return this.values.length;},
        foreach:function(f,c){this.values.forEach(f,c);},
        add:function(){
            for(var i=0;i<arguments.length;i++){
                var arg=arguments[i];
                if(!this.contains(arg))this.values.push(arg);
            }
            return this;
        },
        remove:function(){
            for(var i=0;i<arguments.length;i++){
                var p=this.values.indexOf(arguments[i]);
                if(p==-1)continue;
                this.values.splice(p,1);
            }
            return this;
        }
    }
);