node.js中stream流中可读流和可写流的实现与使用方法实例分析

来源:脚本之家  责任编辑:小易  

那是应该是视频文件,仅仅是存放在你的光盘上,只能用你自己的电脑才能观看。

本文实例讲述了node.js中stream流中可读流和可写流的实现与使用方法。分享给大家供大家参考,具体如下:

WriteByte这个函数好像没见过你的目的就是将二进制数据写入文件吧,这个用以下几个文件操作函

node.js中的流 stream 是处理流式数据的抽象接口。node.js 提供了很多流对象,像http中的request和response,和 process.stdout 都是流的实例。

考试,对于大多数人来说,多少有些可怕,它意味着考前要挑灯夜战,考时会肚子抽筋、额头冒汗、两腿发抖、心

流可以是 可读的,可写的,或是可读可写的。所有流都是 events 的实例。

txt文件要用文本流打开。如果想用字节流读取的话需要知道字符集的;如果连字符集都不清楚那只能分析by

一、流的类型

语言是交际的工具,是文化最重要的载体。语文教学目的在于培养学生以语文能力为核心的语文素质,提高正确理

node.js中有四种基本流类型:

跟许嵩以往的抒情歌相比,这首歌里没有自嘲,没有愤恨,没有对抛弃自己的人的指责,没有对过去的悔恨,更多的是对自己现在生活的轻描淡写:我还留着老旧的相片,偶尔“闲下来“的时候拿出来翻一翻;还养着你走时留下的小狗,它没缘由地变得心事重重、闷闷不乐,其实你忘了带走的又何止它一个;还会做梦梦见你,恍惚中回到了从前,可醒来的那一刹那仿佛又失去你一遍;虽然你已离开好久,可是熟悉的场景总是不经意间让我想起你,想起你的笑,想起你唱给我的歌,想起你讲的冷笑话,回忆越是深刻越是伤人的。不得不说,这种故作平静的独白,更容易憋出内伤,戳中泪点了。因为彼此深爱过,所以没有再联络。不挽回,因为勉强的感情不会有结果。也许,成

1、Writable 可写流 (例:fs.createWriteStream() )

香菜做为评价两极分化最严重的食材之一。不喜欢它的人对它嗤之以鼻,甚至有人表示香菜的味道闻起来像是这个香菜英文名字的词根是“臭虫”可想而知它的味道对于某些人来说有多可怕。不过,根据科学家的研究表示,不爱吃香菜完全是因为基因导致的而全世界大约有15%的人是讨厌香菜的。经科学研究发现,主要是因为这类人群有“OR6A2”这个嗅觉基因,这种基因能够很敏锐地接收到香菜里的醛类物质,也就是香菜特殊味道的来源。这个“OR6A2”离我们生活中肥皂的味道特别接近,所以天生不爱吃香菜的人。就直接感觉香菜有一股肥皂的味道,这让他们觉得香菜闻起来的味道很恶心。甚至世界上还有人成立了“反香菜联盟”如今人数已经超过5

2、Readable 可读流 (例:fs.createReadStream() )

?梅艳芳的旗袍。一般分演唱会和电影里出现的。02、03演唱会的这两件都是来自dior赞助,02更是dior首席设计师亲自出马做的,梅艳芳作为亚洲第一位接受dior赞助演唱会服装的艺人,说明了dior对梅艳芳的肯定。梅艳芳唱《似是故人来》这首歌必穿旗袍,这两个演唱会也不例外,02年极梦幻演唱会一头至今都不过时的白色莫西干发型,搭配这身旗袍简直了,很想好好夸夸,但是词穷了,因为从没想过旗袍还能这么搭配穿。03经典金曲演唱会的比较复古点,因为病情严重,加上化疗,需要戴假发,腰围也一再改宽,甚至需要借助沙发来缓冲,但是梅艳芳依旧超常发挥,那场声音也许不是最好的时候,但是感情依旧真。?????88年梅艳

3、Duplex 可读又可写流 (例:net.Socket )

安全感,官方一点来说就是,渴望稳定、安全的心理需求。恋爱的时候,双方一般都会希望从对方的身上,获得这种感觉。有的人天生没有安全感,对自己的男/女朋友经常患得患失。说几个我觉得安全感比较重要的点。1、依赖。主要是情感和思想上的依赖,比如说,没有对方不知道怎么过了,还有很多人都潜意识里认为爱情是对方拼命对自己好,不管自己有多任性,多无耻,对方依然会瞎了眼般的对自己好。但是却忘记了,爱情里是要双方给予和付出的,自己也要给对方足够的爱。2、独立。不管你有多爱对方,也不要失去自我,放弃自己的学业、事业。有一份除了爱情以外可以让你很投入的事情,也是独立的表现。因为这个事情可以带给你比爱情还要大的安全感,就

4、Transform 读写过程中可修改或转换数据的 Duplex 流 (例:zlib.createDeflate() )

二、流中的数据有两种模式

1、二进制模式,都是 string字符串  和 Buffer。

2、对象模式,流内部处理的是一系统普通对象。

三、可读流的两种模式

1、流动模式 ( flowing ) ,数据自动从系统底层读取,并通过事件,尽可能快地提供给应用程序。

2、暂停模式 ( paused ),必须显式的调用 read() 读取数据。

可读流 都开始于暂停模式,可以通过如下方法切换到流动模式:

1、添加 'data' 事件回调。

2、调用 resume()。

3、调用 pipe()。

可读流通过如下方法切换回暂停模式:

1、如果没有管道目标,调用 pause()。

2、如果有管道目标,移除所有管道目标,调用 unpipe() 移除多个管道目标。

四、创建可读流,并监听事件

const fs = require('fs'); //创建一个文件可读流 let rs = fs.createReadStream('./1.txt', { //文件系统标志 flags: 'r', //数据编码,如果调置了该参数,则读取的数据会自动解析 //如果没调置,则读取的数据会是 Buffer //也可以通过 rs.setEncoding() 进行设置 encoding: 'utf8', //文件描述符,默认为null fd: null, //文件权限 mode: 0o666, //文件读取的开始位置 start: 0, //文件读取的结束位置(包括结束位置) end: Infinity, //读取缓冲区的大小,默认64K highWaterMark: 3 }); //文件被打开时触发 rs.on('open', function () { console.log('文件打开'); }); //监听data事件,会让当前流切换到流动模式 //当流中将数据传给消费者后触发 //由于我们在上面配置了 highWaterMark 为 3字节,所以下面会打印多次。 rs.on('data', function (data) { console.log(data); }); //流中没有数据可供消费者时触发 rs.on('end', function () { console.log('数据读取完毕'); }); //读取数据出错时触发 rs.on('error', function () { console.log('读取错误'); }); //当文件被关闭时触发 rs.on('close', function () { console.log('文件关闭'); });

注意,'open' 和 'close' 事件并不是所有流都会触发。

当们监听'data'事件后,系统会尽可能快的读取出数据。但有时候,我们需要暂停一下流的读取,操作其他事情。

这时候就需要用到 pause() 和 resume() 方法。

const fs = require('fs'); //创建一个文件可读流 let rs = fs.createReadStream('./1.txt', { highWaterMark: 3 }); rs.on('data', function (data) { console.log(`读取了 ${data.length} 字节数据 : ${data.toString()}`); //使流动模式的流停止触发'data'事件,切换出流动模式,数据都会保留在内部缓存中。 rs.pause(); //等待3秒后,再恢复触发'data'事件,将流切换回流动模式。 setTimeout(function () { rs.resume(); }, 3000); });

可读流的 'readable' 事件,当流中有数据可供读取时就触发。

注意当监听 'readable' 事件后,会导致流停止流动,需调用 read() 方法读取数据。

注意 on('data'),on('readable'),pipe() 不要混合使用,会导致不明确的行为。

const fs = require('fs'); let rs = fs.createReadStream('./1.txt', { highWaterMark: 1 }); //当流中有数据可供读取时就触发 rs.on('readable', function () { let data; //循环读取数据 //参数表示要读取的字节数 //如果可读的数据不足字节数,则返回缓冲区剩余数据 //如是没有指定字节数,则返回缓冲区中所有数据 while (data = rs.read()) { console.log(`读取到 ${data.length} 字节数据`); console.log(data.toString()); } });

五、创建可写流,并监听事件

const fs = require('fs'); //创建一个文件可写流 let ws = fs.createWriteStream('./1.txt', { highWaterMark: 3 }); //往流中写入数据 //参数一表示要写入的数据 //参数二表示编码方式 //参数三表示写入成功的回调 //缓冲区满时返回false,未满时返回true。 //由于上面我们设置的缓冲区大小为 3字节,所以到写入第3个时,就返回了false。 console.log(ws.write('1', 'utf8')); console.log(ws.write('2', 'utf8')); console.log(ws.write('3', 'utf8')); console.log(ws.write('4', 'utf8')); function writeData() { let cnt = 9; return function () { let flag = true; while (cnt && flag) { flag = ws.write(`${cnt}`); console.log('缓冲区中写入的字节数', ws.writableLength); cnt--; } }; } let wd = writeData(); wd(); //当缓冲区中的数据满的时候,应停止写入数据, //一旦缓冲区中的数据写入文件了,并清空了,则会触发 'drain' 事件,告诉生产者可以继续写数据了。 ws.on('drain', function () { console.log('可以继续写数据了'); console.log('缓冲区中写入的字节数', ws.writableLength); wd(); }); //当流或底层资源关闭时触发 ws.on('close', function () { console.log('文件被关闭'); }); //当写入数据出错时触发 ws.on('error', function () { console.log('写入数据错误'); });

写入流的 end() 方法 和 'finish' 事件监听

const fs = require('fs'); //创建一个文件可写流 let ws = fs.createWriteStream('./1.txt', { highWaterMark: 3 }); //往流中写入数据 //参数一表示要写入的数据 //参数二表示编码方式 //参数三表示写入成功的回调 //缓冲区满时返回false,未满时返回true。 //由于上面我们设置的缓冲区大小为 3字节,所以到写入第3个时,就返回了false。 console.log(ws.write('1', 'utf8')); console.log(ws.write('2', 'utf8')); console.log(ws.write('3', 'utf8')); console.log(ws.write('4', 'utf8')); //调用end()表明已经没有数据要被写入,在关闭流之前再写一块数据。 //如果传入了回调函数,则将作为 'finish' 事件的回调函数 ws.end('最后一点数据', 'utf8'); //调用 end() 且缓冲区数据都已传给底层系统时触发 ws.on('finish', function () { console.log('写入完成'); });

写入流的 cork() 和 uncork() 方法,主要是为了解决大量小块数据写入时,内部缓冲可能失效,导致的性能下降。

const fs = require('fs'); let ws = fs.createWriteStream('./1.txt', { highWaterMark: 1 }); //调用 cork() 后,会强制把所有写入的数据缓冲到内存中。 //不会因为写入的数据超过了 highWaterMark 的设置而写入到文件中。 ws.cork(); ws.write('1'); console.log(ws.writableLength); ws.write('2'); console.log(ws.writableLength); ws.write('3'); console.log(ws.writableLength); //将调用 cork() 后的缓冲数据都输出到目标,也就是写入文件中。 ws.uncork();

注意 cork() 的调用次数要与 uncork() 一致。

const fs = require('fs'); let ws = fs.createWriteStream('./1.txt', { highWaterMark: 1 }); //调用一次 cork() 就应该写一次 uncork(),两者要一一对应。 ws.cork(); ws.write('4'); ws.write('5'); ws.cork(); ws.write('6'); process.nextTick(function () { //注意这里只调用了一次 uncork() ws.uncork(); //只有调用同样次数的 uncork() 数据才会被输出。 ws.uncork(); });

六、可读流的 pipe() 方法

pipe() 方法类似下面的代码,在可读流与可写流之前架起一座桥梁。

const fs = require('fs'); //创建一个可读流 let rs = fs.createReadStream('./1.txt', { highWaterMark: 3 }); //创建一个可写流 let ws = fs.createWriteStream('./2.txt', { highWaterMark: 3 }); rs.on('data', function (data) { let flag = ws.write(data); console.log(`往可写流中写入 ${data.length} 字节数据`); //如果写入缓冲区已满,则暂停可读流的读取 if (!flag) { rs.pause(); console.log('暂停可读流'); } }); //监控可读流数据是否读完 rs.on('end', function () { console.log('数据已读完'); //如果可读流读完了,则调用 end() 表示可写流已写入完成 ws.end(); }); //如果可写流缓冲区已清空,可以再次写入,则重新打开可读流 ws.on('drain', function () { rs.resume(); console.log('重新开启可读流'); });

我们用 pipe() 方法完成上面的功能。

const fs = require('fs'); //创建一个可读流 let rs = fs.createReadStream('./1.txt', { highWaterMark: 3 }); //创建一个可写流 let ws = fs.createWriteStream('./2.txt', { highWaterMark: 3 }); let ws2 = fs.createWriteStream('./3.txt', { highWaterMark: 3 }); //绑定可写流到可读流,自动将可读流切换到流动模式,将可读流的所有数据推送到可写流。 rs.pipe(ws); //可以绑定多个可写流 rs.pipe(ws2);

我们也可以用 unpipe() 手动的解绑可写流。

const fs = require('fs'); //创建一个可读流 let rs = fs.createReadStream('./1.txt', { highWaterMark: 3 }); //创建一个可写流 let ws = fs.createWriteStream('./2.txt', { highWaterMark: 3 }); let ws2 = fs.createWriteStream('./3.txt', { highWaterMark: 3 }); rs.pipe(ws); rs.pipe(ws2); //解绑可写流,如果参数没写,则解绑所有管道 setTimeout(function () { rs.unpipe(ws2); }, 0);

希望本文所述对大家node.js程序设计有所帮助。

扩展阅读,根据您访问的内容系统为您准备了以下内容,希望对您有帮助。

nodejs fs 读取文件流一次读取多少数据

node中有个流的概念,stream。代表数据流动方向:向内流(可读流),向外流(可写流)。常用的流形式是文件,和tcp套接字。流是以快为单位发送数据,通过监听‘data’事件,每一次得到一块数据即进行一次操作,当数据结束时,监听end事件,执行后续操作。

可读流可以通过stream.pause();stream.resume()暂停恢复流。其中暂停流在写文件时会停止从该文件中读取数据,如果是TCP套接字则不会读取新的数据包,终止其他终端来的数据包。可写流是要从node向外输出的数据,可以是TCP连接也可以是文件。node不会在io操作上阻塞,写入的缓冲区如果被刷新,会发射drain事件,如果没有被刷新,数据将被存储在进程内存中。

TCP连接是双向的,因此既是可读流也是可写流。以node作为服务器,则HTTP的请求是可读流,响应是可写流。

通常在服务器端读取本地数据是快速的,即可读流是快速的。然而由于网络原因,很多响应即可写流是无法保证的,如果请求太多(在服务器的读写范围内),而响应太慢,会导致服务器的可写流填满。此时需要避免慢客户端(前端响应慢)的情况。

[javascript] view plain copy

  • fs = require('fs');  

  • require('http').createServer(function (req,res) {  

  • var rs = fs.createReadStream('./1.txt');  

  • rs.on('data', function(data) {  

  • if(!res.write(data)){  

  • rs.pause();  

  • }  

  • });  

  • rs.on('drain', function(data) {  

  • rs.resume();  

  • });  

  • rs.on('end', function(){  

  • res.end();  

  • })  

  • }).listen(8080,function(){  

  • console.log('正在监听...');  

  • });  

  • 也可以使用stream.pipe();pipe接受可写流作为第一个参数,由传输源调用。
  • [javascript] view plain copy

  • fs = require('fs');  

  • require('http').createServer(function (req,res) {  

  • var rs = fs.createReadStream('./1.txt');  

  • rs.pipe(res);  

  • }).listen(8080,function(){  

  • console.log('正在监听...');  

  • });  

nodejs 怎么下载远程文件并该名称

nodejs对文件的读写还是相当灵活的,可以根据不同的场景来选择不同的方法。

一.直接操作文件

最简单的两个fs.readFile和fs.writeFile

  举例:这个程序的功能是将一个比较大json格式的文件转换成你想自己要格式的文件。

var fs = require('fs');

fs.readFile('./json.json',function(err,data){

if(err) throw err;

var jsonObj = JSON.parse(data);

var space = ' ';

var newLine = '\n';

var chunks = [];

var length = 0;

for(var i=0,size=jsonObj.length;i<size;i++){

var one = jsonObj[i];

//what value you want

var value1 = one['value1'];

var value2 = one['value2'];

....

var value = value1 +space+value2+space+.....+newLine;

var buffer = new Buffer(value);

chunks.push(buffer);

length += buffer.length;

}

var resultBuffer = new Buffer(length);

for(var i=0,size=chunks.length,pos=0;i<size;i++){

chunks[i].copy(resultBuffer,pos);

pos += chunks[i].length;

}

fs.writeFile('./resut.text',resultBuffer,function(err){

if(err) throw err;

console.log('has finished');

});

});

它的原理是将文件数据一次性全部读入内存,优点就是接下来都是在内存的操作,速度会很快。但缺点也很明显,就是当文件非常大时,会造成内存溢出。

二. 使用文件流

   2.1 读取文件,api相见:fs.createReadSream 和 fs.createWriterStream

以下代码实现的功能就是通过文件流来实现图片的复制:

var fs = require('fs');

var rOption = {

flags : 'r',

encoding : null,

mode : 0666

}

var wOption = {

flags: 'a',

encoding: null,

mode: 0666

}

var fileReadStream = fs.createReadStream('./myjpg.jpg',rOption);

var fileWriteStream = fs.createWriteStream('./new_myjpg.jpg',wOption);

fileReadStream.on('data',function(data){

fileWriteStream.write(data);

});

fileReadStream.on('end',function(){

console.log('readStream end');

fileWriteStream.end();

});

如何从字符串中的Node.js创建流

1. 刚创建的一个新实例stream模块,并根据您的需求定制它:

var Stream = require('stream')

var stream = new Stream()

stream.pipe = function(dest) {

dest.write('your string')

}

stream.pipe(process.stdout) // in this case the terminal, change to ya-csv

var Stream = require('stream')

var stream = new Stream()

stream.on('data', function(data) {

process.stdout.write(data) // change process.stdout to ya-csv

})

stream.emit('data', 'this is my string')

2. 将字符串转换为流,通过一个流:

through().pause().queue('your string').end()

例如:

var through = require('through')

// Create a paused stream and buffer some data into it:

var stream = through().pause().queue('your string').end()

// Pass stream around:

callback(null, stream)

// Now that a consumer has attached, don't forget to resume the stream:

stream.resume()

更新:@substack刚刚发布的模块。这就像一个通过()流,但自上nextTick流,当且仅当别人有它在

var resumer = require('resumer')

var stream = resumer().queue('your string').end()

// Now pass `stream` around. No need to resume, it happens automatically.

该resumer().queue('some string').end()idioms是我现在创建adhoc流。这是方便多了,你可以return流,他们还是会得到

3. JavaScript是鸭子类型的 CodeGo.net,所以如果你只是复制一个可读的流的API,它会工作得很好。事实上,你也许可以不是大多数的或刚刚离开他们作为存根,所有你需要的是什么样的你节点的预建EventEmitter类来处理事件,也让你不必addListener和这样的自己。 这里是你如何可能它的CoffeeScript:

class StringStream extends require('events').EventEmitter

constructor: (@string) -> super()

readable: true

writable: false

setEncoding: -> throw 'not implemented'

pause: -> # nothing to do

resume: -> # nothing to do

destroy: -> # nothing to do

pipe: -> throw 'not implemented'

send: ->

@emit 'data', @string

@emit 'end'

那么你也喜欢这样:

stream = new StringStream someString

doSomethingWith stream

stream.send()

最近要写一篇论文,数据质量管理类或者数据处理应用实践类,有这方面资料朋友提拱一下,谢谢大家!

  中科永联高级技术培训中心面向对象(Object Oriented,OO)是当前计算机界关心的重点,它是90年代软件开发方法的主流。面向对象的概念和应用已超越了程序设计和软件开发,扩展到很宽的范围。如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域 谈到面向对象,这方面的文章非常多。但是,明确地给出对象的定义或说明对象的定义的非常少——至少我现在还没有发现。其初,“面向对象”是专指在程序设计中采用封装、继承、抽象等设计方法。可是,这个定义显然不能再适合现在情况。面向对象的思想已经涉及到软件开发的各个方面。如,面向对象的分析(OOA,Object Oriented Analysis),面向对象的设计(OOD,Object Oriented Design)、以及我们经常说的面向对象的编程实现(OOP,Object Oriented Programming)。许多有关面向对象的文章都只是讲述在面向对象的开发中所需要注意的问题或所采用的比较好的设计方法。看这些文章只有真正懂得什么是对象,什么是面向对象,才能最大程度地对自己有所裨益。这一点,恐怕对初学者甚至是从事相关工作多年的人员也会对它们的概念模糊不清。

  面向对象是当前计算机界关心的重点,它是90年代软件开发方法的主流。面向对象的概念和应用已超越了程序设计和软件开发,扩展到很宽的范围。如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。

  一、传统开发方法存在问题

  1.软件重用性差

  重用性是指同一事物不经修改或稍加修改就可多次重复使用的性质。软件重用性是软件工程追求的目标之一。

  2.软件可维护性差

  软件工程强调软件的可维护性,强调文档资料的重要性,规定最终的软件产品应该由完整、一致的配置成分组成。在软件开发过程中,始终强调软件的可读性、可修改性和可测试性是软件的重要的质量指标。实践证明,用传统方法开发出来的软件,维护时其费用和成本仍然很高,其原因是可修改性差,维护困难,导致可维护性差。

  3.开发出的软件不能满足用户需要

  用传统的结构化方法开发大型软件系统涉及各种不同领域的知识,在开发需求模糊或需求动态变化的系统时,所开发出的软件系统往往不能真正满足用户的需要。

  用结构化方法开发的软件,其稳定性、可修改性和可重用性都比较差,这是因为结构化方法的本质是功能分解,从代表目标系统整体功能的单个处理着手,自顶向下不断把复杂的处理分解为子处理,这样一层一层的分解下去,直到仅剩下若干个容易实现的子处理功能为止,然后用相应的工具来描述各个最低层的处理。因此,结构化方法是围绕实现处理功能的“过程”来构造系统的。然而,用户需求的变化大部分是针对功能的,因此,这种变化对于基于过程的设计来说是灾难性的。用这种方法设计出来的系统结构常常是不稳定的 ,用户需求的变化往往造成系统结构的较大变化,从而需要花费很大代价才能实现这种变化。

  二、面向对象的基本概念

  (1)对象。

  对象是人们要进行研究的任何事物,从最简单的整数到复杂的飞机等均可看作对象,它不仅能表示具体的事物,还能表示抽象的规则、计划或事件。

  (2)对象的状态和行为。

  对象具有状态,一个对象用数据值来描述它的状态。

  对象还有操作,用于改变对象的状态,对象及其操作就是对象的行为。

  对象实现了数据和操作的结合,使数据和操作封装于对象的统一体中

  (3)类。

  具有相同或相似性质的对象的抽象就是类。因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象。

  类具有属性,它是对象的状态的抽象,用数据结构来描述类的属性。

  类具有操作,它是对象的行为的抽象,用操作名和实现该操作的方法来描述。

  (4)类的结构。

  在客观世界中有若干类,这些类之间有一定的结构关系。通常有两种主要的结构关系,即一般--具体结构关系,整体--部分结构关系。

  ①一般——具体结构称为分类结构,也可以说是“或”关系,或者是“is a”关系。

  ②整体——部分结构称为组装结构,它们之间的关系是一种“与”关系,或者是“has a”关系。

  (5)消息和方法。

  对象之间进行通信的结构叫做消息。在对象的操作中,当一个消息发送给某个对象时,消息包含接收对象去执行某种操作的信息。发送一条消息至少要包括说明接受消息的对象名、发送给该对象的消息名(即对象名、方法名)。一般还要对参数加以说明,参数可以是认识该消息的对象所知道的变量名,或者是所有对象都知道的全局变量名。

  类中操作的实现过程叫做方法,一个方法有方法名、参数、方法体。消息传递如图10-1所示。

  二、面向对象的特征

  (1)对象唯一性。

  每个对象都有自身唯一的标识,通过这种标识,可找到相应的对象。在对象的整个生命期中,它的标识都不改变,不同的对象不能有相同的标识。

  (2)分类性。

  分类性是指将具有一致的数据结构(属性)和行为(操作)的对象抽象成类。一个类就是这样一种抽象,它反映了与应用有关的重要性质,而忽略其他一些无关内容。任何类的划分都是主观的,但必须与具体的应用有关。

  (3)继承性。

  继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。

  继承性是面向对象程序设计语言不同于其它语言的最重要的特点,是其他语言所没有的。

  在类层次中,子类只继承一个父类的数据结构和方法,则称为单重继承。

  在类层次中,子类继承了多个父类的数据结构和方法,则称为多重继承。

  在软件开发中,类的继承性使所建立的软件具有开放性、可扩充性,这是信息组织与分类的行之有效的方法,它简化了对象、类的创建工作量,增加了代码的可重性。

  采用继承性,提供了类的规范的等级结构。通过类的继承关系,使公共的特性能够共享,提高了软件的重用性。

  (4)多态性(多形性)

  多态性使指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。

  多态性允许每个对象以适合自身的方式去响应共同的消息。

  多态性增强了软件的灵活性和重用性。

  三、面向对象的要素

  (1)抽象。

  抽象是指强调实体的本质、内在的属性。在系统开发中,抽象指的是在决定如何实现对象之前的对象的意义和行为。使用抽象可以尽可能避免过早考虑一些细节。

  类实现了对象的数据(即状态)和行为的抽象。

  (2)封装性(信息隐藏)。

  封装性是保证软件部件具有优良的模块性的基础。

  面向对象的类是封装良好的模块,类定义将其说明(用户可见的外部接口)与实现(用户不可见的内部实现)显式地分开,其内部实现按其具体定义的作用域提供保护。

  对象是封装的最基本单位。封装防止了程序相互依赖性而带来的变动影响。面向对象的封装比传统语言的封装更为清晰、更为有力。

  (3)共享性

  面向对象技术在不同级别上促进了共享

  同一类中的共享。同一类中的对象有着相同数据结构。这些对象之间是结构、行为特征的共享关系。

  在同一应用*享。在同一应用的类层次结构中,存在继承关系的各相似子类中,存在数据结构和行为的继承,使各相似子类共享共同的结构和行为。使用继承来实现代码的共享,这也是面向对象的主要优点之一。

  在不同应用*享。面向对象不仅允许在同一应用*享信息,而且为未来目标的可重用设计准备了条件。通过类库这种机制和结构来实现不同应用中的信息共享。

  4.强调对象结构而不是程序结构

  四、面向对象的开发方法

  目前,面向对象开发方法的研究已日趋成熟,国际上已有不少面向对象产品出现。面向对象开发方法有Coad方法、Booch方法和OMT方法等。

  1.Booch方法

  Booch最先描述了面向对象的软件开发方法的基础问题,指出面向对象开发是一种根本不同于传统的功能分解的设计方法。面向对象的软件分解更接近人对客观事务的理解,而功能分解只通过问题空间的转换来获得。

  2.Coad方法

  Coad方法是1989年Coad和Yourdon提出的面向对象开发方法。该方法的主要优点是通过多年来大系统开发的经验与面向对象概念的有机结合,在对象、结构、属性和操作的认定方面,提出了一套系统的原则。该方法完成了从需求角度进一步进行类和类层次结构的认定。尽管Coad方法没有引入类和类层次结构的术语,但事实上已经在分类结构、属性、操作、消息关联等概念中体现了类和类层次结构的特征。

  3.OMT方法

  OMT方法是1991年由James Rumbaugh等5人提出来的,其经典著作为“面向对象的建模与设计”。

  该方法是一种新兴的面向对象的开发方法,开发工作的基础是对真实世界的对象建模,然后围绕这些对象使用分析模型来进行独立于语言的设计,面向对象的建模和设计促进了对需求的理解,有利于开发得更清晰、更容易维护的软件系统。该方法为大多数应用领域的软件开发提供了一种实际的、高效的保证,努力寻求一种问题求解的实际方法。

  4.UML(Unified Modeling Language)语言

  软件工程领域在1995年~1997年取得了前所未有的进展,其成果超过软件工程领域过去15年的成就总和,其中最重要的成果之一就是统一建模语言(UML)的出现。UML将是面向对象技术领域内占主导地位的标准建模语言。

  UML不仅统一了Booch方法、OMT方法、OOSE方法的表示方法,而且对其作了进一步的发展,最终统一为大众接受的标准建模语言。UML是一种定义良好、易于表达、功能强大且普遍适用的建模语言。它融入了软件工程领域的新思想、新方法和新技术。它的作用域不限于支持面向对象的分析与设计,还支持从需求分析开始的软件开发全过程。

  五、面向对象的模型

  ·对象模型

  对象模型表示了静态的、结构化的系统数据性质,描述了系统的静态结构,它是从客观世界实体的对象关系角度来描述,表现了对象的相互关系。该模型主要关心系统中对象的结构、属性和操作,它是分析阶段三个模型的核心,是其他两个模型的框架。

  1.对象和类

  (1) 对象。

  对象建模的目的就是描述对象。

  (2) 类。

  通过将对象抽象成类,我们可以使问题抽象化,抽象增强了模型的归纳能力。

  (3) 属性。

  属性指的是类中对象所具有的性质(数据值)。

  (4) 操作和方法。

  操作是类中对象所使用的一种功能或变换。类中的各对象可以共享操作,每个操作都有一个目标对象作为其隐含参数。

  方法是类的操作的实现步骤。

  2.关联和链

  关联是建立类之间关系的一种手段,而链则是建立对象之间关系的一种手段。

  (1) 关联和链的含义。

  链表示对象间的物理与概念联结,关联表示类之间的一种关系,链是关联的实例,关联是链的抽象。

  (2) 角色。

  角色说明类在关联中的作用,它位于关联的端点。

  (3) 受限关联。

  受限关联由两个类及一个限定词组成,限定词是一种特定的属性,用来有效的减少关联的重数,限定词在关联的终端对象集中说明。

  限定提高了语义的精确性,增强了查询能力,在现实世界中,常常出现限定词。

  (4) 关联的多重性。

  关联的多重性是指类中有多少个对象与关联的类的一个对象相关。重数常描述为“一”或“多”。

  图10-8表示了各种关联的重数。小实心圆表示“多个”,从零到多。小空心圆表示零或一。没有符号表示的是一对一关联。

  3.类的层次结构

  (1) 聚集关系。

  聚集是一种“整体-部分”关系。在这种关系中,有整体类和部分类之分。聚集最重要的性质是传递性,也具有逆对称性。

  聚集可以有不同层次,可以把不同分类聚集起来得到一颗简单的聚集树,聚集树是一种简单表示,比画很多线来将部分类联系起来简单得多,对象模型应该容易地反映各级层次,图10-10表示一个关于微机的多极聚集。

  (2)一般化关系。

  一般化关系是在保留对象差异的同时共享对象相似性的一种高度抽象方式。它是“一般---具体”的关系。一般化类称为你类,具体类又能称为子类,各子类继承了交类的性质,而各子类的一些共同性质和操作又归纳到你类中。因此,一般化关系和继承是同时存在的。一般化关系的符号表示是在类关联的连线上加一个小三角形,如图10-11

  4.对象模型

  (1)模板。模板是类、关联、一般化结构的逻辑组成。

  (2)对象模型。

  对象模型是由一个或若干个模板组成。模板将模型分为若干个便于管理的子块,在整个对象模型和类及关联的构造块之间,模板提供了一种集成的中间单元,模板中的类名及关联名是唯一的。

  ·动态模型

  动态模型是与时间和变化有关的系统性质。该模型描述了系统的控制结构,它表示了瞬间的、行为化的系统控制

  性质,它关心的是系统的控制,操作的执行顺序,它表示从对象的事件和状态的角度出发,表现了对象的相互行为。

  该模型描述的系统属性是触发事件、事件序列、状态、事件与状态的组织。使用状态图作为描述工具。它涉及到事件、状态、操作等重要概念。

  1.事件

  事件是指定时刻发生的某件事。

  2.状态

  状态是对象属性值的抽象。对象的属性值按照影响对象显著行为的性质将其归并到一个状态中去。状态指明了对象

  对输入事件的响应。

  3.状态图

  状态图是一个标准的计算机概念,他是有限自动机的图形表示,这里把状态图作为建立动态模型的图形工具。

  状态图反映了状态与事件的关系。当接收一事件时,下一状态就取决于当前状态和所接收的该事件,由该事件引起的状态变化称为转换。

  状态图是一种图,用结点表示状态,结点用圆圈表示;圆圈内有状态名,用箭头连线表示状态的转换,上面标记事件名,箭头方向表示转换的方向。

  ·功能模型

  功能模型描述了系统的所有计算。功能模型指出发生了什么,动态模型确定什么时候发生,而对象模型确定发生的客体。功能模型表明一个计算如何从输入值得到输出值,它不考虑计算的次序。功能模型由多张数据流图组成。数据流图用来表示从源对象到目标对象的数据值的流向,它不包含控制信息,控制信息在动态模型中表示,同时数据流图也不表示对象中值的组织,值的组织在对象模型中表示。图10-15给出了一个窗口系统的图标显示的数据流图。

  数据流图中包含有处理、数据流、动作对象和数据存储对象。

  1.处理

  数据流图中的处理用来改变数据值。最低层处理是纯粹的函数,一张完整的数据流图是一个高层处理。

  2.数据流

  数据流图中的数据流将对象的输出与处理、处理与对象的输入、处理与处理联系起来。在一个计算机中,用数据流来表示一中间数据值,数据流不能改变数据值。

  3.动作对象

  动作对象是一种主动对象,它通过生成或者使用数据值来驱动数据流图。

  4.数据存储对象

  数据流图中的数据存储是被动对象,它用来存储数据。它与动作对象不一样,数据存储本身不产生任何操作,它只响应存储和访问的要求。

  六、面向对象的分析

  面向对象分析的目的是对客观世界的系统进行建模。本节以上面介绍的模型概念为基础,结合“银行网络系统”的具体实例来构造客观世界问题的准确、严密的分析模型。

  分析模型有三种用途:用来明确问题需求;为用户和开发人员提供明确需求;为用户和开发人员提供一个协商的基础,作为后继的设计和实现的框架。

  (一) 面向对象的分析

  系统分析的第一步是:陈述需求。分析者必须同用户一块工作来提炼需求,因为这样才表示了用户的真实意图,其中涉及对需求的分析及查找丢失的信息。下面以“银行网络系统”为例,用面向对象方法进行开发。

  银行网络系统问题陈述: 设计支持银行网络的软件,银行网络包括人工出纳站和分行共享的自动出纳机。每个分理处用分理处计算机来保存各自的帐户,处理各自的事务;各自分理处的出纳站与分理处计算机通信,出纳站录入帐户和事务数据;自动出纳机与分行计算机通信,分行计算机与拨款分理处结帐,自动出纳机与用户接口接受现金卡,与分行计算机通信完成事务,发放现金,打印收据;系统需要记录保管和安全措施;系统必须正确处理同一帐户的并发访问;每个分处理为自己的计算机准备软件,银行网络费用根据顾客和现金卡的数目分摊给各分理处。

  图10-18给出银行网络系统的示意图。

  (二)建立对象模型

  首先标识和关联,因为它们影响了整体结构和解决问题的方法,其次是增加属性,进一步描述类和关联的基本网络,使用继承合并和组织类,最后操作增加到类中去作为构造动态模型和功能模型的副产品。

  1.确定类

  构造对象模型的第一步是标出来自问题域的相关的对象类,对象包括物理实体和概念。所有类在应用中都必须有意义,在问题陈述中,并非所有类都是明显给出的。有些是隐含在问题域或一般知识中的。

  按图10-19所示的过程确定类

  查找问题陈述中的所有名词,产生如下的暂定类。

  软件 银行网络 出纳员 自动出纳机 分行

  分处理 分处理计算机 帐户 事务 出纳站

  事务数据 分行计算机 现金卡 用户 现金

  收据 系统 顾客 费用 帐户数据

  访问 安全措施 记录保管

  根据下列标准,去掉不必要的类和不正确的类。

  (1) 冗余类:若两个类表述了同一个信息 ,保留最富有描述能力的类。如"用户"和"顾客"就是重复的描述,因为"顾客"最富有描述性,因此保留它。

  (2) 不相干的类:除掉与问题没有关系或根本无关的类。例如,摊派费用超出了银行网络的范围。

  (3) 模糊类:类必须是确定的,有些暂定类边界定义模糊或范围太广,如"记录保管"就模糊类,它是"事务"中的一部分。

  (4) 属性:某些名词描述的是其他对象的属性,则从暂定类中删除。如果某一性质的独立性很重要,就应该把他归属到类,而不把它作为属性。

  (5) 操作:如果问题陈述中的名词有动作含义,则描述的操作就不是类。但是具有自身性质而且需要独立存在的操作应该描述成类。如我们只构造电话模型,"拨号"就是动态模型的一部分而不是类,但在电话拨号系统中,"拨号"是一个重要的类,它日期、时间、受话地点等属性。

  在银行网络系统中,模糊类是"系统"、"安全措施"、"记录保管"、"银行网络"等。属于属性的有:"帐户数据"、"收据"、"现金"、"事务数据"。属于实现的如:"访问"、"软件"等。这些均应除去。

  2.准备数据字典

  为所有建模实体准备一个数据字典。准确描述各个类的精确含义,描述当前问题中的类的范围,包括对类的成员、用法方面的假设或*。

  3.确定关联

  两个或多个类之间的相互依赖就是关联。一种依赖表示一种关联,可用各种方式来实现关联,但在分析模型中应删除实现的考虑,以便设计时更为灵活。关联常用描述性动词或动词词组来表示,其中有物理位置的表示、传导的动作、通信、所有者关系、条件的满足等。从问题陈述中抽取所有可能的关联表述,把它们记下来,但不要过早去细化这些表述。

  下面是银行网络系统中所有可能的关联,大多数是直接抽取问题中的动词词组而得到的。在陈述中,有些动词词组表述的关联是不明显的。最后,还有一些关联与客观世界或人的假设有关,必须同用户一起核实这种关联,因为这种关联在问题陈述中找不到。

  银行网络问题陈述中的关联:

  ·银行网络包括出纳站和自动出纳机;

  ·分行共享自动出纳机;

  ·分理处提供分理处计算机;

  ·分理处计算机保存帐户;

  ·分理处计算机处理帐户支付事务;

  ·分理处拥有出纳站;

  ·出纳站与分理处计算机通信;

  ·出纳员为帐户录入事务;

  ·自动出纳机接受现金卡;

  ·自动出纳机与用户接口;

  ·自动出纳机发放现金;

  ·自动出纳机打印收据;

  ·系统处理并发访问;

  ·分理处提供软件;

  ·费用分摊给分理处。

  隐含的动词词组:

  ·分行由分理处组成;

  ·分理处拥有帐户;

  ·分行拥有分行计算机;

  ·系统提供记录保管;

  ·系统提供安全;

  ·顾客有现金卡。

  基于问题域知识的关联:

  ·分理处雇佣出纳员;

  ·现金卡访问帐户。

  使用下列标准去掉不必要和不正确的关联:

  (1) 若某个类已被删除,那么与它有关的关联也必须删除或者用其它类来重新表述。在例中,我们删除了"银行网络",相关的关联也要删除。

  (2) 不相干的关联或实现阶段的关联:删除所有问题域之外的关联或涉及实现结构中的关联。如"系统处理并发访问"就是一种实现的概念。

  (3) 动作:关联应该描述应用域的结构性质而不是瞬时事件,因此应删除"自动出纳机接受现金卡","自动出纳机与用户接口"等。

  (4) 派生关联:省略那些可以用其他关联来定义的关联。因为这种关联是冗余的。银行网络系统的初步对象图如图10-20所示。其中含有关联。

  4.确定属性

  属性是个体对象的性质,属性通常用修饰性的名词词组来表示.形容词常常表示具体的可枚举的属性值,属性不可能在问题陈述中完全表述出来,必须借助于应用域的知识及对客观世界的知识才可以找到它们。只考虑与具体应用直接相关的属性,不要考虑那些超出问题范围的属性。首先找出重要属性,避免那些只用于实现的属性,要为各个属性取有意义的名字。按下列标准删除不必要的和不正确的属性:

  (1) 对象:若实体的独立存在比它的值重要,那么这个实体不是属性而是对象。如在邮政目录中,"城市"是一个属性,然而在人口普查中,"城市"则被看作是对象。在具体应用中,具有自身性质的实体一定是对象。

  (2) 定词:若属性值取决于某种具体上下文,则可考虑把该属性重新表述为一个限定词。

  (3) 名称:名称常常作为限定词而不是对象的属性,当名称不依赖于上下文关系时,名称即为一个对象属性,尤其是它不惟一时。

  (4) 标识符:在考虑对象模糊性时,引入对象标识符表示,在对象模型中不列出这些对象标识符,它是隐含在对象模型中,只列出存在于应用域的属性。

  (5) 内部值:若属性描述了对外不透明的对象的内部状态,则应从对象模型中删除该属性。

  (6) 细化:忽略那些不可能对大多数操作有影响的属性。

  5.使用继承来细化类

  使用继承来共享公共机构,以次来组织类,可以用两种方式来进行。

  (1) 自底向上通过把现有类的共同性质一般化为父类,寻找具有相似的属性,关系或操作的类来发现继承。例如"远程事务"和"出纳事务"是类似的,可以一般化为"事务"。有些一般化结构常常是基于客观世界边界的现有分类,只要可能,尽量使用现有概念。对称性常有助于发现某些丢失的类。

  (2) 自顶向下将现有的类细化为更具体的子类。具体化常常可以从应用域中明显看出来。应用域中各枚举字情况是最常见的具体化的来源。例如:菜单,可以有固定菜单,顶部菜单,弹出菜单,下拉菜单等,这就可以把菜单类具体细化为各种具体菜单的子类。当同一关联名出现多次且意义也相同时,应尽量具体化为相关联的类,例如"事务"从"出纳站"和"自动出纳机"进入,则"录入站"就是"出纳站"和"自动出纳站"的一般化。在类层次中,可以为具体的类分配属性和关联。各属性和都应分配给最一般的适合的类,有时也加上一些修正。

  应用域中各枚举情况是最常见的具体化的来源。

  6.完善对象模型

  对象建模不可能一次就能保证模型是完全正确的,软件开发的整个过程就是一个不断完善的过程。模型的不同组成部分多半是在不同的阶段完成的,如果发现模型的缺陷,就必须返回到前期阶段去修改,有些细化工作是在动态模型和功能模型完成之后才开始进行的。

  (1) 几种可能丢失对象的情况及解决办法:

  ·同一类中存在毫无关系的属性和操作,则分解这个类,使各部分相互关联;

  ·一般化体系不清楚,则可能分离扮演两种角色的类

  ·存在无目标类的操作,则找出并加上失去目标的类;

  ·存在名称及目的相同的冗余关联,则通过一般化创建丢失的父类,把关联组织在一起

如何提高代码质量6

高质量代码的三要素 我们评价高质量代码有三要素:可读性、可维护性、可变更性。我们的代码要一个都不能少地达到了这三要素的要求才能算高质量的代码。1.可读性强 一提到可读性似乎有一些老生常谈的味道,但令人沮丧的是,虽然大家一而再,再而三地强调可读性,但我们的代码在可读性方面依然做得非常糟糕。由于工作的需要,我常常需要去阅读他人的代码,维护他人设计的模块。每当我看到大段大段、密密麻麻的代码,而且还没有任何的注释时常常感慨不已,深深体会到了这项工作的重要。由于分工的需要,我们写的代码难免需要别人去阅读和维护的。而对于许多程序员来说,他们很少去阅读和维护别人的代码。正因为如此,他们很少关注代码的可读性,也对如何提高代码的可读性缺乏切身体会。有时即使为代码编写了注释,也常常是注释语言晦涩难懂形同天书,令阅读者反复斟酌依然不明其意。针对以上问题,我给大家以下建议:1)不要编写大段的代码 如果你有阅读他人代码的经验,当你看到别人写的大段大段的代码,而且还不怎么带注释,你是怎样的感觉,是不是“嗡”地一声头大。各种各样的功能纠缠在一个方法中,各种变量来回调用,相信任何人多不会认为它是高质量的代码,但却频繁地出现在我们编写的程序了。如果现在你再回顾自己写过的代码,你会发现,稍微编写一个复杂的功能,几百行的代码就出去了。一些比较好的办法就是分段。将大段的代码经过整理,分为功能相对独立的一段又一段,并且在每段的前端编写一段注释。这样的编写,比前面那些杂乱无章的大段代码确实进步了不少,但它们在功能独立性、可复用性、可维护性方面依然不尽人意。从另一个比较专业的评价标准来说,它没有实现低耦合、高内聚。我给大家的建议是,将这些相对独立的段落另外封装成一个又一个的函数。许多大师在自己的经典书籍中,都鼓励我们在编写代码的过程中应当养成不断重构的习惯。我们在编写代码的过程中常常要编写一些复杂的功能,起初是写在一个类的一个函数中。随着功能的逐渐展开,我们开始对复杂功能进行归纳整理,整理出了一个又一个的独立功能。这些独立功能有它与其它功能相互交流的输入输出数据。当我们分析到此处时,我们会非常自然地要将这些功能从原函数中分离出来,形成一个又一个独立的函数,供原函数调用。在编写这些函数时,我们应当仔细思考一下,为它们取一个释义名称,并为它们编写注释(后面还将详细讨论这个问题)。另一个需要思考的问题是,这些函数应当放到什么地方。这些函数可能放在原类中,也可能放到其它相应职责的类中,其遵循的原则应当是“职责驱动设计”(后面也将详细描述)。下面是我编写的一个从XML文件中读取数据,将其生成工厂的一个类。这个类最主要的一段程序就是初始化工厂,该功能归纳起来就是三部分功能:用各种方式尝试读取文件、以DOM的方式解析XML数据流、生成工厂。而这些功能被我归纳整理后封装在一个不同的函数中,并且为其取了释义名称和编写了注释:Java代码 /** * 初始化工厂。根据路径读取XML文件,将XML文件中的数据装载到工厂中 * @param path XML的路径 */ public void initFactory(String path){ if(findOnlyOneFileByClassPath(path)){return;} if(findResourcesByUrl(path)){return;} if(findResourcesByFile(path)){return;} this.paths = new String[]{path}; } /** * 初始化工厂。根据路径列表依次读取XML文件,将XML文件中的数据装载到工厂中 * @param paths 路径列表 */ public void initFactory(String[] paths){ for(int i=0; i<paths.length; i++){ initFactory(paths[i]); } this.paths = paths; } /** * 重新初始化工厂,初始化所需的参数,为上一次初始化工厂所用的参数。 */ public void reloadFactory(){ initFactory(this.paths); } /** * 采用ClassLoader的方式试图查找一个文件,并调用<code>readXmlStream()</code>进行解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findOnlyOneFileByClassPath(String path){ boolean success = false; try { Resource resource = new ClassPathResource(path, this.getClass()); resource.setFilter(this.getFilter()); InputStream is = resource.getInputStream(); if(is==null){return false;} readXmlStream(is); success = true; } catch (SAXException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (IOException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } return success; } /** * 采用URL的方式试图查找一个目录中的所有XML文件,并调用<code>readXmlStream()</code>进行解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findResourcesByUrl(String path){ boolean success = false; try { ResourcePath resourcePath = new PathMatchResource(path, this.getClass()); resourcePath.setFilter(this.getFilter()); Resource[] loaders = resourcePath.getResources(); for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (SAXException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (IOException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByUrl:"+path,e); } return success; } /** *用File的方式试图查找文件,并调用<code>readXmlStream()</code>解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findResourcesByFile(String path){ boolean success = false; FileResource loader = new FileResource(new File(path)); loader.setFilter(this.getFilter()); try { Resource[] loaders = loader.getResources(); if(loaders==null){return false;} for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (IOException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (SAXException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByFile:"+path,e); } return success; } /** * 读取并解析一个XML的文件输入流,以Element的形式获取XML的根, * 然后调用<code>buildFactory(Element)</code>构建工厂 * @param inputStream 文件输入流 * @throws SAXException * @throws IOException * @throws ParserConfigurationException */ protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{ if(inputStream==null){ throw new ParserConfigurationException("Cann't parse source because of InputStream is null!"); } DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(this.isValidating()); factory.setNamespaceAware(this.isNamespaceAware()); DocumentBuilder build = factory.newDocumentBuilder(); Document doc = build.parse(new InputSource(inputStream)); Element root = doc.getDocumentElement(); buildFactory(root); } /** * 用从一个XML的文件中读取的数据构建工厂 * @param root 从一个XML的文件中读取的数据的根 */ protected abstract void buildFactory(Element root); /** * 初始化工厂。根据路径读取XML文件,将XML文件中的数据装载到工厂中 * @param path XML的路径 */ public void initFactory(String path){ if(findOnlyOneFileByClassPath(path)){return;} if(findResourcesByUrl(path)){return;} if(findResourcesByFile(path)){return;} this.paths = new String[]{path}; } /** * 初始化工厂。根据路径列表依次读取XML文件,将XML文件中的数据装载到工厂中 * @param paths 路径列表 */ public void initFactory(String[] paths){ for(int i=0; i<paths.length; i++){ initFactory(paths[i]); } this.paths = paths; } /** * 重新初始化工厂,初始化所需的参数,为上一次初始化工厂所用的参数。 */ public void reloadFactory(){ initFactory(this.paths); } /** * 采用ClassLoader的方式试图查找一个文件,并调用<code>readXmlStream()</code>进行解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findOnlyOneFileByClassPath(String path){ boolean success = false; try { Resource resource = new ClassPathResource(path, this.getClass()); resource.setFilter(this.getFilter()); InputStream is = resource.getInputStream(); if(is==null){return false;} readXmlStream(is); success = true; } catch (SAXException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (IOException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } return success; } /** * 采用URL的方式试图查找一个目录中的所有XML文件,并调用<code>readXmlStream()</code>进行解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findResourcesByUrl(String path){ boolean success = false; try { ResourcePath resourcePath = new PathMatchResource(path, this.getClass()); resourcePath.setFilter(this.getFilter()); Resource[] loaders = resourcePath.getResources(); for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (SAXException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (IOException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByUrl:"+path,e); } return success; } /** * 用File的方式试图查找文件,并调用<code>readXmlStream()</code>解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findResourcesByFile(String path){ boolean success = false; FileResource loader = new FileResource(new File(path)); loader.setFilter(this.getFilter()); try { Resource[] loaders = loader.getResources(); if(loaders==null){return false;} for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (IOException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (SAXException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByFile:"+path,e); } return success; } /** * 读取并解析一个XML的文件输入流,以Element的形式获取XML的根, * 然后调用<code>buildFactory(Element)</code>构建工厂 * @param inputStream 文件输入流 * @throws SAXException * @throws IOException * @throws ParserConfigurationException */ protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{ if(inputStream==null){ throw new ParserConfigurationException("Cann't parse source because of InputStream is null!"); } DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(this.isValidating()); factory.setNamespaceAware(this.isNamespaceAware()); DocumentBuilder build = factory.newDocumentBuilder(); Document doc = build.parse(new InputSource(inputStream)); Element root = doc.getDocumentElement(); buildFactory(root); } /** * 用从一个XML的文件中读取的数据构建工厂 * @param root 从一个XML的文件中读取的数据的根 */ protected abstract void buildFactory(Element root); 完整代码在附件中。在编写代码的过程中,通常有两种不同的方式。一种是从下往上编写,也就是按照顺序,每分出去一个函数,都要将这个函数编写完,才回到主程序,继续往下编写。而一些更有经验的程序员会采用另外一种从上往下的编写方式。当他们在编写程序的时候,每个被分出去的程序,可以暂时只写一个空程序而不去具体实现功能。当主程序完成以后,再一个个实现它的所有子程序。采用这样的编写方式,可以使复杂程序有更好的规划,避免只见树木不见森林的弊病。有多少代码就算大段代码,每个人有自己的理解。我编写代码,每当达到15~20行的时候,我就开始考虑是否需要重构代码。同理,一个类也不应当有太多的函数,当函数达到一定程度的时候就应该考虑分为多个类了;一个包也不应当有太多的类。。。。。。2)释义名称与注释 我们在命名变量、函数、属性、类以及包的时候,应当仔细想想,使名称更加符合相应的功能。我们常常在说,设计一个系统时应当有一个或多个系统分析师对整个系统的包、类以及相关的函数和属性进行规划,但在通常的项目中这都非常难于做到。对它们的命名更多的还是程序员来完成。但是,在一个项目开始的时候,应当对项目的命名出台一个规范。譬如,在我的项目中规定,新增记录用new或add开头,更新记录用edit或mod开头,删除用del开头,查询用find或query开头。使用最乱的就是get,因此我规定,get开头的函数仅仅用于获取类属性。注释是每个项目组都在不断强调的,可是依然有许多的代码没有任何的注释。为什么呢?因为每个项目在开发过程中往往时间都是非常紧的。在紧张的代码开发过程中,注释往往就渐渐地被忽略了。利用开发工具的代码编写模板也许可以解决这个问题。用我们常用的MyEclipse为例,在菜单“window>>Preferences>>Java>>Code Style>>Code Templates>>Comments”中,可以简单的修改一下。“Files”代表的是我们每新建一个文件(可能是类也可能是接口)时编写的注释,我通常设定为:Java代码 /* * created on ${date} */ /* * created on ${date} */“Types”代表的是我们新建的接口或类前的注释,我通常设定为:Java代码 /** * * @author ${user} */ /** * * @author ${user} */第一行为一个空行,是用于你写该类的注释。如果你采用“职责驱动设计”,这里首先应当描述的是该类的职责。如果需要,你可以写该类一些重要的方法及其用法、该类的属性及其中文含义等。${user}代表的是你在windows中登陆的用户名。如果这个用户名不是你的名称,你可以直接写死为你自己的名称。其它我通常都保持为默认值。通过以上设定,你在创建类或接口的时候,系统将自动为你编写好注释,然后你可以在这个基础上进行修改,大大提高注释编写的效率。同时,如果你在代码中新增了一个函数时,通过Alt+Shift+J快捷键,可以按照模板快速添加注释。在编写代码时如果你编写的是一个接口或抽象类,我还建议你在@author后面增加@see注释,将该接口或抽象类的所有实现类列出来,因为阅读者在阅读的时候,寻找接口或抽象类的实现类比较困难。Java代码 /** * 抽象的单表数组查询实现类,仅用于单表查询 * @author 范钢 * @see com.htxx.support.query.DefaultArrayQuery * @see com.htxx.support.query.DwrQuery */ public abstract class ArrayQuery implements ISingleQuery { ... /** * 抽象的单表数组查询实现类,仅用于单表查询 * @author 范钢 * @see com.htxx.support.query.DefaultArrayQuery * @see com.htxx.support.query.DwrQuery */public abstract class ArrayQuery implements ISingleQuery {...2.可维护性 软件的可维护性有几层意思,首先的意思就是能够适应软件在部署和使用中的各种情况。从这个角度上来说,它对我们的软件提出的要求就是不能将代码写死。1)代码不能写死 我曾经见我的同事将系统要读取的一个日志文件指定在C盘的一个固定目录下,如果系统部署时没有这个目录以及这个文件就会出错。如果他将这个决定路径下的目录改为相对路径,或者通过一个属性文件可以修改,代码岂不就写活了。一般来说,我在设计中需要使用日志文件、属性文件、配置文件,通常都是以下几个方式:将文件放到与类相同的目录,使用ClassLoader.getResource()来读取;将文件放到classpath目录下,用File的相对路径来读取;使用web.xml或另一个属性文件来制定读取路径。我也曾见另一家公司的软件要求,在部署的时候必须在C:/bea目录下,如果换成其它目录则不能正常运行。这样的设定常常为软件部署时带来许多的麻烦。如果服务器在该目录下已经没有多余空间,或者已经有其它软件,将是很挠头的事情。2)预测可能发生的变化 除此之外,在设计的时候,如果将一些关键参数放到配置文件中,可以为软件部署和使用带来更多的灵活性。要做到这一点,要求我们在软件设计时,应当更多地有更多的意识,考虑到软件应用中可能发生的变化。比如,有一次我在设计财务软件的时候,考虑到一些单据在制作时的前置条件,在不同企业使用的时候,可能要求不一样,有些企业可能要求严格些而有些要求松散些。考虑到这种可能的变化,我将前置条件设计为可配置的,就可能方便部署人员在实际部署中进行灵活变化。然而这样的配置,必要的注释说明是非常必要的。软件的可维护性的另一层意思就是软件的设计便于日后的变更。这一层意思与软件的可变更性是重合的。所有的软件设计理论的发展,都是从软件的可变更性这一要求逐渐展开的,它成为了软件设计理论的核心。3.可变更性 前面我提到了,软件的变更性是所有软件理论的核心,那么什么是软件的可变更性呢?按照现在的软件理论,客户对软件的需求时时刻刻在发生着变化。当软件设计好以后,为应对客户需求的变更而进行的代码修改,其所需要付出的代价,就是软件设计的可变更性。由于软件合理地设计,修改所付出的代价越小,则软件的可变更性越好,即代码设计的质量越高。一种非常理想的状态是,无论客户需求怎样变化,软件只需进行适当地修改就能够适应。但这之所以称之为理想状态,因为客户需求变化是有大有小的。如果客户需求变化非常大,即使再好的设计也无法应付,甚至重新开发。然而,客户需求的适当变化,一个合理地设计可以使得变更代价最小化,延续我们设计的软件的生命力。1)通过提高代码复用提高可维护性 我曾经遇到过这样一件事,我要维护的一个系统因为应用范围的扩大,它对机关级次的计算方式需要改变一种策略。如果这个项目统一采用一段公用方法来计算机关级次,这样一个修改实在太简单了,就是修改这个公用方法即可。但是,事实却不一样,对机关级次计算的代码遍布整个项目,甚至有些还写入到了那些复杂的SQL语句中。在这样一种情况下,这样一个需求的修改无异于需要遍历这个项目代码。这样一个实例显示了一个项目代码复用的重要,然而不幸的是,代码无法很好复用的情况遍布我们所有的项目。代码复用的道理十分简单,但要具体运作起来非常复杂,它除了需要很好的代码规划,还需要持续地代码重构。对整个系统的整体分析与合理规划可以根本地保证代码复用。系统分析师通过用例模型、领域模型、分析模型的一步一步分析,最后通过正向工程,生成系统需要设计的各种类及其各自的属性和方法。采用这种方法,功能被合理地划分到这个类中,可以很好地保证代码复用。采用以上方法虽然好,但技术难度较高,需要有高深的系统分析师,并不是所有项目都能普遍采用的,特别是时间比较紧张的项目。通过开发人员在设计过程中的重构,也许更加实用。当某个开发人员在开发一段代码时,发现该功能与前面已经开发功能相同,或者部分相同。这时,这个开发人员可以对前面已经开发的功能进行重构,将可以通用的代码提取出来,进行相应地改造,使其具有一定的通用性,便于各个地方可以使用。一些比较成功的项目组会指定一个专门管理通用代码的人,负责收集和整理项目组中各个成员编写的,可以通用的代码。这个负责人同时也应当具有一定的代码编写功力,因为将专用代码提升为通用代码,或者以前使用该通用代码的某个功能,由于业务变更,而对这个通用代码的变更要求,都对这个负责人提出了很高的能力要求。虽然后一种方式非常实用,但是它有些亡羊补牢的味道,不能从整体上对项目代码进行有效规划。正因为两种方法各有利弊,因此在项目中应当配合使用。2)利用设计模式提高可变更性 对于初学者,软件设计理论常常感觉晦涩难懂。一个快速提高软件质量的捷径就是利用设计模式。这里说的设计模式,不仅仅指经典的32个模式,是一切前人总结的,我们可以利用的、更加广泛的设计模式。a. if...else...这个我也不知道叫什么名字,最早是哪位大师总结的,它出现在Larman的《UML与模式应用》,也出现在出现在Mardin的《敏捷软件开发》。它是这样描述的:当你发现你必须要设计这样的代码:“if...elseif...elseif...else...”时,你应当想到你的代码应当重构一下了。我们先看看这样的代码有怎样的特点。Java代码 if(var.equals("A")){ doA(); }else if(var.equals("B")){ doB(); }else if(var.equals("C")){ doC(); }else{ doD(); } if(var.equals("A")){ doA(); }else if(var.equals("B")){ doB(); }else if(var.equals("C")){ doC(); }else{ doD(); }这样的代码很常见,也非常平常,我们大家都写过。但正是这样平常才隐藏着我们永远没有注意的问题。问题就在于,如果某一天这个选项不再仅仅是A、B、C,而是增加了新的选项,会怎样呢?你也许会说,那没有关系,我把代码改改就行。然而事实上并非如此,在大型软件研发与维护中有一个原则,每次的变更尽量不要去修改原有的代码。如果我们重构一下,能保证不修改原有代码,仅仅增加新的代码就能应付选项的增加,这就增加了这段代码的可维护性和可变更性,提高了代码质量。那么,我们应当如何去做呢?经过深入分析你会发现,这里存在一个对应关系,即A对应doA(),B对应doB()...如果将doA()、doB()、doC()...与原有代码解耦,问题就解决了。如何解耦呢?设计一个接口X以及它的实现A、B、C...每个类都包含一个方法doX(),并且将doA()的代码放到A.doX()中,将doB()的代码放到B.doX()中...经过以上的重构,代码还是这些代码,效果却完全不一样了。我们只需要这样写:Java代码 X x = factory.getBean(var); x.doX(); X x = factory.getBean(var);x.doX();这样就可以实现以上的功能了。我们看到这里有一个工厂,放着所有的A、B、C...并且与它们的key对应起来,并且写在配置文件中。如果出现新的选项时,通过修改配置文件就可以无*的增加下去。这个模式虽然有效提高了代码质量,但是不能滥用,并非只要出现if...else...就需要使用。由于它使用了工厂,一定程度上增加了代码复杂度,因此仅仅在选项较多,并且增加选项的可能性很大的情况下才可以使用。另外,要使用这个模式,继承我在附件中提供的抽象类XmlBuildFactoryFacade就可以快速建立一个工厂。如果你的项目放在spring或其它可配置框架中,也可以快速建立工厂。设计一个Map静态属性并使其V为这些A、B、C...这个工厂就建立起来了。b.策略模式 也许你看过策略模式(strategy model)的相关资料但没有留下太多的印象。一个简单的例子可以让你快速理解它。如果一个员工系统中,员工被分为临时工和正式工并且在不同的地方相应的行为不一样。在设计它们的时候,你肯定设计一个抽象的员工类,并且设计两个继承类:临时工和正式工。这样,通过下塑类型,可以在不同的地方表现出临时工和正式工的各自行为。在另一个系统中,员工被分为了销售人员、技术人员、管理人员并且也在不同的地方相应的行为不一样。同样,我们在设计时也是设计一个抽象的员工类,并且设计数个继承类:销售人员、技术人员、管理人员。现在,我们要把这两个系统合并起来,也就是说,在新的系统中,员工既被分为临时工和正式工,又被分为了销售人员、技术人员、管理人员,这时候如何设计。如果我们还是使用以往的设计,我们将不得不设计很多继承类:销售临时工、销售正式工、技术临时工、技术正式工...如此的设计,在随着划分的类型,以及每种类型的选项的增多,呈笛卡尔增长。通过以上一个系

  • 本文相关:
  • 快速了解node中的stream流是什么
  • node.js使用stream模块实现自定义流示例
  • node.js中你不可不精的stream(流)
  • 详解nodejs文件系统(fs)与流(stream)
  • node.js中流(stream)的使用方法示例
  • 深入nodejs中流(stream)的理解
  • nodejs stream 数据流使用手册
  • node.js中的流(stream)介绍
  • 说说node中的可读流和可写流的区别
  • 浅谈手写node可读流之流动模式
  • node.js文件上传处理示例
  • nodejs中express 常用中间件 body-parser 实现解析
  • 详解node.js一行命令上传本地文件到服务器
  • node.js编写cli的实例详解
  • node.js中的console.info方法使用说明
  • nodejs制作爬虫全过程(续)
  • 用node和express连接mysql实现登录注册的实现代码
  • 2019最新21个mysql高频面试题介绍
  • 使用npm安装最新版本nodejs
  • nodejs入门教程四:url相关模块用法分析
  • nodejs fs 读取文件流一次读取多少数据
  • nodejs 怎么下载远程文件并该名称
  • 如何从字符串中的Node.js创建流
  • 最近要写一篇论文,数据质量管理类或者数据处理应用实践类,有这方面资料朋友提拱一下,谢谢大家!
  • 如何提高代码质量6
  • 为什么我的视频文件直接复制到CD光盘中,就可以播放了,不用刻录吗
  • 把要写入的值组成Bit流,并每8位转化成Byte写入文件
  • 面对即将到来的中考你将如何做为话题写作文。。。。。。谢了
  • Java中提取txt文件,打开是一堆乱码,怎样转化为可读性文字
  • 作文要写关于交流的,谁能给几个论证交流重要性的实例?
  • 免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved