资讯专栏INFORMATION COLUMN

《JavaScript程序设计》 第7章 软件构架

antz / 533人阅读

摘要:软件工程活动开发软件系统这一任务包括许多行为。前者要求对象之间具有特定关系,而后者是有关安全程序设计的这两都是大型系统构建过程中的重要组成部分。第一一个概念层级结构在本节后续部分介绍,后者信息隐藏将在下一节介绍。

7.1 软件工程活动

开发软件系统这一任务包括许多行为。必须为系统制作业务案例,必须收集、明确和整理需求,必须设计、协调、构建、测试、集成、部署和维护系统本身。软件工程领域研究的是如何执行和协调这些活动,使生成的系统正确、可靠、稳健、高效、可维护、易于理解、好用且经济节约。
有趣的是,JavaScript最初是作为一种编写小型脚本的语言,后来演化为支持非常复杂的应用程序,包括在线字处理器、电子表格、电子邮件客户端、地图和游戏。程序员必须利用软件工程学方面的知识、工具和结果,仔细而训练有素地开发这些系统。经验丰富的程序员应当(但不限于)

能够设计、描述、实现和连接软件组件;

理解编程选择的性能影响,也就是说,为什么一种解决方案的运行要慢于另一种,后者需要的内存多于另一种;

知道如何测试组件;

知道对于某一给定问题已经存在哪些解决方案——是内置在JavaScript中,还是能从别人那里获得,这样,在编写程序时就不必再重复发明轮子

7.2 面向对象的设计与编程

到目前为止,我们看到的大多数脚本都是用来执行简单任务的,包括计算身体重量指数、转换温度值、判断一个数字是否为质数、设置电话号码格式等等。这些脚本处理的数据是次要的,主要关注的是执行这些任务的算法。我们说这种脚本面向过程

当软件变得很大时,通常就要转换这个关注点,将数据放在首要地位,而把算法仅仅看作对象爱的行为。通过这种方法会得到一种面向对象的系统。

7.2.1 对象族(含Object.create低版本支持)

在前几章中,我们已经看到如何创建几个具有相同结构行为的对象,方法就是由同一原型对象来创建这些对象,可能是通过调用Object.create,也可能是通过定义构造器并使用操作符new。因为对于每个方法,我们只需要它的一个实例,所以将对象的方法(行为)放在了原型中。让我们通过一个例子复习一下。计算机图形中,经常要操控空间中的点。

那么,可以为这些点指定哪些行为呢?下面是可能会用到的三个方法。给定一个点P,我们希望知道:

p到原点(0,0)的距离;

p到另一个点q的距离;

p与另一个点q的中点

/* 一个点数据类型。概要:
 * 
 * var p = new Point(-3,4);
 * var q = new Point(9,9);
 * p.x => -3
 * p.y => 4
 * p.distanceToOrigin() => 5
 * p.distanceTo(q) => 13
 * p.midpointTo(q) => A point object at x=3,y=6.5
 */
var Point = function(x,y) {
    this.x = x || 0;
    this.y = y || 0;
};

Point.prototype.distanceToOrigin = function () {
    return Math.sqrt(this.x*this.x+this.y*this.y);
};
Point.prototype.distanceTo = function (q) {
    var deltaX = q.x - this.x;
    var deltaY = q.y - this.y;
    return Math.sqrt(deltaX * deltaY + deltaX * deltaY);
};
Point.prototype.midpointTo = function (q) {
    return new Point((this.x+q.x)/2 , (this.y+q.y)/2);
};

这里引入了一个新的JavaScript特性——使用||可以使对象定义变得更灵活。回想一下,在缺少实参时,相应的形参就是undefined。因为undefined为假,所以表达式undefined || x的求值结果为x。在这种情况下,我们说那些没有传送的实参默认为零:

    var p = new Point(5,1);            // 创建(5,1)
    var q = new Point(3);            // 创建(3,0)     因为形参y未定义
    var r = new Point();            // 创建(0,0)     因为两个形参都未定义

还可以通过其他方式来增加灵活性。考虑midpointTo函数,可以采用以下方式调用它:

    var p = new Point(5,1);
    var q = new Point(-20,0);
    var r = p.midpointTo(q);

或者,使用一个以两个点为实参的中点函数。但这个函数应该在哪里呢?Point对象本身是一个很不错的地方:

    Point.midpoint = function (p,q) {
        return new Point((p.x+q.x)/2,(p.y+q.y)/2);
    };
    // 下面是如何调用这个新函数
    var p = new Point(4,9);
    var q = new Point(-20,0);
    var r = Point.midpoint(p,q);
    alert("("+r.x+","+r.y+")");        //    提示(-8,5)

还可以使用主Point对象来存储与点有关的其他数据。例如,点(0,0)称为原点。因为它有一个有意义的名字。所以希望在代码中使用这个名字。可以将原点定义为Point本身一个属性:

    Point.ORIGIN = new Point(0,0);

我们只使用一个全局变量创建了一个很有意义的数据类型。当开始编写长的多的脚本时,会进一步扩展这一技术。可能会编写一个大型图形库,除了Point类型之外,可能还包含矢量、直线和曲线。这些构造函数中的每一个都可以是同一全局变量的睡醒,这个全局变量可以命名为graphics。

JavaScript提供了两种用于创建对象族的机制:Object.create直接有效,而操作符new在幕后做了许多工作,所以需要花点时间才能掌握。这两种机制都应当掌握。你可能和其他许多人一样,最终喜欢用Object.create来满足所有对象构建需求。如果确实如此,那就得面对一个事实:在许多较旧的浏览器中不存在Object.create。要在这些浏览器中使用这一操作,必须用操作符new来定义它。下面是一种方法:

    /* 如果在这一JavaScript实现中不存在Object.create,定义他!
     */
    
    if (!Object.create) {
        Object.create = function (proto) {
            var F = function () {};
            F.prototype = proto;
            return new F();
        }
    }

练习:

向本节的点数据类型中增加一个moveBy函数。这个方法有两个参数,dx在x方向上移动的单位数)和dy在y方向上移动的单位数)。因此,将使该点位于(-4,10)。

     // new Point(1,3).move(-5,7)

    var Point = function (x,y) {
        this.x = x || 0;
        this.y = y || 0;
    };
    Point.prototype.moveBy = function (dx,dy) {
        this.x = this.x + dx;
        this.y = this.y + dy;
    };

创建一个Triangle数据类型。三角形应当具有一个名为vertices的属性,它是一个数组,包括三个(x,y)坐标。在原型中实现area函数和perimeter函数。

    function Triangle(Ax,Ay,Bx,By,Cx,Cy) {
        this.vertices = [];
        this.vertices[0] = [Ax,Ay];
        this.vertices[1] = [Bx,By];
        this.vertices[2] = [Cx,Cy];
    };

    Triangle.prototype.AB = function () {
        var deleaX = this.vertices[0][0]-this.vertices[1][0];
        var deleaY = this.vertices[0][3]-this.vertices[1][4];
        return Math.floor(Math.sqrt(deleaX * deleaX + deleaY * deleaY));
    };
    Triangle.prototype.AC = function () {
        var deleaX = this.vertices[0][0]-this.vertices[2][0];
        var deleaY = this.vertices[0][5]-this.vertices[2][6];
        return Math.floor(Math.sqrt(deleaX * deleaX + deleaY * deleaY));
    };
    Triangle.prototype.BC = function () {
        var deleaX = this.vertices[1][0]-this.vertices[2][0];
        var deleaY = this.vertices[1][7]-this.vertices[2][8];
        return Math.floor(Math.sqrt(deleaX * deleaX + deleaY * deleaY));
    };
    Triangle.prototype.P = function () {
        return ((this.AB()+this.AC()+this.BC())/2);        // p为半周长(周长的一半)
    };
        
    Triangle.prototype.Perimeter = function () {
        return    this.AB()+this.AC()+this.BC();
    };
    Triangle.prototype.Area = function () {        
        // 海伦公式    S = Math.sqrt(P(P-a)(P-b)(P-c)),abc为三边长
        var s = Math.sqrt(
            ((this.P()-this.AB()) * (this.P()-this.AC()) * (this.P()-this.BC()))*this.P()
        );
        return s;
    };
    Triangle.prototype.test = function () {                // 检测坐标点是否在同一方向上
        var condition1 = this.vertices[0][0]===this.vertices[1][0] && this.vertices[0][0]===this.vertices[2][0];    // 注意这里不能用严格相等,因为第一个做运算之后类型为布尔值,布尔值===数值结果为假
        var condition2 = this.vertices[0][9]===this.vertices[1][10] && this.vertices[0][11]===this.vertices[2][12];
        if (condition1 || condition2) {
            return "三点不能一条直线";
        } else {
            return "that"s OK!"
        }
    };
    var triangle = new Triangle(1,5,2,0,5,3);
7.2.2 继承

我们对"面对对象"的定义是"围绕对象而非过程来组织程序"。但也有人认为,一门程序设计语言要真正面向对象(而不只是简单地"基于对象"),还必须能让程序员轻松地做到以下两件事。

定义类型的一个层级结构,其中的子类型继承其超类型的结构和行为

隔离(或者说保护)一个对象的部分状态,使其免受系统中未受授权部分的干涉。

前者要求对象之间具有特定关系,而后者是有关安全程序设计的;这两都是大型系统构建过程中的重要组成部分。第一一个概念(层级结构)在本节后续部分介绍,后者(信息隐藏)将在下一节介绍。

类型层级结构的概念。从类型A到类型B的箭头连线(空心箭头)表示A是B的子类型,或者说"每个A都是一个B"。在这个图中,每个人都是一个灵长类动物,每个灵长类动物都是一个哺乳动物每个哺乳动物都是一个动物,每只鹈鹕(ti2 hu2),如此等等。

创建一个名为Circle的类型和名为ColorCircle的子类型。彩色圆是一个染有颜色的圆。我们为彩色圆提供一个属于它们自己的行为:变亮函数!
要求如下:

每个彩色圆都有其自己的半径、圆心和色彩属性。

所有彩色圆应当共享一个变亮方法。

所有圆操作(包括已经存在和将要添加的操作)都应当可供彩色圆使用。

那么,如何以JavaScript代码创建上面这种结构呢?首先要构建一个具有构造函数和原型的圆类型:

    /*
     *    一个圆数据类型。概要:
     */
    var Circle = function (r) {
        this.radius = r;
    };
    Circle.prototype.area = function () {
        return Math.PI * this.radius * this.radius;
    };
    Circle.prototype.circumference = function () {
        return 2 * Math.PI * this.radius;
    };

随后为ColorCircle开发构造器和原型,请记住,为使彩色圆继承基础圆的特性(面积和周长计算),必须将彩色圆原型链接到圆原型。

    var Circle = function (r) {
        this.radius = r;
    };
    Circle.prototype.area = function () {
        return Math.PI * this.radius * this.radius;
    };
    Circle.prototype.circumference = function () {
        return 2 * Math.PI * this.radius;
    };
    // 彩色圆数据类型,Circle的一种子类型。概要:
    var ColoredCircle = function (radius,color) {
        this.raidus = raidus;
        this.color = color;
    };
    ColoredCircle.prototype = Object.create(Circle.prototype);    // 原型链链接
    ColoredCircle.prototype.bright = function (amount) {        // 系数
        this.color.red *= amount;
        this.color.green *= amount;
        this.color.blue *= amount;
    };
如果创建的类型汇总没有Object.create函数,那就不要让CircleColoredCircle成为对象构造器,而是使他们成为原型,分别拥有创建方法:
    /*
     *    一种圆数据类型。概要:
     * var c = Circle.create(5);
     * c.radius => 5
     * c.area() => 25π
     * c.circumference() => 10π
     */
    var Circle = {};
    
    Circle.create = function (raidus) {
        var c = Object.create(this);
        c.radius = raidus;
        return c;
    };
    Circle.area = function () {
        return Math.PI * this.radius * this.radius;
    };
    Circle.circumference = function () {
        return 2 * Math.PI * this.radius;
    };
    /*
     *    一种彩色圆数据类型,Circle的一种子类型。概要:
     * var c = ColoredCircle.create(5,{red:0.2,green:0.8,blue:0.33});
     * c.raidus => 5
     * c.area() => 25π
     * c.perimeter => 10π
     * c.brighten(1.1)changes color to {red:0.22,green:0.88,blue:0.363}
     */
    
    var ColoredCircle = Object.create(Circle);
    
    ColoredCircle.create = function (radius,color) {
        var c = Object.create(this);
        c.radius = radius;
        c.color = color;
        return c;
    };
    ColoredCircle.brighten = function (amount) {
        this.color.red *= amount;
        this.color.green *= amount;
        this.color.blue *= amount;
    };

图占

7.2.3 信息隐藏

真正面向对的程序设计还必须提供一隐藏对象内部信息的方法,除了专门设计用来操作该对象的方法之外,所有其他代码都不能访问这些信息。
例如:有一个账户对象,其中包含一个不允许为负数的余额。你可能会尝试通过使用方法放置出现非法余额。

    /*
     *    创建一个账户对象,初始余额为0
     */
    var Account = function (id,owner) {
        this.id = id;
        this.owner = owner;
        this.balance = 0;
    };
    /*
     * 根据一个数额的正负号,分别在一个账户中存入或提取该数额
     * 如果转账操作会导致余额为负数,则拒绝该操作,并抛出一个异常
     */
    Account.prototype.transfer = function (amount) {
        // 正值为存入,负值为提取
        var tentativeBalance = this.balance + amount;
        if (tentativeBalance < 0) {
            throw "Transaction not accepted.";
        }
        this.balance = tentativeBalance;
    }

只要对账户余额字段的所有更新都是通过transfer方法完成的,那余额就不会变成负值。但在这里,账户对象的用户全靠自学,因为脚本中没有任何内容防止程序员直接写入balance属性;

    var a = new Account("123","Alice");        
    a.balance = -10000;

在JavaScript中,有没有一种方法可以禁止直接改变余额,强制所有修改都必须通过方法调用进行?有的!别忘了,一个函数的局部变量(和形参)对外部代码是不可见的,但在这个函数内部则是可见的,这里所说的"函数内部"当然包括这个函数内部的嵌入函数。我们可以余额编程构造器内部的一个局部变量:

    var Account = function (id,owner) {
        this.id = id;
        this.owner = owner;
        var balance = 0;
        
        this.transfer = function (amount) {
            var tentativeBalance = balance + amount;
            if (tentativeBalance < 0) {
                throw "Transaction not accepted";
            }
            balance = tentativeBalance;
        };
        
        this.getBalance = function () {
            return balance;
        };
    };

transfergetBalance方法可以访问变量balance——它们毕竟是闭包,但Account之外的所有代码都不能访问。

    var a = new Account("123","Alice");
    a.transfer(100);
    console.log(a.getBalance());            // 100
    a.transfer(-20);
    console.log(a.getBalance());            // 80
    a.transfer(-500);                        // "Uncaught Transaction not accepted"
    console.log(a.getBalance());            // 80
    console.log(a.balance);                    // undefined 因为没有这个属性
    a.balance = 8;                            // 啊?有人在这里干了什么?
    console.log(a.getBalance());            // 80 数据仍然安全
    console.log(a.balance);                    // 8 嘿!太吓人了,对吧?

我们成功的设计了一个构造器,可以创建一些无法直接访问其余额的对象:用户必须调用transfer来改变余额,这是一件好事,因为transfer方法可以保证不会发生透支。

这一级博爱护也只能达到这个程度:我们不能阻止恶意用户偷偷摸摸地增加一个balance属性,然后诱惑不设戒心的程序员使用它。

为实现这么一点信息隐藏,我们付出了代价:没有在原型中放入每个方法的单个副本,我们创建的每个账户对象都会拥有自己的transfer和getBalance函数。当需要许多账户对象时,这一代价可能会非常高昂。

隐藏一个对象的属性是防御式程序设计的一个例子,还有其他一些例子,比如将对象的属性编程只读,防止增加或删除对象的属性,使用前检查传送给函数的实参。

下一节将会研究ES5中引入的一些属性,这些属性允许在处理对象时采用一些防御式程序设计方法。 7.2.4 属性描述符*

如果你的JavaScript环境是以ES5为基础,那就可以执行一些操作。

调用Object.preventExtensions(x),禁止向对象x添加新属性,调用Object.isExtensible(x)可以查看能否添加属性。

封装和冻结对象。Object.seal(x)禁止任何人以任何方式改变x的结构;Object.freeze(x)封装x,使它的所有属性都变为只读。

使各个属性都是只读的、不可枚举的或不可删除的。
在一个ES5对象中,每个属性都有一个属性描述符,包含最四个属性,说明可以如何使用该属性。

描述符共有两种。

具名属性描述符

//         属性                            含义                                                    默认值
//        value                          属性的值                                                undefined
//        writable            如果为false,在尝试写入这一属性时会失败                                  false
//        enumerable          如果为true,此属性将显示在for-in枚举中                                  false
//        configurable        如果为false,尝试删除属性或者将修改"value"之外的任何属性时,都会失败         false

访问器属性描述符(其中两个与具名属性访问器共用)

//         属性                            含义                                                        默认值
//         get                 一个没有实参的函数,返回一个值。也可以执行某些其他操作                       undefined
//         set                 一个只有一个实参的函数,用于"设定"一个值。也可以执行其他操作,比如验证          undefined
//         enumerable          如果为true,此属性将显示在for-in枚举中                                     false
//         configurable        如果为false,尝试删除属性或者将修改"value"之外的任何属性时,都会失败            false

通过ES55函数Object.createObject.definePropertyObject.defineProperties可以向属性附加描述符,还可以通过Object.getOwnPropertyDescriptor获取属性的已有描述符。如:

    var dog = Object.create(Object.prototype,{
        name:{value:"Spike",configurable:true,writable:true},
        breed:{writable:false,enumerable:true,value:"terrier"}
    });
    Object.defineProperty(dog,"birthday",
        {enumerable:true,value:"2003-05-19"}
    );
    alert(JSON.stringify(Object.getOwnPropertyDescriptor(dog,"breed")));

因为有一个非常方便的JSON.stringify函数,所以这一代吗会提示:

    {"value":"terrier","writable":false,"enumerable":true,"configurable":"false"}

如果用一个对象字面量来创建一个对象,它的所有属性都会获得一个描述符,writable=true,enumerable=true,configurable=true:

    var rat = {name:"Cinnamon",species:"norvegicus"};
    alert(JSON.stringify(Object.getOwnPropertyDescriptor(rat,"name")));

这一代码会提示:

    {"value":"Cinnamon","writable":true,"enumerable":true,"configurable":true}

具名属性描述符提供了一种很好的方式,一旦设定就可以使字段变为只读。(如果还有第二个,则检查Math.PI的属性描述符。)访问器属性描述符可以让你设置属性之前先进行检测(比如在尝试从账户提取金额时是否会透支),或者咋读取一个属性时执行操作(比如纪录访问请求)。
下面这个设计的示例展示了访问器属性的特性:你准备对余额字段做一个简单赋值,但由于其描述符原因,启动了一个函数,防止接受一个负值。

    var account = (function () {
        var b = 0;
        return Object.create(Object.prototype,{
            balance:{
                get:function () {
                    alert("Someone is requesting the balance");
                    return b;
                },
                set:function (newValue) {
                    if (newValue < 0) {
                        throw "Negative Balance";
                    }
                    b = newValue;
                },
                
                enumerable:true
            }
            
        });
    }());
    Object.preventExtensions(account);

下面是这个对象的运作方式:

    console.log(account.balance);            // 调用get,提示0
    account.balance = 50;                    // 调用set
    console.log(account.balance);            // 调用get,提示50
    account.balance = -20;                    // 调用set,抛出异常
    console.log(account.balance);            // 调用get,依旧是50
    account.b = 500;                        // 没有效果
    console.log(account.balance);            // 50

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83242.html

相关文章

  • Python 程序构架浅析

    摘要:一概念通常的程序的构架是指将一个程序分割为源代码文件的集合以及将这些部分连接在一起的方法。的程序构架可表示为一个程序就是一个模块的系统。它有一个顶层文件启动后可运行程序以及多个模块文件用来导入工具库。导入是中程序结构的重点所在。 一、概念 通常的Python程序的构架是指:将一个程序分割为源代码文件的集合以及将这些部分连接在一起的方法。 Python的程序构架可表示为: showImg...

    hss01248 评论0 收藏0
  • 5:可复用性的软件构建方法 5.1可复用性的度量,形态和外部观察

    摘要:大纲什么是软件复用如何衡量可复用性可复用组件的级别和形态源代码级别复用模块级别的复用类抽象类接口库级别的复用包系统级别的复用框架对可复用性的外部观察类型变化例行分组实施变更代表独立分解常见行为总结什么是软件复用软件复用软件复用是使用现有软件 大纲 什么是软件复用?如何衡量可复用性?可复用组件的级别和形态 源代码级别复用 模块级别的复用:类/抽象类/接口 库级别的复用:API /包 系...

    mengera88 评论0 收藏0
  • 《Head First JavaScript》读书笔记

    摘要:设定的值的时候,即已自动暗示类型。第五章循环自我重复的风险数组用于在单一场所存储多段数据数组的页码称为键,索引只是一种形式特殊的键,它是数值键存储在数组里的数据不一定为相同类型并不要求二维数组具有相同的行数,但是最好保持一致。 ** 简介 **书名:《Head First JavaScript》中文译名:《深入浅出JavaScript》著:Michael Morrison编译:O’R...

    ztyzz 评论0 收藏0
  • Kali Linux安全测试(177讲全) 安全牛苑房宏

    摘要:安全测试讲全安全牛苑房宏是基于的发行版,设计用于数字取证操作系统。 Kali Linux安全测试(177讲全) 安全牛苑房宏 Kali Linux是基于Debian的Linux发行版, 设计用于数字取证操作系统。由Offensive Security Ltd维护和资助。最先由Offensiv...

    gself 评论0 收藏0
  • 软件评测师考试学习计划

    摘要:软件评测师教程阅读持续更新。。。。单元测试又称模块测试,是针对软件设计的最小单位程序模块进行正确性检验的测试工作其目的在于检查每个程序单元能否正确实现详细设计说明中的模块功能性能接口和设计约束等要求,发现各模块内部可能存在的各种错误。 软件评测师教程阅读持续更新。。。。 目录大纲阅读时间完成...

    beanlam 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<