天天看点

面向对象的javascript系列文章(2)封装——信息隐藏

封装是为了实现在分析层面分离关注点的行为,目的是为了达到信息隐藏。

一、封装是分离关注点的一种实现方式

  对于一个复杂的系统来说,经过抽象之后,可以清楚的看到一个层次结构。对于每一层的抽象,都是由多个模块组成的,这些模块之间互相通信以便于为了上一层的抽象提供服务。一般来说,我们对于研究的每一层抽象,都是先在这个问题空间中找到与某个模块相关的所有特性开始的,对于如何将这些特性体现出是这个模块的?就是通过封装来实现。

比如如下需求:

      某学校的图书馆要引进一批新书,这些新书有索引编号、书名和作者,这些书是存放在图书馆中的。图书馆馆的功能就是存放这些书,并且能够找到一本书被读者去借。

  很显然,对于这么一个简单的图书管理系统来说,这个系统的层析结构并不是特别复杂,一个图书管理系统由图书馆、图书这几个模块/类组成。从中抽象出来的模块结构如下:

  图书:对于每一本图书都有索引编号(isbn)、名字(name)、作者名(author)组成,其中索引编号要满足一定的格式格式必须为12位的数字为唯一标识不能为空,名字和作者名由于存在某些原因可以为空,不存在唯一性。图书给图书馆这个模块提供一些信息:比如对于任意一本图书可以知道它的索引编号、名字、作者名或者同时显示。

  图书馆:可以存放很多本书,可以存放书查找书和被借书。

class Book                            class library

-------------------------                  -------------------------

isbn;必须有值                          books;默认为[]

name; 默认值为null

author;默认值为null

------------------------                  ------------------------

checkIsbn();                          putBook();

getIsbn();                           hasBook();

getName();                           borrowBook();

getAuthor();

setIsbn();

setName();

setAuthor();

display();

-------------------------                 -------------------------

  这样就把图书和图书馆这两个模块从纷杂的需求描述中分离出来了。对于如何去设计实现它呢接着往下看。

二、封装的目的是信息隐藏

  封装的目的是为了信息隐藏。如何去做到信息隐藏呢?就是对于通过分析得到的特性中那些属性私有化,然后对于方法来说有一部分方法只有内部这个模块所使用的,那也私有化,只有那些与处于同一抽象层的另一个模块交互的方法公有化,这些方法一般会通过接口的方式进行表达。

  信息隐藏通过属性私有化部分方法私有化公共方法接口化的实现方式,其目的就是实现模块的高度独立性和灵活性,并且不依赖于其他模块,从而实现较低的耦合度。从而使得对于单元测试等检测方式变得容易,提高了编写代码的效率。在接下来分别介绍如何实现的独立性灵活性和解耦。

2.1属性私有化

  一个模块的属性是这个模块/类的核心,因为就是这个模块中的方法基于这些属性与其他的模块进行交互。

     比如说,一个类中的属性就相当于鸡蛋中的蛋黄,蛋黄承载着孵化小鸡的关键任务是非常重要的,方法相当于蛋白,蛋白的作用就是为了蛋黄能孵化成小鸡提供充足的营养,如果一个鸡蛋的蛋黄流出那么这个鸡蛋肯定是不能孵化小鸡的。一个类的属性就和蛋黄一样如此的重要。其实任何一个软件系统都是对数据进行操作的。 

  如果属性公有的话,可能有一个模块对这个属性进行了修改,但是其他的模块并不知道,还去使用这个属性,那么必然会导致错误。对于其他模块只能增加对这个属性值判断的代码,无疑增加了代码的复杂度和重复程度。

  所以说属性私有化是保证数据完整性的一个很好的方式。对于这个拥有私有属性的模块来说,增加特权方法去操作这些属性,确保属性不会处于无效的状态,同时也方便了其他模块的使用。减少错误的产生。

  所谓的数据完整性就是指数据在赋值的时候要进行检测是否符合赋值要求,在取值的时候不允许对他修改,所以对于每一个属性(isbn、name、author)都有自己的特权方法(getIsbn、putIsbn……)

所以对于上述图书和图书馆的描述将变成如下形式:

class Book                            class library

-------------------------                  -------------------------

private isbn;必须有值                    private books;默认为[]

private name; 默认值为null

private author;默认值为null

------------------------                  ------------------------

checkIsbn();                          putBook();

getIsbn();                           hasBook();

getName();                           borrowBook();

getAuthor();

setIsbn();

setName();

setAuthor();

display();

-------------------------                 -------------------------

属性私有化保证了一个类的数据的完整性,从而使用这个属性的其他类们可以放心的去使用没有必要去添加一些检测这些属性的代码,从而使得当这些属性完整性条件发生改变的时候可以仅仅改变自己这个类其他类不用修改提高了类的独立性降低了耦合度。

2.2部分方法私有化

  一个模块的所有行为无外乎两种,一种就是为其他的模块提供服务,一种就是为自身服务。

  我们可以将那些为自身服务的方法私有化,避免了外部模块对这个模块的干扰,减少了外部模块对该模块的依赖,从而降低耦合度。

class Book                            class library

-------------------------                  -------------------------

private isbn;必须有值                     private books;默认为[]

private name; 默认值为null

private author;默认值为null

------------------------                  ------------------------

private checkIsbn();                    putBook();

getIsbn();                           hasBook();

getName();                           borrowBook();

getAuthor();

setIsbn();

setName();

setAuthor();

display();

-------------------------                 -------------------------

2.3公共方法接口化

  其他的方法为接口的形式,因为这些方法要和其他模块打交道,所以就要提供一种契约后或者说一种标准。当然也可以不选择接口的形式,仅仅是将这个方法公有就好了,但是问题是我这个模块可能会对这个共有的方法进行修改,特别是方法名和参数,如果使用这些公共方法的模块没有修改势必产生没有找到的错误,所以说为了统一标准,要使用接口的形式,当然接口仅仅是一种设计理念实现方式完全可以是最简单的注释的形式。

  所以,对于方法来说,一个模块仅仅是对外公布那些接口的方法,如果一个模块改变了实现方式或者算法,只要实现了接口,其他的模块是感知不到这个改变的。

class Book

-------------------------------

private isbn;必须有值

private name; 默认值为null

private author;默认值为null

-------------------------------

private checkIsbn();

public getIsbn();

public getName();

public getAuthor();

public setIsbn();

public setName();

public setAuthor();

public display();

-------------------------------

interface  publicActionOfBook

-------------------------------

getIsbn();

getName();

getAuthor();

setIsbn();

setName();

setAuthor();

display();

------------------------------

class library

-------------------------

private books;默认为null

-------------------------

private putBook();

public hasBook();

public borrowBook();

-------------------------

方法私有化和公共方法接口化可以使得类的独立性更高并且更加灵活,比如当修改类中某些方法的算法的时候或者甚至在满足接口的情况下改变数据结构都是可以得,因为外部的类仅仅是通过接口和这个类打交道,所以实现两个类或者多个类之间的解耦。

三、javascript中实现封装的两种方式

3.1使用命名规范来实现封装

// 对于接口来说有几种实现方式我们在这里采用注释的形式
// 当然可以使用interface类的方式定义一个接口正如上一篇博客定义的接口方法
/*
interface publicActionOfBook{
	getIsbn();
	getName();
	getAuthor();
	setIsbn();
	setName();
	setAuthor();
	display();
}
 */
function Book(){
	this._isbn;
	this._name;
	this._author;
}
Book.prototype._checkIsbn = function(){
	//在这里检测Book对象的isbn属性的值是否满足一定的条件
	//也就是说是否是只有12个数字
}
Book.prototype.setIsbn= function(isbn){
	if(this._checkIsbn(isbn)){/*必须有唯一的值*/
		this._isbn = isbn;
	}else{
		//抛出异常
	}

}
Book.prototype.getIsbn= function(){
	return this._isbn;
}
Book.prototype.setName = function(name){
	this._name = name || "this book no have name";/*如果没有值则为默认值*/
};
Book.prototype.getName = function(){return this._name;};
......
Book.prototype.display = function(){return this._isbn+this._name+this._author;};

function Library(){
	this._books = [];
}
Library.prototype.putBook =  function (book){
	//必须确认book实现了接口publicActionOfBook
	this._books.push(book.display());
}
Library.prototype.hasBook =  function(){};
....
           

  通过这种方式封装一个类,对于私有的属性和方法在属性名前用下划线标识,除非程序员遵循这一标准非常自觉,否则也是会污染属性的完整性,也就是说对于私有化并没有完全的实现私有。

  这个方式如下好处(一)没有使用javascript闭包作用域这些特征,是的运行起来比较高效,内存占用良好。(二)对于通过继承而来的子类来说,对于私有的方法也是可以集成的,碎玉接下来介绍的这种方法并没有实现这种功能。

3.2使用js一些语法特性来实现封装

var Book = (function(){
	// 类属性和类方法
	//这是一个常量类属性,这个属性限制创建Book对象的个数
	var BOOKNUM = 100;

	var Book = function(newIsbn,newName,newAuthor){
		var isbn,name,author;

		function checkIsbn = function(){
			//在这里检测Book对象的isbn属性的值是否满足一定的条件
			//也就是说是否是只有12个数字
		};
		Book.prototype.setIsbn= function(isbn){
			if(checkIsbn(isbn)){/*必须有唯一的值*/
			isbn = isbn;
			}else{
			//抛出异常
			}
		};
		Book.prototype.getIsbn= function(){
			return isbn;
		};
		Book.prototype.setName = function(name){
			name = name || "this book no have name";/*如果没有值则为默认值*/
		};
		Book.prototype.getName = function(){return this._name;};
		......
		Book.prototype.display = function(){return isbn+name+author;};

		// 初始化
		if(!checkBookNum()){
			BOOKNUM--;
			this.setIsbn(newIsbn);
			this.setName(newName);
			this.setAuthor(newAuthor);
		}
	}
	//类方法,检测是否创建了多余100个book类
	function checkBookNum(){
		if(BOOKNUM<=0){
			throw new Error("最多创建100个Book对象");
		}
		return true;
	}
	return Book;
})();

var Library = (function(){
	var Library = function (){
		var books = [];
		Library.prototype.putBook =  function (book){
			//必须确认book实现了接口publicActionOfBook
			books.push(book.display());
		}
		Library.prototype.hasBook =  function(){};
		....
	}
	
	return Library;
})();

var b1 = new Book("12345678912","javascript","BOOCH");
var l1 = new Library();
l1.putBook(b1);
           

   这种方式实现了真正意义上的类的封装,实现了属性和某些方法的私有化,同时某些公共方法的接口化,湿的即使改变book内部的数据结构形式,只要满足相应的接口格式的话,那么Library是不会去修改的。实现了Book类的独立性和模块化,减少了Book和Library的依赖,以至于实现解耦。

  但是这种实现方式存在一个致命的问题就是性能,因为这种独立性的实现是依托于javascript闭包和作用域链这种语法格式的,这种创建闭包的形式本身是很消耗性能和内存的。

  一般是使用第一种方式通过编程人员的自觉性实现实例属性和方法的私有化,通过第二种方式实现类属性和类方法的私有化,或者实现常量的形式。

四、最终补充说明的事情

4.1实现某些功能性的函数比如toString、valueOf等。

  对于一般的对象在进行加减法或者比较运算的时候,会将操作数进行转化,可能转化成字符串类型或者是数字类型。以便于能够济宁相应的操作。

  一般情况下,对于操作对象是对象类型,如果这个位置需要一个数字的话,那么就先调用valueOf如果这个结果通过转化成数字后还不是一个数字类型的话,那么在调用toString最终返回这个值,如果这个位置需要一个字符串的话,则先调用toString在调用valueof,如果不告诉什么类型比如+,那么会默认按照数字的形式进行处理。

  所以说主动声明tostring和valueof方法很重要。

4.2方法借用

因为使用接口过来规范类的接口,所以会存在相同类似的类或者是不同类型的但是实现了这个接口的类中对这个接口定义的类的实现可能会和我将要定义的类的该接口实现相同那么我就可以去借用了。

如下是最终实现版本:

// 对于接口来说有几种实现方式我们在这里采用注释的形式
// 当然可以使用interface类的方式定义一个接口正如上一篇博客定义的接口方法
/*
interface publicActionOfBook{
	getIsbn();
	getName();
	getAuthor();
	setIsbn();
	setName();
	setAuthor();
	display();
}
 */
var Book = (function(){
	//这是一个常量类属性,这个属性限制创建Book对象的个数
	var BOOKNUM = 100;

	//类方法,检测是否创建了多余100个book类
	function checkBookNum(){
		if(BOOKNUM<=0){
			throw new Error("最多创建100个Book对象");
		}
		return true;
	}
	function Book(newIsbn,newName,newAuthor){
		
		//实现初始化
		if(checkBookNum()){
			this._isbn;
			this._name;
			this._author;

			this.setIsbn(newIsbn);
			this.setName(newName);
			this.setAuthor(newAuthor);
		}
		
	};
	Book.prototype._checkIsbn = function(){
		//在这里检测Book对象的isbn属性的值是否满足一定的条件
		//也就是说是否是只有12个数字
		
	};
	Book.prototype.setIsbn= function(isbn){
		if(this._checkIsbn(isbn)){/*必须有唯一的值*/
			this._isbn = isbn;
		}else{
			//抛出异常
		}
	};
	Book.prototype.getIsbn= function(){
		return this._isbn||;
	};
	Book.prototype.setName = function(name){
		this._name = name || "this book no have name";/*如果没有值则为默认值*/
	};
	Book.prototype.getName = function(){
		return this._name;
	};
	......
	Book.prototype.display = function(){
		return this._isbn+this._name+this._author;
	};
	Book.prototype.display.toString = Book.prototype.display;
	Book.prototype.display.valueof = function(){
		//方法借用
		return Object.prototype.valueof.call();
	}
	return Book;
})();
var Library = (function(){
	var Library = function(){
		this._books = [];
	}
	Library.prototype.putBook =  function (book){
		//必须确认book实现了接口publicActionOfBook
		一定要保证接口的实现,切记,否则可能耦合度变高
		this._books.push(book.display());
	}
	Library.prototype.hasBook =  function(){};
	Library.prototype.toString = function(){
		return Array.prototype.join.call(this._books,",");
	};
	Library.prototype.valueOf = Library.prototype.toString;
	....
return Library;
})();
           

继续阅读