软件设计模式(一)

268 阅读13分钟

面向对象设计原则 + 创建型设计模式

软件设计中,被反复使用的一种代码设计经验,使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。

软件设计原则(面向对象)

面向对象的设计原则有七个,包括:开闭原则、里氏替换原则、迪米特原则(最少知道原则)、单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则

七大原则之间并不是相互孤立的,彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。

开闭原则(OCP)

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

两个重要特性:

  1. 对于扩展是开放的
  2. 对于修改是封闭的

背景

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

优点

  • 对软件测试友好

软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。

  • 可复用性好

我们可以在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。

  • 可维护性好

由于对于已有的软件系统的组件,特别是它的抽象底层不去修改,因此,我们不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。

OCP原则

interface ICourse{
    name: string;
    price: string;
    getName: Function;
    getPrice: Function;
}

class Course<ICourse> {
    constructor(protected name, protected price){}
    getName(){
        return this.name;
    }
    getPrice(){
        return this.price;
    }
}
const english = new Course("英语","500");
console.log("English===>", english.getName(),english.getPrice());

/**
 * @description 英语课打折?salePrice
 */

里氏代换原则(Liskov Substitution Principle, LSP)

  1. 如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型
  2. 有引用基类的地方必须能透明地使用其子类的对象
  3. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
function Vehicle(my) {
    var my = my || {};
    my.speed = 0;
    my.running = false;

    this.speed = function() {
        return my.speed;
    };
    this.start = function() {
        my.running = true;
    };
    this.stop = function() {
        my.running = false;
    };
    this.accelerate = function() {
        my.speed++;
    };
    this.decelerate = function() {
        my.speed--;
    }, 
    this.state = function() {
        if (!my.running) {
            return "parked";
        }
        else if (my.running && my.speed) {
            return "moving";
        }
        else if (my.running) {
            return "idle";
        }
    };
}
//上述代码我们定义了一个Vehicle函数,其构造函数为vehicle对象提供了一些基本的操作,我们来想想如果当前函数当前正运行在服务客户的产品环境上,如果现在需要添加一个新的构造函数来实现加快移动的vehicle。思考以后,我们写出了如下代码:
function FastVehicle(my) {
    var my = my || {};

    var that = new Vehicle(my);
    that.accelerate = function() {
        my.speed += 3;
    };
    return that;
}

在浏览器的控制台我们都测试了,所有的功能都是我们的预期,没有问题,FastVehicle的速度增快了3倍,而且继承他的方法也是按照我们的预期工作。此后,我们开始部署这个新版本的类库到产品环境上,可是我们却接到了新的构造函数导致现有的代码不能支持执行了,下面的代码段揭示了这个问题:

var maneuver = function(vehicle) {
    write(vehicle.state());
    vehicle.start();
    write(vehicle.state());
    vehicle.accelerate();
    write(vehicle.state());
    write(vehicle.speed());
    vehicle.decelerate();
    write(vehicle.speed());
    if (vehicle.state() != "idle") {
        throw "The vehicle is still moving!";
    }
    vehicle.stop();
    write(vehicle.state());
};

迪米特原则(Law of Demeter, LoD)

  1. 一个对象应该对其他对象保持最少的了解 尽量降低类与类之间的耦合。

单一职责原则(Law of Demeter, LoD)

  1. 单一职责原则是指不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责
function Product(id, description) {
    this.getId = function () {
        return id;
    };
    this.getDescription = function () {
        return description;
    };
}

function Cart(eventAggregator) {
    var items = [];

    this.addItem = function (item) {
        items.push(item);
    };
}

(function () {
    var products = [new Product(1, "Star Wars Lego Ship"),
            new Product(2, "Barbie Doll"),
            new Product(3, "Remote Control Airplane")],
cart = new Cart();

    function addToCart() {
        var productId = $(this).attr('id');
        var product = $.grep(products, function (x) {
            return x.getId() == productId;
        })[0];
        cart.addItem(product);

        var newItem = $('<li></li>').html(product.getDescription()).attr('id-cart', product.getId()).appendTo("#cart");
    }

    products.forEach(function (product) {
        var newItem = $('<li></li>').html(product.getDescription())
                                    .attr('id', product.getId())
                                    .dblclick(addToCart)
                                    .appendTo("#products");
    });
})();

匿名函数里却包含了很多不相关的职责,让我们来看看到底有多少职责:

首先,有product的集合的声明 其次,有一个将product集合绑定到#product元素的代码,而且还附件了一个添加到购物车的事件处理 第三,有Cart购物车的展示功能 第四,有添加product item到购物车并显示的功能 重构代码

参考了martinfowler的事件聚合(Event Aggregator)理论在处理代码以便各对象之间进行通信

//首先我们先来实现事件聚合的功能,该功能分为2部分,1个是Event,用于Handler回调的代码,1个是EventAggregator用来订阅和发布Event
        function Event(name) {
            var handlers = [];

            this.getName = function () {
                return name;
            };

            this.addHandler = function (handler) {
                handlers.push(handler);
            };

            this.removeHandler = function (handler) {
                for (var i = 0; i < handlers.length; i++) {
                    if (handlers[i] == handler) {
                        handlers.splice(i, 1);
                        break;
                    }
                }
            };

            this.fire = function (eventArgs) {
                handlers.forEach(function (h) {
                    h(eventArgs);
                });
            };
        }
         function EventAggregator() {
            var events = [];

            function getEvent(eventName) {
                return $.grep(events, function (event) {
                    return event.getName() === eventName;
                })[0];
            }

            this.publish = function (eventName, eventArgs) {
                var event = getEvent(eventName);

                if (!event) {
                    event = new Event(eventName);
                    events.push(event);
                }
                event.fire(eventArgs);
            };

            this.subscribe = function (eventName, handler) {
                var event = getEvent(eventName);

                if (!event) {
                    event = new Event(eventName);
                    events.push(event);
                }

                event.addHandler(handler);
            };
        }
// 然后,我们来声明Product对象,代码如下:
function Product(id, description) {
    this.getId = function () {
        return id;
    };
    this.getDescription = function () {
        return description;
    };
}
//接着来声明Cart对象,该对象的addItem的function里我们要触发发布一个事件itemAdded,然后将item作为参数传进去。

function Cart(eventAggregator) {
    var items = [];

    this.addItem = function (item) {
        items.push(item);
        eventAggregator.publish("itemAdded", item);
    };
}
//CartController主要是接受cart对象和事件聚合器,通过订阅itemAdded来增加一个li元素节点,通过订阅productSelected事件来添加product。

function CartController(cart, eventAggregator) {
    eventAggregator.subscribe("itemAdded", function (eventArgs) {
        var newItem = $('<li></li>').html(eventArgs.getDescription()).attr('id-cart', eventArgs.getId()).appendTo("#cart");
    });

    eventAggregator.subscribe("productSelected", function (eventArgs) {
        cart.addItem(eventArgs.product);
    });
}
//Repository的目的是为了获取数据(可以从ajax里获取),然后暴露get数据的方法。

function ProductRepository() {
    var products = [new Product(1, "Star Wars Lego Ship"),
            new Product(2, "Barbie Doll"),
            new Product(3, "Remote Control Airplane")];

    this.getProducts = function () {
        return products;
    }
}
//ProductController里定义了一个onProductSelect方法,主要是发布触发productSelected事件,forEach主要是用于绑定数据到产品列表上,代码如下:

function ProductController(eventAggregator, productRepository) {
    var products = productRepository.getProducts();

    function onProductSelected() {
        var productId = $(this).attr('id');
        var product = $.grep(products, function (x) {
            return x.getId() == productId;
        })[0];
        eventAggregator.publish("productSelected", {
            product: product
        });
    }

    products.forEach(function (product) {
        var newItem = $('<li></li>').html(product.getDescription())
                                    .attr('id', product.getId())
                                    .dblclick(onProductSelected)
                                    .appendTo("#products");
    });
}
//最后声明匿名函数(需要确保HTML都加载完了才能执行这段代码,比如放在ready方法里):

(function () {
    var eventAggregator = new EventAggregator(),
    cart = new Cart(eventAggregator),
    cartController = new CartController(cart, eventAggregator),
    productRepository = new ProductRepository(),
    productController = new ProductController(eventAggregator, productRepository);
})();
//重构的结果就是写了一大堆的对象声明,但是好处是每个对象有了自己明确的职责,该展示数据的展示数据,改处理集合的处理集合,这样耦合度就非常低了

接口分离原则

  1. 客户端不应该依赖它不需要的接口
  2. 类间的依赖关系应该建立在最小的接口上

依赖倒置原则

  1. 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

类A直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类A的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻辑;类B和类 C 是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

组合/聚合复用原则

  1. 尽量采用组合(contains-a)、聚合(has-a)的方式而不是继承(is-a)的关系来达到软件的复用目的 组合/聚合复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。 原则是尽量首先使用合成 / 聚合的方式,而不是使用继承。

设计模式

单例模式

是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

// 饿汉式单列
// 无论你调没调用say方法,只要你把这个类引进来了,他就已经创建出来实例化的对象了,将实例化的对象复制给自身的静态属性,所以也就意味着你还没用但是constructor函数就已经会执行了
class Demo01{
    static _self = new Demo01();
    say(){
        console.log('demo01');
    }
}
Demo01._self.say();

// 懒汉式单列模式是你必须要调用某个方法时,他才会去检测是否有没有这个实例
class Demo02{
    say(){
        console.log('demo02');
    }
    static getInstance(){
        if(!this.Instance){
            console.log("this.Instance===>",this.Instance);
            this.Instance = new Demo02();
        }
        return this.Instance;
    }
}
Demo02.getInstance().say();

原型模式

是一种创建型设计模式,使你能够复制已有对象, 而又无需使代码依赖它们所属的类。原型模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用原型模式。

// 定义原型
const person = {
	name: '',
	age: 0,
	sex: '',
	hobby: '',
	sayName: function() {
		console.log(this.name)
	}
}

// 通过原型克隆新对象
const person1 = Object.create(person)
person1.name = 'zkk'
person1.sayName() // => 'zkk'
const person2 = Object.create(person)
person2.name = 'zcc'
person2.sayName() // => 'zcc'

抽象工厂模式(Abstract Factory Pattern)

提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。 抽象工厂模式,它创建的结果不是一个对象,实现子类对"抽象类"(JavaScript没有抽象类,只是模拟抽象类)的继承,然后子类再自行实例化;

interface A {}
interface B {}

class AP implements A {};
class BP implements B {};
class A2P implements A {};
class B2P implements B {};

interface AbstractFactory{
  createA(): A;
  createB(): B;
}

class Factory1 implements AbstractFactory {
  createA(): A {
    return new AP();
  }
  createB(): B {
    return new BP();
  }
}

class Factory2 implements AbstractFactory {
  createA(): A {
    return new A2P();
  }
  createB(): B {
    return new B2P();
  }
}

const factory1 = new Factory1();
const factory2 = new Factory2();

工厂方法模式

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

interface Shootable{
    shoot();
}

abstract class Gun implements Shootable{ // 抽象产品 - 枪
    abstract shoot();
}

class AK47 extends Gun{ //具体产品 - AK47
    shoot(){
        console.log('ak47 shoot.');
    }
}

class M4A1 extends Gun{ //具体产品 - M4A1
    shoot(){
        console.log('m4a1 shoot.');
    }
}

abstract class GunFactory{ //抽象枪工厂
    abstract create(): Gun;
}

class AK47Factory extends GunFactory{ //Ak47工厂
    create(): Gun{
        let gun = new AK47();  // 生产Ak47
        console.log('produce ak47 gun.');
        this.clean(gun);       // 清理工作
        this.applyTungOil(gun);// Ak47是木头枪托,涂上桐油
        return gun;
    }

    private clean(gun: Gun){
        //清洗
        console.log('clean gun.');
    }

    private applyTungOil(gun: Gun){
        //涂上桐油
        console.log('apply tung oil.');
    }
}

class M4A1Factory extends GunFactory{ //M4A1工厂
    create(): Gun{
        let gun = new M4A1();   // 生产M4A1
        console.log('produce m4a1 gun.');
        this.clean(gun);        // 清理工作
        this.sprayPaint(gun);   // M4是全金属,喷上漆
        return gun;
    }

    private clean(gun: Gun){
        //清洗
        console.log('clean gun.');
    }

    private sprayPaint(gun: Gun){
        //喷漆
        console.log('spray paint.');
    }
}

let ak47 = new AK47Factory().create();
ak47.shoot();

let m4a1 = new M4A1Factory().create();
m4a1.shoot();

//output
produce ak47 gun.
clean gun.
apply tung oil.
ak47 shoot.

produce m4a1 gun.
clean gun.
spray paint.
m4a1 shoot.

使用场景

  1. 一个类不知道它所需要的对象的类 在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。

  2. 一个类通过其子类来指定创建哪个对象 在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

  3. 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。

建造者模式(Builder Pattern)

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象

//创建一位人类
var Human = function (param) {
    //技能
    this.skill = param && param.skill || '保密';//如果存在param参数,并且param拥有skill属性,就用这个属性赋值给this的skill属性,否则将yoga默认值保密来设置
    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 = '对不起,我们还不清楚您所选择职位的相关描述';
        }
    })(work,that);
}
//更换期望的职位
Work.prototype.changeWork = function (work) {
    this.work = work;
}
//添加对职位的描述
Work.prototype.changeDescript = function (sentence) {
    this.workDescript = sentence;
}


/**
 *应聘者建造类
 * 参数name:姓名(全名)
 * 参数work:期望职位
 */
 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);//xiao
console.log(person.name.secondName);//ming
console.log(person.work.work);//工程师
console.log(person.work.workDescript);//每天沉醉于代码
person.work.changeDescript('更改描述!');
console.log(person.work.workDescript);//更改描述!

对比 建造者模式和工厂模式(包括抽象工厂)

工厂模式(包括抽象工厂)主要是为了创建对象实例或者类簇,关心的是最终产出(创建)的是什么,而不关心创建的过程。

建造者模式的目标任务也是创建对象,但该模式更多关心的是对象创建的整个过程,甚至关心到对象创建的每一个细节。 比如用建造者模式创建一个“Person”的对象时,它不仅要得到Person的对象,还要深入到对象创建的过程中去,人应该穿什么衣服,男的还是女的,兴趣爱好什么等。