《JavaScript 模式》读书笔记(7)— 设计模式2

  这一篇我们主要来学习装饰者模式、策略模式以及外观模式。其中装饰者模式稍微复杂一点,大家认真阅读,要自己动手去实现一下哦。

 

四、装饰者模式

  在装饰者模式中,可以在运行时动态添加附加功能到对象中。当处理静态类时,这可能是一个挑战。在JavaScript中,由于对象是可变的,因此,添加功能到对象中的过程本身并不是问题。

  装饰者模式的一个比较方便的特征在于其预期行为的可定制和可配置特性。可以从仅具有一些基本功能的普通对象开始,然后从可用装饰资源池中选择需要用于增强普通对象的那些功能,并且按照顺序进行装饰,尤其是当装饰顺序很重要的时候。

 

用法

  让我们仔细看一下该模式的使用示例。假设正在开发一个销售商品的web应用,每一笔新销售都是一个新的sale对象。该对象“知道”有关项目的价格,并且可以通过调用sale.getPrice()方法返回其价格。根据不同的情况,可以用额外的功能装饰这个对象。试想这样一个场景,销售客户在加拿大的魁北克省。在这种情况下,买方需要支付联邦税和魁北克省税。遵循这种装饰者模式,您会说需要使用联邦税和魁北克省税装饰者来“装饰”这个对象。然后,可以用价格格式化功能装饰该对象。这种应用场景看起来如下所示:

var sale = new Sale(100); //该价格为100美元
sale = sale.decorate('fedtax'); //增加联邦税
sale = sale.decorate('quebec'); //增加省级税
sale = sale.decorate('money'); //格式化成美元货币形式
sale.getPrice(); //"$112.88"

  在另一种情况下,买方可能在一个没有省税的省份,并且您可能也想使用加元的形式对其价格进行格式化,因此,您可以按照下列方式这样做:

var sale = new Sale(100); //该价格为100美元
sale = sale.decorate('fedtax'); //增加联邦税
sale = sale.decorate('CDN'); //格式化为CDN货币形式
sale.getPrice(); //"CDN$ 105.00"

  正如您所看到的,这是一种非常灵活的方法,可用于增加功能以及调整运行时对象。让我们来看看如何处理该模式的实现。

 

实现

  实现装饰者模式的其中一个方法是使得每个装饰者成为一个对象,并且该对象包含了应该被重载的方法。每个装饰者实际上继承了目前已经被前一个装饰者进行增强后的对象。每个装饰方法在uber(继承的对象)上调用了相同的方法并获取其值,此外它还继续执行了一些操作。

  最终的结果是,当您在第一个用法示例中执行sale.getPrice()时,调用了money装饰者的方法(见下图)。但是由于每个装饰方法首先调用父对象的方法,money的getPrice()将会首先调用quebec的getPrice(),这有需要依次调用fedtax的getPrice()等。该调用链一路攀升到由Sale()构造函数所实现的原始未经修饰的getPrice()。

  该实现是从一个构造函数和一个原型方法开始的:

function Sale(price) {
    this.price = price || 100;
}

Sale.prototype.getPrice = function() {
    return this.price;
};

  装饰者对象都将以构造函数的属性这种方式来实现:

Sale.decorators = {};

  让我们看一个装饰者示例。它是一个实现了自定义getPrice()方法的对象。请注意,该方法首先会从父对象的方法中获取值,然后再修改该值:

Sale.decorators.fedtax = {
    getPrice: function () {
        var price = this.uber.getPrice();
        price += price * 5 / 100;
        return price;
    }
}

  同样的,我们可以实现其他装饰者,其数量由需求来定。它们可以是核心Sale()功能的扩展,也可以实现成插件。它们设置可以“驻留”在其他文件中,并且可被第三方开发人员所开发和共享。

Sale.decorators.quebec = {
    getPrice: function () {
        var price = this.uber.getPrice();
        price += price * 7.5 / 100;
        return price;
    }
}

Sale.decorators.money = {
    getPrice: function () {
        return '$' + this.uber.getPrice().toFixed(2);
    }
}

Sale.decorators.cdn = {
    getPrice: function () {
        return 'CDN$ ' + this.uber.getPrice().toFixed(2);
    }
}

  最后,让我们看看称之为decorate()的“神奇”方法,它可以将所有的块拼接在一起。请记住,调用该方法的方式如下:

sale = sale.decorate("fedtax");

  "fedtax"字符串将对应于Sale.decorators.fedtax中实现的对象。新装饰的对象newobj将继承目前我们所拥有的对象(无论是原始对象,还是已经添加了最后的修饰者的对象),这也就是对象this。为了完成继承部分的代码,我们使用了前一章中的临时构造函数模式。首先,我们也设置了newobj的uber属性,以便于子对象可以访问父对象,然后,我们从装饰者中将所有的额外属性复制到新装饰的对象newobj中。最后,返回newobj,而在我们具体的用法例子中,它实际上成为了更新的sale对象。

Sale.prototype.decorate = function (decorator) {
    var F = function () {},
        overrides = this.constructor.decorators[decorator],
        i,newobj;
    
    F.prototype = this;
    newobj = new F();
    newobj.uber = F.prototype;
    for(i in overrides) {
        if(overrides.hasOwnProperty(i)) {
            newobj[i] = overrides[i];
        }
    }
    return newobj;
}

 

使用列表实现

  现在让我们探讨一个稍微不同的实现方法,它利用来JavaScript语言的动态性质,并且根本不需要使用继承。此外,并不是使每个装饰方法调用链中前面的方法,我们可以简单将前面方法的结果作为参数传递到下一个方法。

  这种实现方法还可以很容易的支持反装饰(undecorating)或撤销装饰,这意味着可以简单的从装饰者列表中删除一个项目。

  下面的用法示例将略微简单一点,这是由于我们没有将从decorate()返回的值赋给对象。在这个实现中,decorate()并没有对该对象执行任何操作,它只是将返回的值追加到列表中:

var sale = new Sale(100); //该价格为100美元
sale.decorate('fedtax'); //增加联邦税
sale.decorate('quebec'); //增加省级税
sale.decorate('money'); //格式化为美元货币形式
console.log(sale.getPrice()); //"$112.88"

  现在,Sale()构造函数中有一个装饰者列表并以此作为自身的属性:

function Sale(price) {
    this.price = (price > 0) || 100;
    this.decorators_list = [];
}

  可用装饰者将再次以Sale.decorators属性的方式实现。请注意,现在getPrice()方法变得更为简单了,这是因为它们并没有调用父对象的getPrice()以获得中间结果,而这个结果将作为参数传递给它们:

Sale.decorators = {};

Sale.decorators.fedtax = {
    getPrice: function (price) {
        return price + price * 5 / 100;
    }
};

Sale.decorators.quebec = {
    getPrice: function (price) {
        return price + price * 7.5 / 100;
    }
};

Sale.decorators.money = {
    getPrice: function (price) {
        return "$" + peice.toFixed(2);
    }
};

  在下面的代码中,有趣的部分发生在父对象的decorate()和getPrice()方法中。在以前的实现中,decorate()具有一定的复杂性,而getPrice()却是相当的简单。然而,在本实现中却采用了恰好相反的方式:decorate()仅用于追加列表,而getPrice()却完成所有工作。这些工作包括遍历当前添加的装饰者以及调用每个装饰者的getPrice()方法、传递从前一个方法中获得的结果。

  下面是一个完成的例子:

function Sale(price) {
    this.price = (price > 0) || 100;
    this.decorators_list = [];
}

Sale.decorators = {};

Sale.decorators.fedtax = {
    getPrice: function (price) {
        return price + price * 5 / 100;
    }
};

Sale.decorators.quebec = {
    getPrice: function (price) {
        return price + price * 7.5 / 100;
    }
};

Sale.decorators.money = {
    getPrice: function (price) {
        return "$" + price.toFixed(2);
    }
}

Sale.prototype.decorate = function(decorator) {
    this.decorators_list.push(decorator);
};

Sale.prototype.getPrice = function () {
    var price = this.price,
        i,
        max = this.decorators_list.length,
        name;
    
    for(i = 0; i < max;i += 1) {
        name = this.decorators_list[i];
        price = Sale.decorators[name].getPrice(price);
    }
    return price;
};

var sale = new Sale(100); //该价格为100美元
sale.decorate('fedtax'); //增加联邦税
sale.decorate('quebec'); //增加省级税
sale.decorate('money'); //格式化为美元货币形式
console.log(sale.getPrice());

  装饰者模式的第二种实现方法更为简单,并且也不涉及继承。此外,装饰方法也是非常简单的。所有这些工作都是通过“同意”被装饰的那个方法来完成的。在这个实现示例中,getPrice()是唯一允许装饰的方法。如果想拥有更多可以被装饰的方法,那么每个额外的装饰方法都需要重复遍历装饰者列表这一部分的代码。然而,这很容易抽象成一个辅助方法,通过它来接受方法并使其成为“可装饰”的方法。在这样的实现中,sale中的decorators_list属性变成了一个对象,且该对象中的每个属性都是以装饰对象数组中的方法和值命名。

 

五、策略模式

  策略模式支持您在运行时选择算法。代码的客户端可以使用同一个接口来工作,但是它却根据客户正在试图执行任务的上下文,从多个算法中选择用于处理特定任务的算法。

  使用策略模式的其中一个例子是解决表单验证的问题。可以创建一个具有validate()方法的验证器(validator)对象。无论表单的具体类型是什么,该方法都将会被调用,并且总是返回相同的结果,一个未经验证的数据列表以及任意的错误消息。

  但是根据具体的表单形势以及待验证的数据,验证其的客户端可能选择不同类型的检查方法。验证其将选择最佳的策略(strategy)以处理任务,并且将具体的数据验证委托给适当的算法。

 

数据验证示例

  假设有以下数据块,他可能来自于网页上的一个表单,而您需要验证它是否有效:

var data = {
    first_name: 'Super',
    last_name: 'Man',
    age: 'unknown',
    username: 'o_O'
};

  在这个具体的例子中,为了使验证器知道什么是最好的策略,首先需要配置该验证器,并且设置认为是有效的且可接受的规则。

  比如说在表单验证中,您对姓氏不作要求且接受任意字符作为名字,但是要求年龄必须为数字,并且用户名中仅出现字母和数字且无特殊符号。该配置如下所示:

validator.config = {
    first_name: 'isNonEmpty',
    age: 'isNumber',
    username: 'isAlphaNum'
};

  现在,validator对象已经配置完毕并可用于数据处理,您可以调用validator对象的validate()方法并将任意验证错误信息打印到控制台中:

validator.validate(data);
if(validator.hasErrors()) {
    console.log(validator.messages.join("\n"));
}

  上述语句将会打印出下列错误消息:

Invalid value for *age*, the value can only be a valid number,e.g. 1, 3.14 or 2010
Invalud value fror *username*, the value can only contain characters and numbers, no special symols

  现在,让我们看看验证程序是如何实现该validator的。用于检查的可用算法也是对象,它们具有一个预定义的接口, 提供了一个validate()方法和一个可用于提示错误消息的一行帮助信息。

// 非空值的检查
validator.types.isNonEmpty = {
    validate: function(value) {
        return value !== '';
    },
    instructions: 'the value cannot be empty'
};

// 检查值是否是一个数字
validator.types.isNumber = {
    validate: function(value) {
        return !isNaN(value);
    },
    instructions: 'the value can only be a valid number,e.g. 1, 3.14 or 2010'
};

// 检查该值是否只包含字母和数字
validator.types.isAlphaNum = {
    validate: function(value) {
        return !/[^a-z0-9]/i.test(value);
    },
    instructions: 'the value can only contain characters and numbers, no special symols'
};

  最后,核心的validator对象如下所示(这是一个完整的例子):

var validator = {
    // 所有可用的检查
    types: {},
    // 在当前验证会话中的错误消息
    messages: [],
    // 当前验证配置 
    // 名称:验证类型
    config: {},

    // 接口方法
    // ‘data’为键值对
    validate: function (data) {
        var i, msg, type, checker, result_ok;

        // 重置所有消息
        this.messages = [];
        for (i in data) {
            if (data.hasOwnProperty(i)) {
                type = this.config[i];
                checker = this.types[type];

                if (!type) {
                    continue; // 不需要验证
                }
                if (!checker) { //uh-oh
                    throw {
                        name: 'ValidationError',
                        messgae: 'No handler to validate type' + type
                    };
                }
                result_ok = checker.validate(data[i]);
                if (!result_ok) {
                    msg = "Invalid value for *" + i + "*," + checker.instructions;
                    this.messages.push(msg);
                }
            }
        }
        return this.hasErrors();
    },
    // 帮助程序
    hasErrors: function () {
        return this.messages.length !== 0;
    }
}

// 非空值的检查
validator.types.isNonEmpty = {
    validate: function (value) {
        return value !== '';
    },
    instructions: 'the value cannot be empty'
};

// 检查值是否是一个数字
validator.types.isNumber = {
    validate: function (value) {
        return !isNaN(value);
    },
    instructions: 'the value can only be a valid number,e.g. 1, 3.14 or 2010'
};

// 检查该值是否只包含字母和数字
validator.types.isAlphaNum = {
    validate: function (value) {
        return !/[^a-z0-9]/i.test(value);
    },
    instructions: 'the value can only contain characters and numbers, no special symols'
};

var data = {
    first_name: 'Super',
    last_name: 'Man',
    age: 'unknown',
    username: 'o_O'
};

validator.config = {
    first_name: 'isNonEmpty',
    age: 'isNumber',
    username: 'isAlphaNum'
};

validator.validate(data);
if (validator.hasErrors()) {
    console.log(validator.messages.join("\n"));
}

  正如您所看到的,validator对象是通用的,可以像这样将其保存下来以用于验证用例。增强validator对象的方法是添加更多的类型检查。如果在多个页面中使用它,很快就会有一个优良的特定检查集合。以后,针对每个新的用例,所需要做的就是配置该验证器并运行validate()方法。

 

六、外观模式

  外观(facade)模式是一种简单的模式,它为对象提供了一个可供选择的接口。这是一种非常好的设计实践,可保持方法的简洁性并且不会使他们处理过多的工作。如果原来有许多接受多个参数的uber方法,相比而言,按照本实现方法,最终将会创建更多数量的方法。有时候,两个或更多的方法可能普遍的被一起调用。在这样的情况下,创建另一个方法以包装重复的方法调用时非常有意义的。

  例如,当处理浏览器事件时,您有以下方法:

  stopPropagation():中止事件以避免其冒泡上升到父节点。

  preventDefault():阻止浏览器执行默认动作(例如,阻止下面的链接或提交表单)。

  以上是两个单独的方法且各自具有不同的目标,他们之间应保持互相独立,但在同一时间,他们经常被一起调用。为此,并不需要在程序中到处复制这两个方法调用,可以创建一个外观方法从而同时调用这两个方法:

var myevent = {
    // ...
    stop: function (e) {
        e.preventDefault();
        e.stopPropagation();
    }
}

  外观模式非常适合于浏览器脚本处理,据此可将浏览器之间的差异隐藏在外观之后。继续返回到前面的例子,可以添加代码来处理在IE的事件API中的差异。

var myevent = {
    // ...
    stop: function (e) {
        // 其他
        if(typeof e.preventDefault === 'function') {
            e.preventDefault();
        }
        if(typeof e.stopPropagation === 'function') {
            e.stopPropagation();
        }

        // IE
        if(typeof e.returnValue === 'boolean') {
            e.returnValue = false;
        }
        if(typeof e.cancelBubble === 'boolean') {
            e.cancelBubble = true;
        }
    }
}

  外观模式对于重新设计和重构的工作也很有帮助。当需要替换一个具有不同实现的对象时,不得不花费一段时间对他重新进行修改(这是一个复杂的对象),而且同时还要编写使用该对象的新代码。通过使用外观模式,可以首先考虑新对象的API,然后继续在原有对象的前面创建一个外观。这样,当您着手完全取代原有对象的时候,仅需修改更少的客户端代码,这是由于任何最新的客户端代码都已经使用了这个新API。

 

  这一篇,我们学习了装饰者模式,策略模式,外观模式。从这两篇文章的学习,我们发现,这些设计模式,实际上是非常贴近我们的工作生活的。这些设计模式,都可以在我们的开发工作中找到现存的影子。这回,你是不是能了解到一点,为什么设计模式这么重要。其实,设计模式,个人理解,就是贴合业务场景的某一种应用的具体实现方式。

  下一篇,我们学习最后三个设计模式。

posted @ 2020-09-02 14:30  Zaking  阅读(160)  评论(0编辑  收藏  举报