zoukankan      html  css  js  c++  java
  • 设计模式研究

    1.面向对象编程

    1.1 对象封装

    函数的简单编写到对象封装

    //简单的函数编写
    function checkName() {
        console.log("checkName")
        return true;
    }
    
    function checkEmail(a) {
        console.log("checkEmail");
        return false;
    }
    
    function checkPassword() {
        console.log("checkPassword");
        return true;
    }
    
    //调用
    checkName();
    checkName();
    checkPassword();
    
    //升级版1:用对象收编函数变量
    var checkObject = {
        checkName: function () {
            console.log("checkName")
            return true;
        },
        checkEmail: function () {
            console.log("checkEmail");
            return false;
        },
        checkObject: function () {
            console.log("checkPassword");
            return true;
        }
    }
    
    //调用
    checkObject.checkName();
    checkObject.checkEmail();
    checkObject.checkObject()
    
    
    //升级版2:对象的另一种形式,函数也是对象
    let checkObject_1 = function () {};
    checkObject_1.checkName = function () {
        console.log("checkName")
        return true;
    }
    checkObject_1.checkEmail = function () {
        console.log("checkEmail");
        return false;
    }
    checkObject_1.checkPassword = function () {
        console.log("checkPassword");
        return true;
    }
    
    //调用 
    checkObject_1.checkName();
    checkObject_1.checkEmail();
    checkObject_1.checkPassword();
    checkObject_1.id = 3;
    console.log(checkObject_1.id);//3
    
    //如果别人想用到这个对象方法就有些麻烦了,因为这个对象不能复制一份
    //上面的写法,如果其中一个调用者改变了这个对象,则这个对象就会发生变化
    
    //下面通过定义一个函数,函数内部定义了包含3个校验函数的对象
    //每次通过执行这个函数来返回一个新的对象的方式
    //这样每次调用下面函数返回的对象都是一个新的对象,调用者之间是不会影响的
    let checkObject_3 = function () {
        return {
            checkName: function () {
                console.log("checkName")
                return true;
            },
            checkEmail: function () {
                console.log("checkEmail");
                return false;
            },
            checkObject: function () {
                console.log("checkPassword");
                return true;
            }
        }
    }
    checkObject_3().checkName();
    checkObject_3().id = 3;
    console.log(checkObject_3().id);
    //undefined,每次函数返回都是一个新的对象
    
    

    2.创建型设计模式

    2.1 简单工厂模式

    简单工厂模式:又叫静态工厂方法,由一个工厂对象决定创建某一个产品对象类的实例。主要用来创建同一类对象。

    1.需求分析

    假设小白现在要完成登陆模块的需求,有下列4个需求

    显示警告框1:用户名不能多于16个字母或者数字

    显示警告框2:密码错误

    显示警告框3:用户名不存在,请重新输入,不过这里有些变化,就是要在警告框里面添加一个注册按钮

    显示警告框4:除了有确定取消按钮,还要提示一句“欢迎回来”

    //简单工厂模式
    
    //显示警告框1:用户名不能多于16个字母或者数字
    var LoginAlert = function (text) {
        this.content = text;
    }
    
    LoginAlert.prototype.show = function () {
        console.log("-----------")
        console.log(this.content);
    }
    
    var userNameAlert = new LoginAlert('用户名不能多于16个字母或者数字');
    userNameAlert.show();
    
    //显示警告框2:密码错误
    var passwordAlert = new LoginAlert('密码错误');
    passwordAlert.show();
    
    //显示警告框3:用户名不存在,请重新输入,不过这里有些变化,就是要在警告框里面添加一个注册按钮
    //这样的变化导致之前的类不可以复用,又要创建新的类了
    var LoginConfirm = function (text) {
        this.content2 = text;
    }
    
    LoginConfirm.prototype.show = function () {
        console.log("-----------")
        console.log(this.content2);
        console.log(this.content2);
    }
    
    var loginFailConfirm = new LoginConfirm('提示用户名不存在,请重新输入')
    loginFailConfirm.show();
    
    //显示警告框4:除了有确定取消按钮,还要提示一句“欢迎回来”
    //这样的变化导致之前的类不可以复用,又要创建新的类了
    var LoginPromt = function (text) {
        this.content3 = text;
    
    }
    LoginPromt.prototype.show = function () {
        console.log("-----------")
        console.log(this.content3);
        console.log("欢迎回来");
    }
    var loginPromt = new LoginPromt("提示欢迎回来");
    loginPromt.show();
    
    

    2.类的工厂模式

    假设小明来完成注册模块的功能,当用户输入的新用户名规范不正确的时候提示“用户名不能多于16个字母或者数字”;当用户输入的2次密码不对时候,提示’您两次输入的密码不一致,请重新输入', 这时候小明问项目经理:项目经理,登陆模块是谁做的,里面是不是也有提示框之类的需求。于是,小明找小白说借用一下写提示框的方法,小白将LoginAlert,LoginConfirm,LoginPromt3个类告诉了小明,但是小白写的3个方法比较分散,每次创建时候,还要找到对应的类,太麻烦了,而且在注册模块使用Login的前缀也不是很好,所以最好封装在一个方法里面

    这时候考虑使用简单工厂来改善一下代码,这样方便各个模块的人调用你写的方法,而不用关注其内部实现。

    简单工厂的函数,其他人都不用再关注这些对象到底依赖于哪个基类了,只要知道这个函数就行了,这种模式叫简单工厂模式。

    举个例子,比如体育商店卖体育器材,里面有很多体育用品,以及相关介绍,当来到体育用品店买一个篮球以及相关介绍时候,你只需要问售货员,他会帮你找到你所要的东西,如下的代码所示:

    var BasketBall = function () {
        this.intro = '篮球盛行于美国';
    }
    BasketBall.prototype = {
        getMember: function () {
            console.log('每个队伍需要5名队员');
        },
        getBallSize: function () {
            console.log('篮球很大');
        }
    }
    
    var FootBall = function () {
        this.intro = '足球在世界范围内很流行';
    }
    FootBall.prototype = {
        getMember: function () {
            console.log('每个队伍需要11名队员');
        },
        getBallSize: function () {
            console.log('足球很大')
        }
    }
    
    var Tennis = function () {
        this.intro = '每年有很多网球系列赛'
    }
    Tennis.prototype = {
        getMember: function () {
            console.log('每个队伍需要1名队员')
        },
        getBallSize: function () {
            console.log('网球很小')
        }
    }
    
    var SportsFactory = function (name) {
        switch (name) {
            case 'NBA':
                return new BasketBall();
            case 'wordCup':
                return new FootBall();
            case 'FrenchOpen':
                return new Tennis();
        }
    }
    //当想和小伙伴踢足球,只需要告诉店员我要每个足球即可。你使用这个商店工厂仅仅需要记住SportsFactory这个工厂对象就行了。
    //这个工厂魔术师会帮你找到你需要的一切
    var football = SportsFactory('wordCup');
    console.log(football);
    console.log(football.intro);
    football.getMember();
    
    //FootBall {intro: "足球在世界范围内很流行"}
    //足球在世界范围内很流行
    //每个队伍需要11名队员
    
    

    小白,照着上面的步骤修改了一下,代码如下所示:

    var PopFactory = function (name, content) {
        switch (name) {
            case 'alert':
                return new LoginAlert(content);
            case 'confirm':
                return new LoginConfirm(content);
            case 'prompt':
                return new LoginPromt(content);
        }
    }
    
    var alertDialog = PopFactory('alert', '提示框');
    alertDialog.show();
    
    // -----------
    // 提示框
    

    3.对象的工厂模式

    工厂模式的方式比之前已经好很多了,但是之前写的LoginAlert,LoginConfirm和LoginPromt3个类,有很多地方都是相同的,是可以抽象提取出来公用的,也可以使用简单工厂的方法来实现他们。

    小白很惊讶,那么怎么实现呢?还是和上面一样的结构吗?

    小明说:不太一样,详细一点说,简单工厂的理念就是创建对象,像上面那种方式是不同的类实例化,不过除此之外简单工厂模式还可以用来创建相似对象,而你创建的这3个类(对象)很多地方都比较相似,比如都有关闭按钮,都有提示文案,所以你可以通过将这些相似东西抽取,不相似的针对性处理即可,这有点类似我们之前学习继承时的寄生模式,但是不太一样,因为这里没有父类,所以无需做任何集成,这里只需要创建一个对象,然后对这个对象大量扩展方法和属性,并最终将对象返回出来。

    举个例子,比如像创建一些书,那么这些书都有一些相似的地方,比如目录,页码等。有一些不相似的地方,比如书名、出版时间、书的类型等,对于创建的对象相似的属性好处理,不同的属性就要针对性地处理了,比如我们将不同的属性作为参数传递进来处理。如下面的代码

    function createBook(name, time, type) {
        //创建一个对象,并对对象拓展属性和方法
        var o = new Object();
        o.name = name;
        o.time = time;
        o.type = type;
        o.getName = function () {
            console.log(this.name);
        }
        //将对象返回
        return o;
    }
    
    var book1 = createBook('js book', 2014, 'js');
    var book2 = createBook('css book', 2013, 'css');
    book1.getName();
    //js book
    book2.getName();
    // css book
    

    真的很想寄生式继承,只不过这里的o没有继承任何类或对象。

    所以将上面注册相关的3个类改成工厂模式也就很简单了,首先抽取他们的相同点,比如共有属性this.content,原型共有方法show,当然也有不同点,比如确认框和提示框的按钮等,所以代码如下:

    function createPop(type, text) {
        var o = new Object();
        o.content = text;
        o.show = function () {
            console.log(this.content);
        }
    
        if (type === 'alert') {
            //警告框差异部分
            o.alert = 'LoginAlert';
            console.log('LoginAlert');
        }
        if (type === 'promt') {
            //提示框差异部分
            o.promt = 'LoginPromt';
            console.log('LoginPromt');
        }
        if (type === 'confirm') {
            //确认框差异部分
            o.confirm = 'LoginConfirm';
            console.log('LoginConfirm');
        }
        return o;
    }
    
    // var PopFactory = function (name, content) {
    //     // switch (name) {
    //     //     case 'alert':
    //     //         return createPop(name,content);
    //     //     case 'confirm':
    //     //         return createPop(name,content);
    //     //     case 'prompt':
    //     //         return createPop(name,content);
    //     // }
    //     //可以简化为下面的形式
    //     return createPop(name,content);
    // }
    
    var alertDialog = createPop('alert', '提示框');
    alertDialog.show();
    // LoginAlert
    // 提示框
    

    4.工厂模式总结

    2和3的工厂模式还是有一定区别的,2是通过类实例化对象 创建的,3是通过创建一个新对象然后包装增强其属性和功能实现的,他们之间的差异性也造成前面通过类创建的对象,如果这些类继承同一个父类,那么他们父类的原型上的方法是可以共用的,而后面通过寄生方式创建的对象都是一个新的个体,那么他们的方法就不能公用了。当然选择哪种工厂方式来实现需求还要看你是如何分析你的需求的

    2.2 工厂方法模式

    工厂方法模式:通过对产品类的抽象使其创建业务主要负责用于创建多类产品的实例。

    1.需求分析

    需求:广告是公司主要的一个经济来源,这不,很多企业等着来公司首页打广告呢。

    小白,公司来了一批广告资源需要投放,关于计算机培训的。一批是java的,用绿色字体;还有一批是PHP的,要用黄色字体,红色背景。小白开始创建2个类,然后通过实例对象方式来完成这个需求。

    //创建Java学科类
    var Java = function (content) {
        //将内容保存在content里面以备以后使用
        this.content = content;
        //创建对象时,通过闭包,直接执行 ,将内容按需求的样式插入到页面内
        (function (content) {
            console.log(content);
            var div = document.createElement('div');
            div.innerHTML = content;
            div.style.color = 'green';
            document.getElementById('container').appendChild(div);
        })(content);
    }
    //创建PHP学科类
    var Php = function (content) {
        //将内容保存在content里面以备以后使用
        this.content = content;
        (function (content) {
            console.log(content);
            var div = document.createElement('div');
            div.innerHTML = content;
            div.style.color = 'yellow';
            document.getElementById('container').appendChild(div);
        })(content);
    }
    //创建Javascript学科类
    var JavaScript = function (content) {
        //将内容保存在content里面以备以后使用
        this.content = content;
        (function (content) {
            console.log(content);
            var div = document.createElement('div');
            div.innerHTML = content;
            div.style.color = 'pink';
            document.getElementById('container').appendChild(div);
        })(content);
    }
    
    //小白想着刚学完简单工厂模式,心想:正好派上用场了,就用简单工厂模式来实现,
    //这样日后再创建对象找工厂就好了
    function JobFactory(type, content) {
        switch (type) {
            case 'Java':
                return new Java(content);
            case 'php':
                return new Php(content);
            case 'Javascript':
                return new JavaScript(content);
        }
    }
    
    //测试用例
    JobFactory('Javascript', 'Javascript哪家强');
    //Javascript哪家强
    

    小白,又来了一批UI学科,红色边框,小白沉默了...

    需求总在变,不知道哪种解决方式更好,开始需求简单,我就直接创建对象,后来需求多了,我就用简单工厂方法重构,但现在又变了,我不仅仅呀欧添加了,还要修改工厂函数,而我更担心的是未来的需求还会变...

    小明说:不用担心,需求变化是正常的,刚才你用简单工厂模式遇到的问题就是没添加一个类就要修改2处是吧,所以说你可以使用一些工厂方法模式,这样以后每需要一个类,你只需要添加这个类就行,其他的不用操心。

    工厂方法模式本意是将实际创建对象工作推迟子类当中。这样核心类就成了抽象类,不过对于Javascript不必这样深究,Javascript没有像传统创建抽象类那样的方式轻易创建抽象类,所以在Javascipt中实现工厂方法模式我们只需要参考它的核心思想即可。所以将工厂方法看做是一个实例化对象的工厂类。安全起见,我们采用安全模式类,而我们将创建对象的基类放在工厂方法类的原型中即可。

    2.安全模式类

    安全模式类是说可以屏蔽使用这对类的错误使用造成的错误。比如对于一个类的创建,我们知道类的前面是需要有new关键字的,如果其他人不知道这个对象是一个类,那在使用时很可能忽略new关键字直接执行类,此时得到的并不是我们预期的对象,如下所示:

    var Demo = function () {
    }
    Demo.prototype = {
        show: function () {
            console.log('成功获取!')
        }
    }
    
    var d = new Demo();
    d.show();
    //成功获取!
    var dd = Demo();
    dd.show();
    //TypeError: Cannot read property 'show' of undefined
    
    

    安全模式就是为了解决上面这个问题,解决方法很简单,就是在构造函数开始时先判断当前对象this是不是指向类(Demo),如果是则通过new关键字创建对象,如果不是则说明类在全局作用域中执行(通常情况下),那么既然在全局作用域中执行。当然this就会执行window了。这样我们就要重新返回新创建的对象了
    如下代码所示:

    var Demo = function () {
        if (!(this instanceof Demo)) {
            return new Demo();
        }
        console.log('new demo');
    }
    Demo.prototype = {
        show: function () {
            console.log('成功获取!')
        }
    }
    
    // var dddd = Demo();
    // dddd.show();
    //成功获取!
    var ddd = new Demo();
    ddd.show();
    //new demo
    //成功获取!
    

    3.安全的工厂方法

    //安全模式创建的工厂类
    var Factory = function (type, content) {
        if (this instanceof Factory) {
            var s = new this[type](content);
            return s;
        } else {
            return new Factory(type, content);
        }
    }
    
    //工厂原型中设置创建所有类型数据对象的基类
    //工厂原型中设置创建所有类型数据对象的基类
    Factory.prototype = {
        Java: function (content) {
            //将内容保存在content里面以备以后使用
            this.content = content;
            console.log('Java');
            // (function (content) {
            //     console.log(content);
            //     var div = document.createElement('div');
            //     div.innerHTML = content;
            //     div.style.color = 'yellow';
            //     document.getElementById('container').appendChild(div);
            // })(content);
        },
        JavaScript: function (content) {
            this.content = content;
            console.log('JavaScript');
            // (function (content) {
            //     console.log(content);
            //     var div = document.createElement('div');
            //     div.innerHTML = content;
            //     div.style.color = 'pink';
            //     document.getElementById('container').appendChild(div);
            // })(content);
        },
        UI: function (content) {
            this.content = content;
            console.log('UI');
            // (function (content) {
            //     console.log(content);
            //     var div = document.createElement('div');
            //     div.innerHTML = content;
            //     div.style.color = 'yellow';
            //     document.getElementById('container').appendChild(div);
            // })(content);
        },
        php: function (content) {
            this.content = content;
            console.log('php');
            // (function (content) {
            //     console.log(content);
            //     var div = document.createElement('div');
            //     div.innerHTML = content;
            //     div.style.color = 'green';
            //     document.getElementById('container').appendChild(div);
            // })(content);
        }
    };
    

    添加最新的广告数据

    var data=[
        {type:'JavaScript',content:'JavaScript哪家强'},
        {type:'Java',content:'Java'},
        {type:'UI',content:'UI'},
        {type:'php',content:'php哪家强'}    
    ]
    data.forEach(function(perValue){
        Factory(perValue.type,perValue.content);
    })
    
    //JavaScript
    //Java
    //UI
    //php
    

    4.工厂方法总结:

    对于创建多类对象,前面学过的简单工厂模式就不太适用了,这是简单工厂模式的应用局限,当然这正是工厂方法模式的价值所在,通过工厂方法模式我们可以轻松创建多个类的实例对象,这样工厂方法对象在创建对象的方法也避免了使用者与对象类之间的耦合,用户不必关系创建该对象的具体类,只需要调用工厂方法即可。

    2.3 抽象工厂模式

    抽象工厂模式:通过对类的工厂抽象使其业务用于对产品类簇的创建,而不负责创建某一类产品的实例。

    1.需求分析

    小白说:小明,上次你为我介绍工厂方法时曾提到过抽象类,那么在Javascript中如何创建一个抽象类呀?抽象类有什么用?与抽象类相关的都有哪些涉及模式呀?

    小明:抽象类?在Javascript中abstract还是一个保留字,所以目前来说还不能像传统面向对象那么轻松的创建。抽象类是一种声明但不能使用的类,当使用时就会报错。不过Javascript是灵活的,所以我们可以在类的方法中手动地抛出错误来模拟抽象类。如下代码

    //汽车抽象类,当使用实例对象的方法时会抛出错误
    var Car = function () {};
    Car.prototype = {
        getPrice: function () {
            return new Error('抽象方法不能调用');
        },
        getSpeed: function () {
            return new Error('抽象方法不能调用');
        }
    }
    

    我们看到我们创建的这个Car类其实什么都不能做,创建时没有任何属性,然而原型prototype上的方法也不能使用,否则会报错。但在继承上却是很有用的,因为定义了一种类,并定义了该类所必备的方法,如果在子类中没有重写这些方法,那么当调用时能找到这些方法就会报错

    2.抽象工厂模式

    面向对象里面有一种常见的模式叫做抽象工厂模式,这个模式可不简单,在Javascript中一般不用来创建具体对象。因为抽象类中定义的方法只是显性地定义一些功能,但没有具体的实现,而一个对象是要具有一套完整的功能的,所以用抽象类创建的对象当然也是抽象的了,所以一般用它作为父类来创建一些子类。如下代码;

    //抽象工厂方法
    var VehicelFactory = function (subType, superType) {
        //判断抽象工厂中是否有该抽象类
        if (typeof VehicelFactory[superType] === 'function') {
            //缓存类
            function F() {};
            //继承父类属性和方法
            F.prototype = new VehicelFactory[superType]();
            //将子类constructor指向子类
            subType.constructor = subType;
            //子类原型继承'父类'
            subType.prototype = new F();
        } else {
            throw new Error('未创建该抽象类')
        }
    }
    //小汽车抽象类
    VehicelFactory.Car=function() {
        this.type='car';
    }
    
    VehicelFactory.Car.prototype={
        getPrice:function() {
            return new Error('抽象方法不能调用');
        },
        getSpeed:function() {
            return new Error('抽象方法不能调用');
        }
    }
    
    //公交车抽象类
    VehicelFactory.Bus=function() {
        this.type='bus';
    }
    
    VehicelFactory.Bus.prototype-{
        getPrice:function() {
            return new Error('抽象方法不能调用');
        },
        getSpeed:function() {
            return new Error('抽象方法不能调用');
        }   
    }
    //货车抽象类
    VehicelFactory.Truck=function() {
        this.type='bus';
    }
    
    VehicelFactory.Truck.prototype-{
        getPrice:function() {
            return new Error('抽象方法不能调用');
        },
        getSpeed:function() {
            return new Error('抽象方法不能调用');
        }   
    }
    
    

    可以看出,工厂其实是一个实现子类继承父类的方法,在这个方法中我们需要通过传递子类以及要继承父类(抽象类)的名称,并且在抽象工厂方法中又增加了一次对抽象类存在性的一次判断,如果存在,则将子类继承父类的方法。然后子类通过寄生式继承。继承过程中有一个地方要注意,就是在对过渡类的原型继承时,我们不是继承父类的原型,而是通过new关键字复制的父类的一个实例,这么做是因为过渡类不应仅仅继承父类的原型方法,还要继承父类的对象属性,所以通过new关键字

    对抽象工厂添加抽象类也很特殊,因为抽象工厂是个方法不需要实例化对象,故只需要一份,因此直接为抽象工厂添加类的属性即可,于是通过点语法在抽象工厂上添加需要的三个汽车簇抽象类Car,Bus,Track。

    3.抽象与实现

    如何实现呢?

    既然抽象工厂是用来创建子类的,(本例中其实是让子类继承父类,是对子类的一个拓展),所以我们需要一些产品子类,然后让子类继承相应的产品簇抽象类,如下代码

    //宝马汽车子类
    var BMW = function (price, speed) {
        this.price = price;
        this.speed = speed;
    }
    
    //抽象工厂实现对Car抽象类的继承
    VehicelFactory(BMW, 'Car');
    BMW.prototype.getPrice = function () {
        console.log(this.price)
        return this.price;
    }
    
    //宇通汽车子类
    var YUTONG = function (price, speed) {
        this.price = price;
        this.speed = speed;
    }
    
    //抽象工厂实现对Bus抽象类的继承
    VehicelFactory(YUTONG, 'Bus');
    YUTONG.prototype.getPrice = function () {
        console.log(this.price)
        return this.price;
    }
    
    //奔驰汽车子类
    var BenzTruck = function (price, speed) {
        this.price = price;
        this.speed = speed;
    }
    
    //抽象工厂实现对Truck抽象类的继承
    VehicelFactory(BenzTruck, 'Truck');
    BenzTruck.prototype.getPrice = function () {
        console.log(this.price)
        return this.price;
    }
    
    
    //具体实例
    var dmw = new BMW('price=100', 'speed=10');
    console.log(dmw.getPrice())
    //price=100
    console.log(dmw.type);
    //car
    console.log(dmw.getType());
    //TypeError: dmw.getType is not a function
    

    通过抽象工厂,我们就能知道每个子类到底是哪一种类别了,然后他们也具备了该类所必备的属性和方法了。

    4.抽象工厂模式总结

    抽象工厂模式是设计模式中最抽象的一种,也是创建模式中唯一一种抽象化设计模式。该模式创建出的结果不是一个真实的对象实例,而是一个类簇,它制定了类的结构,这就是区别于简单工厂模式创建单一对象,工厂方法模式创建多个对象。当然由于Javascript中不支持抽象化创建与虚拟方法,所以导致这种模式不能像其他面向对象语言中应用得那么广泛。

    2.4 建造者模式

    1.需求分析

    小明:刚接到任务,有一些找工作的人,想借助网站发布简历

    小白:哦,人很多吗?都有什么要求,要不要我帮你选选呀?

    小明:不是咱们公司招聘,是将他们的简历往外推销。不过接到的简历还真不少,这些简历有一个要求,除了将他们的兴趣爱好以及一些特长发布在页面里,其他信息,如联系方式不要发布到网站上。要让需求公司来找咱们。他们想找的工作是可以分类的,比如喜欢编程的人来说他们要找的职位就是工程师了,这里可能还有一些描述,比如‘每天沉醉于编程...'

    小白:这样创建他们需要不少工厂方法吧,很多部分需要抽象提取,不过首先要明确创建内容。

    比如创建用户信息 如用户姓名等要独立处理,因为他们是要隐藏显示的。

    比如这些应聘者也要独立创建,因为他们代表一个整体。

    还有这些工作职位也要独立创建,他们是应聘者的一部分,而且种类很多。

    这样一来,要创建的东西就多了,不仅应聘者需要创建,每位应聘者的信息、应聘职位都要创建,不过前面提到的几种模式好像都不太适合这个需求。

    引入建造者模式:工厂模式主要是为了创建对象实例或者类簇,关心的是最后产出的是什么。不关心你创建的整个过程,仅仅需要知道你最终创建的结果。所以通过工厂模式我们得到的是对象实例或者类簇。然而建造者模式在创建对象时更复杂一些,虽然其目的也是为了创建对象,但是更多关心的是创建这个对象的整个过程,甚至于创建对象的每个细节, 比如创建一个人,我们创建的结果不仅仅要得到人的实例,还要关注创建人的时候,这个人应该穿什么衣服,男的还是女的。所以建造者模式更注重的是创建的细节。

    本例中我们需要的不仅仅是一个应聘者的一个实例,还要在创建过程中注意一下这个应聘者都有哪些兴趣爱好,他的姓名等信息,他所期望的职位是什么。那么这些关注点都是需要我们创建的。如下代码

    //创建一位人类
    var Human = function (param) {
        //技能,小技巧,如果param.skill有值,就取这个值,如果没有,就是保密的信息
        this.skill = param && param.skill || '保密';
        //兴趣爱好
        this.hobby = param && param.hobby || '保密';
    }
    Human.prototype = {
        getSkill: function () {
            return this.skill;
        },
        getHobby: function () {
            return this.hobby;
        }
    }
    
    //实例化姓名类
    var Named = function (name) {
        var that = this;
        //构造器
        //构造器解析姓名的姓与名
        (function (name, that) {
            that.wholeName = name;
            if (name.indexOf(' ') > -1) {
                that.FirstName = name.slice(0, name.indexOf(''));
                that.SecondName = name.slice(name.indexOf(' '));
            }
        })(name, that);
    }
    
    //实例化职位类
    var Work = function (work) {
        var that = this;
        //构造器
        //构造器中通过传入的职位特征来设置相应职位以及描述
        (function (work, that) {
            switch (work) {
                case 'code':
                    that.work = '工程师';
                    that.workDescript = '每天沉醉于编程'
                    break;
                case UI:
                case UE:
                    that.work = '设计师';
                    that.workDescript = '设计更似一种艺术';
                    break;
                case 'teach':
                    that.work = '教师';
                    that.workDescript = '分享也是一种快乐'
                    break;
                default:
                    that.work = work;
                    that.workDescript = '对不起,我们还不清楚您选择职位的相关描述'
            }
        })(name, that);
    }
    //更换期望的职位
    Work.prototype.changeWork = function (work) {
        this.work = work;
    }
    //添加对职位的描述
    Work.prototype.changeDescript = function (setence) {
        this.workDescript = setence;
    }
    
    

    2.需求完成

    这样我们就创建了抽象出来的3个类---应聘者类,姓名解析类和期望职位类。我们的目的是要创建一位应聘者,所以需要上面抽象的3个类。这样我们写一个建造者类,在建造者类中我们要童工对这3个类进行组合调用,就可以创建出一个完整的应聘者对象。

    var Person = function (name, work) {
        //创建应聘者缓存对象
        var _person = new Human();
        //创建应聘者姓名解析对象
        _person.name = new Named(name);
        //创建应聘者期望职位
        _person.work = new Work(work);
        //将创建的应聘者对象返回
        return _person;
    }
    
    //测试代码
    var person=new Person('xiao ming','code');
    console.log(person.skill);
    //保密
    console.log(person.name.FirstName);
    // xiaoxiaoxiao
    console.log(person.work.work);
    // 工程师
    console.log(person.work.workDescript);
    // 每天沉醉于编程
    person.work.changeDescript('更改一下职位描述');
    console.log(person.work.workDescript);
    // 更改一下职位描述
    

    如上所述,应聘者建造者中我们分成3个部分来创建一个应聘者对象。以前工厂模式创建出来的是一个对象,别无他求,所以那仅仅是一个实实在在的创建过程。而建造者模式就有所不同,它不仅可以得到创建的结果,也参与了创建的具体过程,对于创建的具体细节也参与了干涉,创建的对象更复杂。

    3.建造者模式总结

    回忆前面学过的几种工厂模式,他们都有一个共同特点,就是创建的结果都是一个完整的个体,我们对创建过程不得而知,我们只了解得到的创建结果对象。而在建造者模式中我们关心的是对象床架过程,因此常将创建对象的类模块化,这样使被创建的类的每个模块都可以得到灵活的应用与高质量的复用。

    当然这种方式对于整体对象类的拆分无形中增加了结构的复杂性,因此如果对象粒度很小,或者模块间的复用率很低并且变动不太,我们最好还是要创建整体对象。

    2.5 原型模式

    原型模式:用原型实例指向创建对象的类,使用于创建新的对象的类共享原型对象的属性以及方法。

    1.需求分析

    假设页面中有很多焦点图(网页中很常见的一种图片轮播,切换效果),如果我们要实现这些焦点图最好的方式 就是通过创建对象来一一实现,所以我们就需要有一个焦点类图,比如这个类定义为LoopImages。

    //图片轮播类
    var LoopImages = function (imgArr, container) {
        this.imageArray = imgArr; //轮播图片数组
        this.container = container; //轮播图片容器
        this.createImage = function () {} //创建轮播图片
        this.changeImage = function () {}; //切换下一张图片
    }
    

    如果一个页面中多个这类焦点图,其切换动画一般是多样化的,有的可能是上下切换,有的可能是左右切换,有的可能是渐隐渐现的。创建的轮播类应该是多样化的,同样切换效果也是多样化的。因此让不同特效类来继承这个基类,然后对于差异化的需求通过重写这些继承下来的属性或方法来解决。如下代码

    //上下滑动切换类
    var SlideLoopImg = function (imgArr, container) {
        //构造函数继承图片轮播类
        LoopImages.call(this, imgArr, container);
        //重写继承的切换的下一张图片方法
        this.changeImage = function () {
            console.log('SlideLoopImage changeImage function');
        }
    }
    
    //渐隐渐现类
    var FadeLoopImage = function (imgArr, container, arrow) {
        LoopImages.call(this, imgArr, container);
        //切换箭头私有变量
        this.arrow = arrow;
        this.changeImage = function () {
            console.log('FadeLoopImage chagneImage function');
        }
    }
    
    //创建一个渐隐渐现切换图片类
    var fadeImg = new FadeLoopImage(['123.jpg', '123.jpg', '123.jpg'], 'slide', ['left.jpg', 'right.jpg']);
    fadeImg.changeImage();
    

    2.最优解决方案

    基类LoopImages,作为基类是要被子类继承的,此时将属性和方法都写在基类的构造函数里会有一些问题,比如每次子类继承都要创建一次父类,加入构造函数中创建时存在很多耗时的逻辑,或者每次初始化做一些重复性的东西,这样性能消耗还是很大的。为了提高性能,我们需要有一种共享机制,每当创建基类时候,每次创建的一些简单而又有一些差异化的属性我们可以放在构造函数中,而我们将消耗资源较大的方法放在基类的原型中,这样会避免很多不必要的消耗。这就是原型模式。

    原型 模式就是将可复用的、可共享的、耗时大的从基类中提取出来然后放在原型中,然后子类通过继承将方法和属性继承下来,对于子类说那些需要重写的方法进行重写,这样子类创建的对象就既具有子类的属性和方法也共享了类的原型方法,如下代码:

    var LoopImage = function (imgArr, container) {
        this.imgArray = imgArr; //轮播图片数组
        this.container = container; //轮播图片容器
    }
    
    LoopImage.prototype = {
        //创建轮播图片
        createImage: function () {
            console.log('LoopImage createImage function');
        },
        //切换下一张图片
        changeImage: function () {
            console.log('LoopImage changeImage function');
        }
    }
    
    //上下滑动切换类
    var SlideLoopImage = function (imgArr, container) {
        //构造函数继承图片轮播类
        LoopImage.call(this, imgArr, container);
    }
    
    SlideLoopImage.prototype = new LoopImage();
    //重写继承的切换下一张图片方法
    SlideLoopImage.prototype.changeImage = function () {
        console.log('SlideLoopImage changeImage function');
    }
    
    //渐隐渐现类
    var FadeLoopImage = function (imgArr, container, arrow) {
        LoopImage.call(this, imgArr, container);
        //切换箭头私有变量
        this.arrow = arrow;
    }
    FadeLoopImage.prototype = new LoopImage();
    FadeLoopImage.prototype.changeImage = function () {
        console.log('FadeLoopImage changeImage function');
    }
    
    //测试用例
    var fadeImg = new FadeLoopImage(['123.jpg', '123.jpg', '123.jpg'], 'slide', ['left.jpg', 'right.jpg']);
    console.log(fadeImg);
    // arrow:Array(2) ["left.jpg", "right.jpg"]
    // container:"slide"
    // imgArray:Array(3) ["123.jpg", "123.jpg", "123.jpg"]
    // length:3
    // __proto__:Array(0) [, …]
    // 0:"123.jpg"
    // 1:"123.jpg"
    // 2:"123.jpg"
    // __proto__:Object {imgArray: undefined, container: undefined, changeImage: }
    //   changeImage:function() { … }
    //   container:undefined
    //   imgArray:undefined
    console.log(fadeImg.container);
    // slide
    fadeImg.changeImage();
    // FadeLoopImage changeImage function
    LoopImage.prototype.changeImage();
    

    如上面代码所示,fadeImg里面除了继承父类的属性和方法之外,还定义了自己的原型方法

    这几行的数据是fadeImg子类的拥有的属性

    // arrow:Array(2) ["left.jpg", "right.jpg"]
    // container:"slide"
    // imgArray:Array(3) ["123.jpg", "123.jpg", "123.jpg"]

    这几行的数据是继承父类的属性

    // proto:Object {imgArray: undefined, container: undefined, changeImage: }
    // container:undefined
    // imgArray:undefined

    3.原型模式的特点

    关于原型模式还有一个特点:

    原型对象是一个共享对象,无论是父类的实例对象或者是子类的继承,都是对它的一个指针引用,所以原型对象才会被共享。既然被共享,那么对原型对象的拓展,不论是子类或者父类的实例对象都会继承下来。如下测试代码,可以看出,子类和父类原型链上的对象都是共享的

    LoopImage.prototype.getImagLength=function() {
        return this.imgArray.length;
    }
    
    FadeLoopImage.prototype.getContainer=function() {
        return this.container;
    }
    
    
    console.log(fadeImg.getImagLength()); //3
    console.log(fadeImg.getContainer()); //slide
    
    

    所以说原型模式有一个特点就是在任何时候都能对子类或者父类进行方法的拓展,而且所有被实例化的对象或者类都能获取这些方法,这样给予我们对功能拓展的自由性。但是这种方式太自由了,不要随意去做,否则修改类的其他属性或者方法有可能会影响到别人。

    4.原型继承

    原型模式更多的是在对象的创建上。比如创建一个实例对象的构造函数比较复杂,或者耗时比较长,或者通过多个对象来实现。此时我们最好不要用new关键字来复制这些基类,但可以通过对这些对象属性或者方法进行复制来实现床架。这是原型模式的最初思想。如果涉及多个对象,我们也可以通过原型模式实现对新对象的创建。那么首先要有一个原型模式的对象复制方法。如下代码所示:

    /**
     * @基于已经存在的模板对象克隆出新对象的模式: 
     * @param arguments[0], arguments[1],参数1,参数2表示模板对象
     * @return: 新对象
     * 注意,这里对模板对象的属性实质上进行了浅复制
     */
    function prototypeExtend() {
        var F = function () {},
            args = arguments,
            i = 0,
            length = arguments.length;
    
        for (; i < length; i++) {
            //遍历每个模板对象中的属性
            for (var j in args[i]) {
                //将这些属性复制到缓存类原型中
                F.prototype[j] = args[i][j]
            }
        }
        //返回缓存类的一个实例
        return new F();
    }
    
    
    var penguin = prototypeExtend({
        speed: 20,
        swim: function () {
            console.log('游泳速度' + this.speed);
        }
    }, {
        run: function (speed) {
            console.log('奔跑速度' + speed);
        }
    }, {
        jump: function () {
            console.log('跳跃速度');
        }
    })
    
    penguin.swim();
    // 游泳速度20
    penguin.run(10);
    // 奔跑速度10
    penguin.jump();
    // 跳跃速度
    

    既然通过prototypeExtend创建的是一个对象,我们就无需再用new去创建新的实例对象,我们可以直接使用这个对象。

    5.原型模式总结

    原型模式可以让多个对象分享同一个原型对象的属性和方法,这也是一种继承。不过这种继承的实现是不需要创建的,而是将原型对象分享给那些继承的对象。当然有时每个继承对象独立拥有一份原型兑现个,此时我们需要对原型对象进行复制。

    2.6 单例模式

    单例模式:又称为单体模式,是只允许实例化一次的对象类。有时我们也用一个对象来规划一个命名空间,井井有条地管理对象上的属性和方法。

    1.需求分析

    小白接到一个任务,公司要开展活动,经理让小白做一个宣传页面。小白在页面实现新闻列表中实现的鼠标特效定义了如下方法:

    function g(id) {
        console.log(id)
        return document.getElementById(id);
    }
    
    function css(id, key, value) {
        //简单样式属性设置
        g(id).style[key] = value;
    }
    
    function attr(id, key, value) {
        g(id)[key] = value;
    }
    
    function html(id, value) {
        g(id).innerHTML = value;
    }
    
    function on(id, type, fn) {
        g(id['on' + type]) = fn;
    }
    

    小明说:你怎么定义了这么多方法?

    小白:这样做有什么不好吗?

    小明:你在页面中添加了很多变量,比如你定义的绑定事件方法on,如果日后别人要为你的页面添加需求,增加代码而定义一个On变量或者重写了on方法,这样就会和你的代码起冲突了。所以你最好要用单例来修改一下你书写的代码。

    小白:单例模式?你是要我定义的这些功能方法放在一个对象里面吗?

    小明:单例模式经常为我们一个命名空间。

    为了让代码更易懂,然们长使用单词定义方法,但是不同的人定义的方法有可能是仙童的。所以需要用命名空间来约束每个人定义的变量来解决这个问题。比如上面的代码使用xiaozhang命名控件,如下代码:

    var Ming={
        g:function(id) {
            console.log(id)
            return document.getElementById(id);
        },
        css:function(id, key, value) {
            //简单样式属性设置
            g(id).style[key] = value;
        }
    }
    

    上面代码里面有个问题,在css方法里面使用了g方法,上面说过,单例模式定义的方法一定要加上命名空间,所以不要将css方法中的g方法改成Ming.g方法,由于g方法和css方法都在单例对象Ming中,也就是说着2个方法都是单例对象Ming中的方法,而对象中this指代当前对象,可以修改为下面这样

    var Ming={
        g:function(id) {
            console.log(id)
            return document.getElementById(id);
        },
        css:function(id, key, value) {
            //简单样式属性设置
            this.g(id).style[key] = value;
        }
    }
    

    2.模块分明

    其实在Javascript中单例模式除了定义命名空间外,还有一个作用,就是通过单例模式来管理代码库的各个模块。

    baidu.dom.addClass 添加元素类

    baidu.dom.append 插入元素

    baidu.event.trim 去除字符串空白字符

    3.创建一个小型代码库

    以后写自己的小型方法库的时候可以用单例模式来规范我们自己代码库的各个模块,比如我们有一个A库,它包含公用模块、工具模块、ajax模块和其他模块,那么我们就可以订制一下如下的小型代码库:

    var A = {
        Util: {
            util_method: function () {},
            util_method: function () {}
        },
        Tool: {
            tool_method: function () {},
            tool_method: function () {}
        },
        Ajax: {
            get: function () {},
            post: function () {}
        },
        others: {
    
        }
    }
    

    如果我们想使用的话,就像下面这样:

    A.Util.util_method()()

    4.无法修改的静态变量

    在代码管理上,还有一个功能适用于单例模式,就是管理静态变量。

    Javascript中没有static这类关键词,所以变量可以被修改。静态变量不可以被修改,所以我们将变量放在一个函数内部,通过特权方法访问,不提供赋值变量的方法就可以实现了。还有就是让创建函数的函数只执行一次,创建对对象内保存的静态变量通过取值器访问,最后将这个对象作为一个单例放在全局空间里作为静态变量单例对象供其他人使用。如下代码:

    var Conf = (function () {
        //私有变量
        var conf = {
            MAX: 10,
            MIN: 1
        }
            //返回对象
        return {
            get: function (name) {
                return conf[name] ? conf[name] : null
            }
        }
    })();
    //测试
    var count = Conf.get('MAX');
    console.log(count)
    //10
    

    5.惰性单例

    有时候对于单例对象需要延迟创建,也称为’惰性创建‘,如下代码

    var LazySingle = (function () {
        //单例实例引用
        var _instance = null;
        //单例
        function Single() {
            //定义私有属性和方法
            return {
                publicMethod: function () {},
                publicProperty: '1.0'
            }
        }
        //获取单例对象接口
        return function () {
            if (!_instance) {
                _instance = Single();
            }
            return _instance;
        }
    })();
    //测试
    console.log(LazySingle().publicProperty)
    // 1.0
    

    3.结构型设计模式

    3.1 外观模式

    外观模式:为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问容易。在javascript中有时也会用于对底层结构兼容性做统一封装来简化用户使用。

    1.需求分析

    小白为页面文档document对象绑定了一个click事件来实现隐藏提示框的交互功能,如下代码:

    document.onclick = function (e) {
        e.preventDefault();
        if (e.target != document.getElementById('myInput')) {
            console.log('hide page alert');
        }
    }
    

    上面的代码功能是完成了,但是为document绑定了onclick事件,onclick是DOM0级事件,如果我们团队中有人再次通过document绑定click事件,就相当于重复定义了一个方法,会将上面定义的方法进行覆盖,所以这种方式是很危险的。

    应该用DOM2级事件处理程序提供的方法addEventListener来实现,

    然而老版本的IE浏览器(低于9)是不支持这个方法的,所以你要用attachEvent,

    当然如果有不支持DOM2级事件处理程序的浏览器,你只能用onclick事件方法绑定事件。

    如此看来为元素绑定一个事件真不是一件容易的事,要做这么多的事情,有没有一个兼容所有浏览器的方式呢?

    2.外观模式实现

    可以用外观模式封装他们,就像我们吃饭时可以点套餐的方式一样,不用再浏览遍历每一种菜了。因为套餐已经为我们订制好了。那么在javascript中也可以通过外观点套餐那样定义一个统一接口方法,这样提供一个更简单的 高级接口,简化了我们对复杂的底层接口不统一的使用需求。如下代码:

    //外观模式实现
    function addEvent(dom, type, fn) {
        //对于支持DOM2级事件处理程序addEventListener方法的浏览器
        if (dom.addEventListener) {
            dom.addEventListener(type, fn, false);
            //对于不支持DOM2级事件处理程序addEventListener方法提供attachEvent方法的浏览器
        } else if (dom.attachEvent) {
            dom.attachEvent('on' + type, fn);
        } else {
            //不支持上面2种方式的
            dom['on' + type] = fn;
        }
    }
    

    以后对于支持addEventListener和attachEvent方法的浏览器就可以放心地绑定多个事件了,如下所示:

    var myInput = document.getElementById('myInput');
    addEvent(myInput, 'click', function () {
        console.log('绑定第一个事件')
    })
    
    addEvent(myInput, 'click', function () {
        console.log('绑定第二个事件')
    })
    
    addEvent(myInput, 'click', function () {
        console.log('绑定第三个事件')
    })
    

    如此一来,在团队开发中就可以放心地为元素绑定事件了。

    3.外观模式总结

    当一个复杂的系统提供一系列复杂的接口方法时,为系统的管理方便会造成接口方法的使用及其复杂。这在团队的多人开发中,撰写成本是很大的。当然通过外观模式,对 接口的二次封装封装其隐藏性,并简化其使用是一种很不错的实践。当然这种实践增加了一些资源开销以及程序的复杂度,对于成本来说也是可以忽略的。

    外观模式是对接口方法的外层包装,以供上层代码调用。因此有时外观模式封装的接口方法不需要接口的具体实现,只需要按照接口使用规则即可。这也是对系统与用户(使用者)之间的一种松散耦合。使系统与用户之间不会因结构的变化而相互影响。

    3.2 适配器模式

    适配器模式:将一个类(对象)的接口转化为另一个接口,以满足用户需求,使类(对象)之间接口的不兼容问题通过适配器得以解决

    1.需求分析

    小白:咱们的作品活动页面还在用公司内部开发的A框架,可是很多新来的同事使用A框架开发很吃力,为了让他们 尽快融入,引入jquery框架

    小明:可是以前功能代码是不是还要重新用jquery写一遍呀,一行一行将用到A框架的地方都修改成jquery的形式

    小白:不用呀,简单写个适配器就可以啦

    生活中,水房的2根垂直相交的水管连接处的直角弯管就是一个适配器,还有电源适配器等等。适配器:其实就是为2个代码库所写的代码兼容运行而书写的额外代码,有了这样的适配器,就不需要重写以前的功能代码了。

    小白:公司的A框架书写格式与jquery代码书写格式相同,所以需要在加载完jquery框架后写一个适配器。将已有的功能适配到jquery。比如页面的加载时间和点击事件,2个框架代码写法是类似的。像下面这样就可以轻松实现:

    window.A=A=jQuery
    

    刷新页面,页面可以正常工作,就可以使用熟悉的jquery代码了

    2.适配异类框架

    上面的例子,是A框架代码和jquery代码框架是类似的 。如果2个框架代码相差较多,对于异类框架代码,就要一行一行修改了。这种方式是比较麻烦的,通过适配器,我们发现如果2种框架的"血缘"比较相近,用适配器是比较容易的,如果血缘相差 较远,要复杂的多。要写很多代码

    3.参数适配器

    比如方法需要传递多个参数,例如

    function doSomething(name,title,color,size,prize) {
        
    }
    

    记住这些参数的顺序是很难的,所以经常是以一个参数对象方式传入的,如下所示:

    /**
     * @description: 
     * @param {obj} 
     * obj.name:name
     * obj.title:title
     * obj.color:color
     * obj.size:size
     * obj.prize:prize
     * @return: 
     */
    function doSomething(obj) {
        
    }
    

    然而当调用它的时候我们又不知道传递的参数是否完成,如有一些必须参数没有传入,一些参数是有默认值等,此时我们通常的做法是用适配器来适配传入的这个参数对象。如下所示:

    /**
     * @description: 
     * @param {obj} 
     * obj.name:name
     * obj.title:title
     * obj.color:color
     * obj.size:size
     * obj.prize:prize
     * @return: 
     */
    function doSomething(obj) {
        var _adapter={
            name:'渔业清河',
            title:'设计模式',
            age:24,
            color:'pink',
            size:100,
            price:50
        }
    
        for (var i in _adapter) {
            _adapter[i] = obj[i] || _adapter[i];
        }
    }
    

    其实很多插件对于参数配置都是这么做的。

    4.数据适配

    插件开发中,对于参数的适配又有衍生,比如对数据的适配,比如这里有一个数组:

    var arr=['Javascript','book','前端编程语言','8月1日']
    

    我们发现数组中每个成员代表的意义不同,所以这种数据结构语义不好,我们通常将其适配成对象形式,比如下面这种对象数据结构。

    function arrToObjAdapter(arr) {
        return {
            name: arr[0],
            type: arr[1],
            title: arr[2],
            data: arr[3]
        }
    }
    var arr=['Javascript','book','前端编程语言','8月1日']
    var adapterData=arrToObjAdapter(arr);
    console.log(adapterData);
    //{name: "Javascript", type: "book", title: "前端编程语言", data: "8月1日"}
    

    这也为数据的灵活使用提供了新思路。

    5.服务器端数据适配

    适配器模式最重要的是解决了前后端的数据依赖,前端不再为后端传递的数据所束缚,如果后台因为架构改变导致传递的数据结构发生变化,我们就只需要写个适配器就 可以放心了。

    比如我们使用jquery像后端接口发送数据,然后用doSomething方法处理接收的数据。如果后端的数据经常变化,比如有些网站拉取的新闻数据,后端有时候无法控制数据的格式,我们在调用something方式时候最好不要直接调用,最好先将后台传递来的数据是配成我们可用的数据再使用,这样更安全。

    如下所示:

    //为简化模式,这里使用jquery的ajax方法,理想数据是一个一维数组
    function ajaxAdapter(data) {
        return [data['key1'],data['key2'],data['key3']];
    }
    
    $.ajax({
        url: 'something.php',
        success: function (data, status) {
            if (data) {
                //使用适配后的数据---返回的对象
                doSomething(ajaxAdapter(data));
            }
        }
    })
    
    

    像这样,如果后台数据有任何变化,我们只需要适配更改ajaxApater适配器装换格式就可以了。

    6.适配器模式总结

    Javascript中适配器的使用,更多的是在对象之间,为了使对象可用,通常我们会将对象拆分并重新包装,这样我们就要了解适配对象的内部结构。这也是与外观模式的区别所在。当然适配器模式同样解决了对象之间的耦合度。包装的适配代码增加了一些资源开销,当然这是微乎其微的。

    3.3 装饰者模式

    装饰者模式:在不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法)使原有对象可以满足用户的更复杂需求。

    1.需求分析

    没有一成不变的需求,这步,为增加用户使用表单的交互体验,项目经理来找小白,正在谈后续需求改进呢?

    小白:用户信息表单需求有些变化,以前是用户点击输入框时,如果输入框输入的内容有限制,那么其后面显示用户输入内容的限制格式的提示文案,现在我们要多加一条,默认输入框上边显示一行提示文案,当用户点击输入框时文案消失。

    小白心想:这很简单,找到对应的代码,然后在后面增加几句就可以了呀。

    如下的修改代码:

    //输入框元素
    var telInput=document.getElementById('tel_input');
    //输入格式提示文案
    var telWarnText=document.getElementById('tel_warn_text');
    //点击输入框显示输入框输入格式提示文案
    InputDeviceInfo.onclick=function() {
        telWarnText.style.display='inline-block';
    }
    
    //小白修改后的代码
    
    //输入框元素
    var telInput = document.getElementById('tel_input');
    //输入格式提示文案
    var telWarnText = document.getElementById('tel_warn_text');
    //输入框提示输入文案,增加的内容
    var telDemoText = document.getElementById('tel_demo_text');
    //点击输入框显示输入框输入格式提示文案
    InputDeviceInfo.onclick = function () {
        telWarnText.style.display = 'inline-block';
        //增加的内容
        telDemoText.style.display = 'none';
    }
    

    可是悲剧发生了,小白修改了一个电话输入框,后面还有姓名输入框,地址输入框等等。还要像电话输入框这样在文件中查找功能代码,然后一个一个修改么?

    小明:小白,怎么了?

    小白:项目经理让我给原来用户信息框中增加一些需求,然而我改一个容易,后面还有这么多输入框,我还要一个一个去文件中查找代码,不知如何是好了。

    小明:试试装饰者模式吧

    2.装饰者模式分析

    装饰者模式,是添加东西的意思,比如买一个新房子,你想住的更舒适,那么你刚刚买的未装修过的新房子就不能满足你的需求了,所以你要对其装饰一番。同样,装饰者模式这是这个道理,原有的功能已经不能满足用户的需求了,此时你要做的就是为原有功能添砖加瓦,设置新功能和属性来满足用户提出的需求。

    如何实现呢?首先明确原有的功能是哪些?看看已经写过的功能代码,这些就是已有的功能,你要做的就是在这基础上添加一些功能来满足用户的需求。

    //装饰者
    var decorator = function (input, fn) {
        //获取事件源
        var input = document.getElementById(input);
        //若事件源已经绑定事件
        if (typeof input.onclick === 'function') {
            //缓存事件源已有回调函数
            var oldClickFn = input.onclick;
            //为事件源定义新的事件
            input.onclick = function () {
                //事件源原有回调函数
                oldClickFn();
                //新增会调函数
                fn();
            }
        } else {
            //若事件源未绑定事件,直接为事件源添加新增回调函数
            input.onclick = fn;
        }
    }
    

    3.为输入框添砖加瓦

    看看上面的代码,此时装饰者不仅仅对绑定过事件的输入框添加新的功能,未绑定的输入框同样可以,如下:

    //电话输入框功能装饰
    decorator('tel_input', function () {
        document.getElementById('tel_input').style.display = 'none';
    })
    
    //姓名输入框功能装饰
    decorator('name_input', function () {
        document.getElementById('name_input').style.display = 'none';
    })
    
    //地址输入框功能装饰
    decorator('address_input', function () {
        document.getElementById('address_input').style.display = 'none';
    })
    

    真是太棒了,通过装饰者对象方法,无论输入框是否绑定过事件,都可以轻松完成增加隐藏显示框的需求,不错。

    4.装饰者模式和适配器模式的比较

    装饰者模式很简单,就是对原有对象的属性与方法的添加。和适配器模式有什么不同呢?

    适配器方法使对原有对象适配,添加的方法与原有方法功能上大致相似。但是装饰者提供的方法与原来的方法功能项是有一定区别的。再有,使用适配器时,新增的方法是要调用原来的方法;不过在装饰者模式中,不需要了解对象原有的功能,并且对象原有的方法照样原封不动地使用。

    既然在适配器中增加的方法要调用原有的方法,就要了解原有方法实现的具体细节,而在装饰者中原封不动地使用,我们就不需要知道原有方法实现的具体细节,只有当我们调用方法时才会知道。

    5.装饰者模式总结

    装饰模式是对原有功能的一种增加与拓展,可以在不了解原有功能的基础。适配器模式进行拓展,很多时候是对对象内部结构的充足,因此需要理解其自身结构。而装饰者对对象的拓展是一种良性拓展,不用了解其具体实现,只在外部进行封装拓展,这又是对原有功能完整性的一种保护。

    3.4 桥接模式

    桥接模式:在系统沿着多个维度变化的时候,又不增加其复杂度并已达成解耦。

    1.需求分析

    有时候页面的一些小小细节改变常常因为逻辑相似导致大片臃肿的代码,让页面苦涩不堪。这不,小白为解决这类问题,已经熬了整整一个上午了。

    小明;什么功能让你写了一上午?

    小白:项目经理让我把页面上部的用户信息部分添加一些鼠标划过特效。不过用户信息是由很多小组件组成。你看,对于用户名,鼠标划过直接改变背景色,但是像用户等级、用户消息这类组件(只有文字),只能改变里面的数字内容,处理的逻辑不太一样,所以写了不少代码。不过写完时,自己感觉很多是多余的,却又不知道该如何完善,如下代码

    var spans = document.getElementsByTagName('span');
    //为用户名添加特效
    spans[0].onmouseover = function () {
        this.style.color = 'red';
        this.style.background = '#ddd';
    }
    spans[0].onmouseout = function () {
        this.style.color = '#333';
        this.style.background = '#f77777'
    }
    
    //为等级绑定特性
    spans[1].onmouseover = function () {
        this.getElementsByTagName('strong')[0].style.color = 'red';
        this.getElementsByTagName('strong')[0].style.background = '#ddd';
    }
    spans[1].onmouseout = function () {
        this.getElementsByTagName('strong')[0].style.color = '#333';
        this.getElementsByTagName('strong')[0].style.background = '#f77777'
    }
    

    2.提取公共点

    上面的代码是有点冗余,不过问题是在哪里呢?要对事件的回调函数再做处理,写代码时候一定要注意对相同的逻辑做抽象提取处理,这一点很重要。如果这点能做到,代码就会更简洁,重用率也会更大,可读性也会更高,这也是我们面向对象编程的一个目的。对于用户信息模块的每个部分鼠标划过和鼠标离开2个事件的执行函数很大一部分是相似的,比如他们都处理每个部件的每个元素,都是处理该元素的字体颜色和背景颜色。所以对于这个相似点的抽象提取是很有必要的,比下代码,创建这样一个函数,解除与事件中this的耦合。

    //抽象
    function changeColor(dom, color, bg) {
        //设置元素的字体颜色
        dom.style.color = color;
        //设置元素的背景颜色
        dom.style.background = bg;
    }
    

    3.事件与业务逻辑之间的桥梁

    剩下要做的就是对元素绑定事件了,仅仅知道元素事件绑定与抽象提取的设置样式方法changeColor还是不够的。需要用一个方法将他们链接起来,那这个方法就是桥接方法。这种模式就是桥接模式。像在北京开着车去沈阳,需要找到一条连接北京与沈阳的公路,才能顺利在两地往返。对于事件的桥接方法,我们可以用一个匿名函数来代替,否则直接将changeColor作为事件的回调函数,那刚才做的事情就白做了,因为他们还将耦合在一起。如下面的为用户名绑定事件:

    var spans = document.getElementsByTagName('span');
    //为用户名添加特效
    spans[0].onmouseover = function () {
        changeColor(this,'red','#ddd');
    }
    

    如上代码,changeColor中的dom实质上是事件回调函数中的this,那么我们想要就解除他们之间的耦合,我们就需要一个桥接方法---匿名回调函数。通过这个回调函数,我们将获取到的this传递到changeColor函数中,即可实现需求。同样对于用户名的事件用同样的方式就可以了

    var spans = document.getElementsByTagName('span');
    //为用户名添加特效
    spans[0].onmouseover = function () {
        changeColor(this,'red','#ddd');
    }
    spans[0].onmouseout = function () {
        changeColor(this,'#333','#f77777');
    }
    
    //为等级绑定特性
    spans[1].onmouseover = function () {
        changeColor(this.getElementsByTagName('strong')[0],'red','#ddd');
    }
    spans[1].onmouseout = function () {
        changeColor(this.getElementsByTagName('strong')[0],'#333','#f77777');
    }
    
    

    若如此,现在的代码比之前要清晰很多了,如果想对需求做任何修改,我们只需要更改changeColor中的内容就可以了,而不必要到每个事件回调函数中去修改,当然这是以新增了一个桥接函数为代价的。

    感觉桥接模式只是先抽象提取共用部分,然后将实现与抽象通过桥接方法链接在一起,来实现解耦的作用。

    4.多元化对象

    不过桥接模式的强大之处不仅在此,对于多维的变化也同样适用。比如书写一个canvas跑步游戏的时候,对于游戏中的人、小精灵、小球等一系列的实物都有动作单元,而他们每个动作实现起来又是统一的,比如人和精灵和球的运动其实就是坐标位置x和y的变化,球的颜色与精灵颜色的绘制方式相似等等。这样我们就可以将这些多维变化部分,提取出来作为一个抽象运动单元进行保存,当我们创建实体时,将需要的每个抽象动作单元通过桥接,链接在一起运动,这样他们之间不会相互影响而且该方式降低了他们之间的耦合

    如下代码:

    //多维变量类
    //运动单元
    function Speed(x,y) {
        this.x=x;
        this.y=y;
    }
    Speed.prototype.run=function() {
        console.log('运动起来');
    }
    //着色单元
    function Color(cl) {
        this.color=cl;
    }
    
    Color.prototype.draw=function() {
        console.log('绘制色彩');
    }
    
    //变形单元
    function Shape(sp) {
        this.Shape=sp;
    }
    
    Shape.prototype.change=function() {
        console.log('改变形状');
    }
    
    //说话单元
    function Speak(wd) {
        this.word=wd;
    }
    
    Speak.prototype.say=function() {
       console.log('书写字体');
    }
    
    //创建一个球类,并且它可以运动,也可以着色
    function Ball(x,y,c) {
        //实现运动单元
        this.speed=new Speed(x,y);
        //实现着色单元
        this.color=new Color(c);
    }
    
    Ball.prototype.init=function() {
        //实现运动单元
        this.speed.run();
        //实现着色单元
        this.color.draw();
    }
    
    //创建一个人物类,他可以讲话,以及说话
    function People(x,y,f) {
        this.speed=new Speed(x,y);
        this.font=new Speak(f);
    }
    
    People.prototype.init=function() {
        this.speed.run();
        this.font.say();
    }
    
    //实现一个人物时候,直接实例化一个任务对象,这样他就可以有运动和说话的动作了
    var p=new People(10,12,16);
    p.init();
    
    

    5.桥接模式总结

    桥接模式最重要的是将实现层(如元素绑定的时间)与抽象层(如修饰页面UI逻辑)解耦分离,使两部分可以独立变化。由此可以看出桥接模式主要是对结构之间的解构,而前面学过的抽象工厂模式与创建者模式主要业务在于创建。通过桥接模式实现的解耦,使实现层与抽象层分开处理,避免需求的变化造成对象内部的修改,体现了面向对象对拓展的开发,以及对修改的关闭原则,是很有用的。

    3.5 享元模式

    享元模式:运用共享模式有效地支持大量的细粒度的对象,避免对象间拥有相同的内容造成多余的开销

    1.需求分析

    新闻模块终于写完了,可是随着内容的增多,单页面已经无法容纳所有的新闻了,所以经常准备采取分页显示所有的新闻。

    如下代码,写的代码在Chrome浏览器中运行良好,可是到了低版本IE浏览器怎么一卡一卡的呢?

    代码实现了很简单的一个新闻翻页功能,点击下一页隐藏当前页面的新闻,然后显示后面的5个。

    var dom = null; //缓存创建的新闻标题元素
    var paper = 0; //当前页数
    var num = 5; //每页显示新闻数据
    var i = 0; //创建新闻元素时保存变量
    var len = article.length; //新闻数据长度
    
    for (; i < len; i++) {
        dom = document.createElement('div'); //创建包装新闻标题元素
        dom.innerHTML = article[i]; //向元素中添加新闻标题
        if (i >= num) { //默认显示第一页
            dom.style.display = 'none';
        }
        document.getElementById('container').appendChild(dom);
    }
    //下一页绑定事件
    document.getElementById('next_page').onclick = function () {
        var div = document.getElementById('container').getElementsByTagName('div'),
            //获取所有新闻标题包装元素
            j = k = n = 0; //j,k循环变量,n当前页显示的第一个新闻序号
        n = ++paper % Math.ceil(len / num) * num; //获取当前页显示的第一个新闻序号
        for (; j < len; j++) {
            div[j].style.display = 'none'; //隐藏所有新闻
        }
    
        for (; k < 5; k++) {
            if (div[n + k]) {
                div[n + k].style.display = 'block'; //显示当前页新闻
            }
        }
    }
    
    

    思路:页面加载后,异步请求新闻数据,然后创建所有条新闻并插入页面中,需要显示哪页就将当前页的新闻显示,其他的新闻隐藏....

    2.冗余的结构

    问题就出在这里了。所有的新闻都有相同的结构,只是内部不同罢了,所以创建的几百条新闻同时插入页面并操作的多余的开销在低版本的IE浏览器中当然会严重影响其性能。对于相同结构造成的多余开销问题,可以用享元模式来解决。

    享元模式是指对相同的数据结构进行提取,不过享元模式主要还是对其数据、方法共享分离它将数据和方法分问为内部数据、内部方法和外部数据、外部方法。内部数据与内部方法指的是相似或者共有的数据和方法,所以将这一部分提取出来以减少开销,以提高性能。就想城市里人们每天上班,有人开私家车,有人坐公交,坐公交的开销要比私家车的开销少很多,主要是因为它让更多的人共有这一交通工具。

    3.享元对象

    这样,上面写功能里面,这些新闻个体都有共同的结构,所以它们应该作为内部的数据,而’下一页‘按钮绑定的事件已经不能再抽象提取了,所以应该就是外部的方法了。

    既然这些内部的数据提取出来了,为了能使用他们,我们还需要提供一个操作方法,如下是内部数据

    var FlyWeight=function() {
        //已创建的元素
        var created=[];
        //创建一个新闻包装容器
        function create() {
            var dom=document.createElement('div');
            //将容器插入新闻列表容器中
            document.getElementById('container').appendChild(dom);
            //缓存新创建的元素
            created.push(dom);
            //返回创建的新元素
            return dom;
        }
    
        return {
            //获取创建新闻元素方法
            getDiv:function() {
                //如果已创建的元素当前元素总个数,则创建
                if(created.length<5) {
                    return create();
                }else {
                    //获取第1个元素,并插入最后面
                    var div=created.shift();
                    created.push(div);
                    return div;
                }
            }
        }
    }
    

    4.享元动作

    其实面向对象编程还是很有用的,比如说一个游戏我们可能创建一些人、精灵等角色,那么他们都会有运动这个动作,其实这一动作在所有角色中实现方式都是相同的。对此我们可以创建一个通用的享元类,让它可以实现横向移动以及纵向移动,如下所示:

    var FlyWeight = {
        moveX: function (x) {
            this.x = x;
        },
        moveY: function (y) {
            this.y = y;
        }
    }
    

    其他任何角色都可以通过继承的方式来实现这些方法,比如让人来继承移动方法

    var FlyWeight = {
        moveX: function (x) {
            this.x = x;
        },
        moveY: function (y) {
            this.y = y;
        }
    }
    
    //让人来继承的移动的方式
    var Player = function (x, y, c) {
        this.x = x;
        this.y = y;
        this.color = c;
    }
    
    Player.prototype = FlyWeight;
    Player.prototype.changeC = function (c) {
        this.color = c;
    }
    
    //让精灵继承移动的方法
    var Spirit = function (x, y, r) {
        this.x = x;
        this.y = y;
        this.r = r;
    }
    
    Spirit.prototype = FlyWeight;
    Spirit.prototype.changeR = function (r) {
        this.r = r;
    }
    
    //创建一个人
    var player1 = new Player(5, 6, 'red');
    console.log(player1);
    //{x: 5, y: 6, color: "red"}
    player1.changeR('rr');
    player1.moveX(7);
    player1.moveY(8)
    console.log(player1);
    //{x: 7, y: 8, color: "red", r: "rr"}
    
    
    //创建精灵
    var spirit=new Spirit(2,3,4);
    console.log(spirit);
    // {x: 2, y: 3, r: 4}
    spirit.moveY(5);
    spirit.moveX(6);
    console.log(spirit);
    // {x: 6, y: 5, r: 4}
    

    从上面可以看出,无论是游戏中的人还是精灵都拥有了运动这一方法,这样我们就将本来在任务类和精灵类中的内部方法(移动方法)提取出来,实现了公用,减少了其他类重写时造成的不必要的开销。类似这样的提取在页面中也是很常见的,所以我们要善于观察提取可共享的数据与方法来优化我们的应用。

    5.享元模式总结

    享元模式应用的目的是为了提高程序的执行效率与系统的性能,因此在大型系统开发中应用是比较广泛的,百分之一的效率提成有时会发生质的改变。它可以避免程序中的数据重复,有时系统内存在大量对象,会造成大量内存占用,所以应用享元模式来减少内存消耗是很有必要的。不过应用时一定要找准内部状态(数据与方法)与外部状态(数据与方法),这样能更合理地提取分离。当然在一些小程序中,性能与内存的消耗对程序的执行影响不大时,强行应用享元模式而引入复杂代码逻辑,往往会收到负效应。

  • 相关阅读:
    Asp.net MVC 自定义路由在IIS7以上,提示Page Not Found 解决方法
    mysql 常用操作
    Mongo常用操作
    Cent Os 常用操作
    Window 8.1 开启Wifi共享
    解决 对象的当前状态使该操作无效 的问题
    unity3d: how to display the obj behind the wall
    unreal network
    rust borrow and move
    erlang的map基本使用
  • 原文地址:https://www.cnblogs.com/zdjBlog/p/13038605.html
Copyright © 2011-2022 走看看