面向对象设计原则 + 创建型设计模式
软件设计中,被反复使用的一种代码设计经验,使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。
软件设计原则(面向对象)
面向对象的设计原则有七个,包括:开闭原则、里氏替换原则、迪米特原则(最少知道原则)、单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则
七大原则之间并不是相互孤立的,彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。
开闭原则(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)
- 如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型
- 有引用基类的地方必须能透明地使用其子类的对象
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
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)
- 一个对象应该对其他对象保持最少的了解 尽量降低类与类之间的耦合。
单一职责原则(Law of Demeter, LoD)
- 单一职责原则是指不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责
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);
})();
//重构的结果就是写了一大堆的对象声明,但是好处是每个对象有了自己明确的职责,该展示数据的展示数据,改处理集合的处理集合,这样耦合度就非常低了
接口分离原则
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
依赖倒置原则
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
类A直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类A的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻辑;类B和类 C 是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
组合/聚合复用原则
- 尽量采用组合(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.
使用场景
-
一个类不知道它所需要的对象的类 在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
-
一个类通过其子类来指定创建哪个对象 在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
-
将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
建造者模式(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的对象,还要深入到对象创建的过程中去,人应该穿什么衣服,男的还是女的,兴趣爱好什么等。