JavaScript面向对象之七大基本原则实例详解

本文实例讲述了JavaScript面向对象之七大基本原则。分享给大家供大家参考,具体如下:

面向对象编程有自己的特性与原则,如果对于面向对象有一些了解的话,面向对象三大特征,封装、继承、多态,如果对面向对这三个概念不太了解,请参考面向对象之三个基本特征(javaScript)

单一职责

如果我们在编写程序的时候,一类或者一个方法里面包含了太多方法,对于代码的可读性来说,无非是一场灾难,对于我们来说。所以为了解决这个问题,出现了单一职责。

什么是单一职责

单一职责:又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。(节选自百度百科)

按照上面说的,就是对一个类而言,应该仅有一个引起它变化的原因。换句话说,一个类的功能要单一,只做与它相关的事情。在类的设计过程中要按职责进行设计,彼此保持正交,互不干涉。

单一职责的好处

  1. 类的复杂性降低,实现什么职责都有清晰明确的定义
  2. 可读性提高,复杂性降低,那当然可读性提高了
  3. 可维护性提高,可读性提高,那当然更容易维护了
  4. 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

实例

class ShoppinCar {
  constructor(){
    this.goods = [];
  }
  addGoods(good){
    this.goods = [good];
  }
  getGoodsList(){
    return this.goods;
  }
}
class Settlement {
  constructor(){
    this.result = 0;
  }
  calculatePrice(list,key){
    let allPrice = 0;
    list.forEach((el) => {
      allPrice += el[key];
    })
    this.result = allPrice;
  }
  getAllPrice(){
    return this.result;
  }
}

用上面的代码来说ShoppinCar类存在两个方法addGoodsgetGoodsList,分别是添加商品和获取商品列表。Settlement类中存在两个方法calculatePricegetAllPrice分别做的事情是计算价钱与获取总价钱。ShoppinCarSettlement都是在做自己的事情。添加商品与计算价格,虽然在业务上是相互依赖的,但是在代码中分别用两个类,然他们自己做自己的事情。其中任何一个类更改不会对另一个类进行更改。

开闭原则

在一个类中暴露出去的方法,若这个方法变更了,则会产生很大的后果,可能导致其他依赖于这个方法且有不需要变更的业务造成大面积瘫痪。为了解决这个问题,可以单独再写一个方法,若这个方法与这个类中的其他方法相互依赖。

解决办法:

  1. 把其中依赖的代码copy一份到新的类中。
  2. 在新类中引用旧类中的方法。

两种方法都不是最好的解决方案。

第一种方法会导致代码大量的重复,第二种方法会导致类与类之间互相依赖。

什么是开闭原则

开闭原则:“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。(节选自百度百科)

开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,底层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。开闭原则是一个最基本的原则,另外六个原则都是开闭原则的具体形态,是指导设计的工具和方法,而开闭原则才是精神领袖.

开闭原则好处

  1. 开闭原则有利于进行单元测试
  2. 开闭原则可以提高复用性
  3. 开闭原则可以提高可维护性
  4. 面向对象开发的要求

实例

class Drag {
  down(){
    // ...
  }
  move(){
    // ...
    // 对拖拽没有做任何限制可以随意拖拽
  }
  up(){
    // ...
  }
}
class LimitDrag extends Drag {
  move(){
    // ...
    // 重写该方法对拖拽进行限制处理
  }
}

LimitDrag中重写了move方法,若修改了可以满足两种需求,一种是限制型拖拽,一种是不限制型拖拽,任何一个更改了另外一个还是可以正常运行。

里氏替换

每个开发人员在使用别人的组件时,只需知道组件的对外裸露的接口,那就是它全部行为的集合,至于内部到底是怎么实现的,无法知道,也无须知道。所以,对于使用者而言,它只能通过接口实现自己的预期,如果组件接口提供的行为与使用者的预期不符,错误便产生了。里氏替换原则就是在设计时避免出现派生类与基类不一致的行为。

什么是里氏替换

里氏替换原则:OCP作为OO的高层原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性。“抽象”是语言提供的功能。“多态”由继承语义实现。(节选自百度百科)

里氏替换好处

  1. 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
  2. 提高代码的重用性
  3. 子类可以形似父类,但是又异于父类。
  4. 提高代码的可扩展性,实现父类的方法就可以了。许多开源框架的扩展接口都是通过继承父类来完成。
  5. 提高产品或项目的开放性

实例

// 抽象枪类
class AbstractGun {
  shoot(){
    throw "Abstract methods cannot be called";
  }
}
// 步枪
class Rifle extends AbstractGun {
  shoot(){
    console.log("步枪射击...");
  }
}
// 狙击枪
class AUG extends Rifle {
  zoomOut(){
    console.log("通过放大镜观察");
  }
  shoot(){
    console.log("AUG射击...");
  }
}
// 士兵
class Soldier {
  constructor(){
    this.gun = null;
  }
  setGun(gun){
    this.gun = gun;
  }
  killEnemy(){
    if(!this.gun){
      throw "需要给我一把枪";
      return;
    }
    console.log("士兵开始射击...");
    this.gun.shoot();
  }
}
// 狙击手
class Snipper extends Soldier {
  killEnemy(aug){
    if(!this.gun){
      throw "需要给我一把枪";
      return;
    }
    this.gun.zoomOut();
    this.gun.shoot();
  }
}
let soldier = new Soldier();
soldier.setGun(new Rifle());
soldier.killEnemy();

let snipper = new Snipper();
// 分配狙击枪
snipper.setGun(new AUG());
snipper.killEnemy();

snipper.setGun(new Rifle());
// snipper.killEnemy(); // this.gun.zoomOut is not a function

从上述代码中可以看出,子类和父类之间关系,子类方法一定是等于或大于父类的方法。子类能够出现的父类不一定能出现,但是父类出现的地方子类一定能够出现。

依赖倒置

如果方法与方法之间或类与类之间,存在太多的依赖关系会导致代码可读性以及可维护性很差。依赖倒置原则能够很好的解决这些问题。

什么是依赖倒置

依赖倒置原则:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。(节选自百度百科)

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  2. 抽象不应该依赖细节
  3. 细节应该依赖抽象

依赖倒置好处

  1. 通过依赖于接口,隔离了具体实现类
  2. 低一层的变动并不会导致高一层的变动
  3. 提高了代码的容错性、扩展性和易于维护

实例

// 抽象枪类
class AbstractGun {
  shoot(){
    throw "Abstract methods cannot be called";
  }
}
// 步枪
class Rifle extends AbstractGun {
  shoot(){
    console.log("步枪射击...");
  }
}
// 狙击枪
class AUG extends AbstractGun {
  shoot(){
    console.log("AUG射击...");
  }
}

从上面的代码可以看出,步枪与狙击枪的shoot全部都是依赖于AbstractGun抽象的枪类,上述编程满足了依赖倒置原则。

接口隔离

什么是接口隔离

接口隔离:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。(节选自百度百科)

接口隔离原则与单一职责原则的审视角度不相同。单一职责原则要求是类和接口的职责单一,注重的职责,这是业务逻辑上的划分。接口隔离原则要求接口的方法尽量少。

接口隔离好处

  1. 避免接口污染
  2. 提高灵活性
  3. 提供定制服务
  4. 实现高内聚

实例

function mix(...mixins) {
 class Mix {}
 for (let mixin of mixins) {
  copyProperties(Mix, mixin);
  copyProperties(Mix.prototype, mixin.prototype);
 }
 return Mix;
}
function copyProperties(target, source) {
 for (let key of Reflect.ownKeys(source)) {
  if ( key !== "constructor"&& key !== "prototype"&& key !== "name") {
   let desc = Object.getOwnPropertyDescriptor(source, key);
   Object.defineProperty(target, key, desc);
  }
 }
}
class Behavior {
  eat(){
    throw "Abstract methods cannot be used";
  }
  call(){
    throw "Abstract methods cannot be used";
  }
}
class Action {
  climbTree(){
    throw "Abstract methods cannot be used";
  }
}
class Dog extends Behavior{
  eat(food){
    console.log(`狗正在吃${food}`);
  }
  hungry(){
    console.log("汪汪汪,我饿了")
  }
}
const CatMin = mix(Behavior,Action);
class Cat extends CatMin{
  eat(food){
    console.log(`猫正在吃${food}`);
  }
  hungry(){
    console.log("喵喵喵,我饿了")
  }
  climbTree(){
    console.log("爬树很开心哦~")
  }
}
let dog = new Dog();
dog.eat("骨头");
dog.hungry();
let cat = new Cat();
cat.eat("鱼");
cat.hungry();
cat.climbTree();

大家一定要好好分析一下上面的代码,共有两个抽象类,分别对应不同的行为,CatDog类拥有共同的行为,但是Cat又拥有其自己单独的行为,使用抽象(即接口)继承其方法,使用接口隔离使其完成各自的工作,各司其职。

迪米特法则

迪米特法则:最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。英文简写为: LoD.(节选自百度百科)

迪米特法则的做法观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。一个类应该对其他对象保持最少的了解。通俗来讲,就是一个类对自己依赖的类知道的越少越好。因为类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

迪米特法则好处

  1. 减少对象之间的耦合性

实例

class ISystem {
  close(){
    throw "Abstract methods cannot be used";
  }
}
class System extends ISystem{
  saveCurrentTask(){
    console.log("saveCurrentTask")
  }
  closeService(){
    console.log("closeService")
  }
  closeScreen(){
    console.log("closeScreen")
  }
  closePower(){
    console.log("closePower")
  }
  close(){
    this.saveCurrentTask();
    this.closeService();
    this.closeScreen();
    this.closePower();
  }
}
class IContainer{
  sendCloseCommand(){
    throw "Abstract methods cannot be used";
  }
}
class Container extends IContainer{
  constructor(){
    super()
    this.system = new System();
  }
  sendCloseCommand(){
    this.system.close();
  }
}
class Person extends IContainer{
  constructor(){
    super();
    this.container = new Container();
  }
  clickCloseButton(){
    this.container.sendCloseCommand();
  }
}
let person = new Person();
person.clickCloseButton();

上面代码中Container作为媒介,其调用类不知道其内部是如何实现,用户去触发按钮,Container把消息通知给计算机,计算机去执行相对应的命令。

组合/聚合复用原则

聚合(Aggregation)表示一种弱的‘拥有'关系,体现的是A对象可以包含B对象但B对象不是A对象的一部分。

合成(Composition)则是一种强的'拥有'关系,体现了严格的部分和整体关系,部分和整体的生命周期一样。

组合/聚合:是通过获得其他对象的引用,在运行时刻动态定义的,也就是在一个对象中保存其他对象的属性,这种方式要求对象有良好定义的接口,并且这个接口也不经常发生改变,而且对象只能通过接口来访问,这样我们并不破坏封装性,所以只要类型一致,运行时还可以通过一个对象替换另外一个对象。

优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上,这样类和类继承层次会保持较小规模,而且不太可能增长为不可控制的庞然大物。

组合/聚合复用原则好处

  1. 新的实现较为容易,因为超类的大部分功能可通过继承关系自动进入子类;
  2. 修改或扩展继承而来的实现较为容易。

实例

function mix(...mixins) {
 class Mix {}
 for (let mixin of mixins) {
  copyProperties(Mix, mixin);
  copyProperties(Mix.prototype, mixin.prototype);
 }
 return Mix;
}
function copyProperties(target, source) {
 for (let key of Reflect.ownKeys(source)) {
  if ( key !== "constructor"&& key !== "prototype"&& key !== "name") {
   let desc = Object.getOwnPropertyDescriptor(source, key);
   Object.defineProperty(target, key, desc);
  }
 }
}
class Savings {
  saveMoney(){
    console.log("存钱");
  }
  withdrawMoney(){
    console.log("取钱");
  }
}
class Credit {
  overdraft(){
    console.log("透支")
  }
}
const CarMin = mix(Savings,Credit);
class UserCar extends CarMin {
  constructor(num,carUserName){
    super();
    console.log()
    this.carNum = num;
    this.carUserName = carUserName;
  }
  getCarNum(){
    return this.carNum;
  }
  getCarUserName(){
    return this.carUserName;
  }
}
let myCar = new UserCar(123456798,"Aaron");
console.log(myCar.getCarNum());
console.log(myCar.getCarUserName());
myCar.saveMoney();
myCar.withdrawMoney();
myCar.overdraft();

总结

这些原则在设计模式中体现的淋淋尽致,设计模式就是实现了这些原则,从而达到了代码复用、增强了系统的扩展性。所以设计模式被很多人奉为经典。我们可以通过好好的研究设计模式,来慢慢的体会这些设计原则。

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

(0)

相关推荐

  • JavaScript面向对象三个基本特征实例详解【封装、继承与多态】

    本文实例讲述了JavaScript面向对象三个基本特征.分享给大家供大家参考,具体如下: 了解过面向对象的同学应该都知道,面向对象三个基本特征是:封装.继承.多态,但是对于这三个词具体可能不太了解.对于前端来讲接触最多的可能就是封装与继承,对于多态来说可能就不是那么了解了. 封装 在说封装之先了解一下封装到底是什么? 什么是封装 封装:将对象运行所需的资源封装在程序对象中--基本上,是方法和数据.对象是"公布其接口".其他附加到这些接口上的对象不需要关心对象实现的方法即可使用这个对象.

  • JavaScript面向对象编程入门教程

    尽管面向对象JavaScript与其他语言相比之下存在差异,并由此引发了一些争论,但毋庸置疑,JavaScript具有强大的面向对象编程能力 本文先从介绍面向对象编程开始,然后回顾JavaScript对象模型,最后演示JavaScript中的面向对象编程概念. JavaScript回顾 如果你对诸如变量(variables).类型(types).函数(functions).以及作用域(scope)等JavaScript概念觉得心里没底,那么你可以阅读重新介绍JavaScript中的这些主题.你还

  • javascript 面向对象编程基础 多态

    Javascript已经可以模拟出面向对象的封装和继承特性,但是不幸的是Javascript对多态特性的支持非常弱!其它面向对象语言的多态一般都由方法重载和虚方法来实现多态,Javascript也通过这两种途径来实现! 重载:由于Javascript是弱类型的语言,而且又支持可变参数,当我们定义重载方法的时候,解释器无法通过参数类型和参数个数来区分不同的重载方法,因此方法重载是不被支持的!当先后定义了同名的方法的时候,后定义的方法会覆盖先定义的方法! 既然解释器无法分辨重载方法,那就手动区分不同

  • javascript 面向对象全新理练之数据的封装

    今天主要讨论如何在 JavaScript 脚本中实现数据的封装(encapsulation).数据封装说的简单点就是把不希望调用者看见的内容隐藏起来.它是面向对象程序设计的三要素之首,其它两个是继承和多态,关于它们的内容在后面再讨论. 关于数据封装的实现,在 C++.Java.C# 等语言中是通过 public.private.static 等关键字实现的.在 JavaScript 则采用了另外一种截然不同的形式.在讨论如何具体实现某种方式的数据封装前,我们先说几个简单的,大家所熟知却又容易忽略

  • 面向对象的Javascript之二(接口实现介绍)

    就足以说明接口在面向对象的领域中有多重要.但JS却不像其他面向对象的高级语言(C#,Java,C++等)拥有内建的接口机制,以确定一组对象和另一组对象包含相似的的特性.所幸的是JS拥有强大的灵活性(我在上文已谈过),这使得模仿接口特性又变得非常简单.那么到底是接口呢? 接口,为一些具有相似行为的类之间(可能为同一种类型,也可能为不同类型)提供统一的方法定义,使这些类之间能够很好的实现通信. 那使用接口到底有哪些好处呢?简单地说,可提高系统相似模块的重用性,使得不同类的通信更加稳固.一旦实现接口,

  • Javascript 面向对象(一)(共有方法,私有方法,特权方法)

    私有方法:私有方法本身是可以访问类内部的所有属性的,即私有属性和公有属性.但是私有方法是不可以在类的外部被调用. 私有方法写法: 复制代码 代码如下: function myClass () { var private_attribute = initial_value; function private_method () { } var private_method2 = function () { } } 实例showpet()就是一个私有方法 复制代码 代码如下: var pet=fun

  • js面向对象之常见创建对象的几种方式(工厂模式、构造函数模式、原型模式)

    在上篇文章给大家介绍了javascript面向对象基础,本篇文章继续深入学习javascript面向对象,JS的语法非常灵活,简单的对象创建就有好几种不同的方法.这些过于灵活的地方有时候确实很让人迷惑,那么今天我们就来梳理一下JS中常用的创建对象的几种方法吧. 前言 虽然使用 Object构造函数 或者使用 对象字面量 可以很方便的用来创建一个对象,但这种方式有一个明显的缺点:使用一个接口创建多个对象会产生很多冗余的代码.因此为了解决这个问题,人们开始使用以下几种方式来常见对象. 工厂模式 该模

  • js面向对象之静态方法和静态属性实例分析

    本文实例讲述了js面向对象之静态方法和静态属性.分享给大家供大家参考.具体分析如下: 先看如下代码: 复制代码 代码如下: <script type="text/javascript"> function Bird(){  this.wing = 2;  this.fly = function(){   alert("我是鸟,我会飞");  } } var maque = new Bird();//创建maque对象后,我们可以使用Bird的方法和属性

  • JS面向对象基础讲解(工厂模式、构造函数模式、原型模式、混合模式、动态原型模式)

    什么是面向对象?面向对象是一种思想!(废话). 面向对象可以把程序中的关键模块都视为对象,而模块拥有属性及方法.这样我们如果把一些属性及方法封装起来,日后使用将非常方便,也可以避免繁琐重复的工作.接下来将为大家讲解在JS中面向对象的实现.   工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,而由于在ECMAScript中无法创建类,因此用函数封装以特定接口创建对象.其实现方法非常简单,也就是在函数内创建一个对象,给对象赋予属性及方法再将对象返回即可. function createBlo

  • JS 面向对象的5钟写法

    Java代码 复制代码 代码如下: //第1种写法 function Circle(r) { this.r = r; } Circle.PI = 3.14159; Circle.prototype.area = function() { return Circle.PI * this.r * this.r; } var c = new Circle(1.0); alert(c.area()); Java代码 复制代码 代码如下: //第2种写法 var Circle = function() {

  • 彻底理解js面向对象之继承

    说道这个继承,了解object-oriented的朋友都知道,大多oo语言都有两种,一种是接口继承(只继承方法签名):一种是实现继承(继承实际的方法) 奈何js中没有签名,因而只有实现继承,而且靠的是原型链实现的.下面正式的说一说js中继承那点事儿 1.原型链 原型链:实现继承的主要方法,利用原型让一个引用类型继承另一个引用类型的属性和方法. 回顾:构造函数,原型,实例三者的关系 每一个构造函数都有一个原型对象(Person.prototype):原型对象都包含指向构造函数的指针(constru

随机推荐