Jser 设计模式系列之面向对象 - 接口封装与继承

简介:

GOF在《设计模式》中说到:面向接口编程,而非面向实现编程

鉴于此,这个概念可见一斑!

JS却不像其他面向对象的高级语言(C#,Java,C++等)拥有内建的接口机制,以确定一组对象和另一组对象包含相似的的特性。所幸的是JS拥有强大的灵活性,这使得模仿接口特性又变得非常简单。那么到底是接口呢?

 

接口概念:

接口提供了一种用以说明一个对象应该具有那些方法的手段

接口,为一些具有相似行为的类之间(可能为同一种类型,也可能为不同类型)提供统一的方法定义,使这些类之间能够很好的实现通信

 

使用接口的优点:

  • 自我描述性,促进代码的重用
  • 明确一个类实现的方法,帮助其使用这个类
  • 稳定不同类之间的通信

一个需求,需要多个部门协调合作的时候,接口的概念就特别重要了,每个部门可以按部就班的做自己的事情,涉及到交互的时候,提供接口处理即可

就好像主板上的内存条,CPU,硬盘,主板提供各种接口,其他设备直接按相应的接口插入即可

 

javascript语言要实现接口的概念还是有局限性的问题的

  • 弱类型,具有极强的表现力的语言,那么使用了接口后,其实就强化了类型的作用,降低了语言的灵活性了
  • 最重要的就是JS没有对这种机制的支持,没有强制性

 


javascript中模仿接口

常用的几种手法:

  • 通过注释
  • 通过属性检查模仿
  • 鸭式辨型模仿

接口本身就是一个抽象的概念,至于如何实现各有不同

这是我的一段项目代码,接口是通过模块闭包实现的,这样的好处更能体现出封装性

复制代码
//引入编译模块
define('ProcessMgr', [
    'Compile'
], function(compile) {

    var flipContentProcessed,
        flipWidgetProcessed, disposeWidgetBind, objectKeys,
        converWidgetWapper, ActionMgr, getHotspot,

        //内部消息传递接口
        //
        //  1 构建节点树
        //       A 构建纯DOM节点
        //       B 构建content对象, 这是第二种加载模式,动态预加载
        //  2 绑定节点事件
        //  3 手动触发事件
        //  4 自动触发事件
        //  5 事件委托处理
        //  6 翻页动作
        //  7 翻页完成动作
        //  8 复位动作
        //  9 销毁动作
        //  10 移除整个页面结构
        //
       
ProcessMgr =
 { 
            'preCompile'     : preCompile,
            'executeCompile' : executeCompile,
            'assignTrigger'  : assignTrigger,
            'assignAutoRun'  : assignAutoRun,
            'assignSuspend'  : assignSuspend,
            'assignDispose'  : assignDispose,
            'recovery'       : recovery,
            'destroy'        : destroy,
            'removePage'     : removePage
        },

        ...........具体处理的方法...................


        return ProcessMgr; //返回对外接口
 })
复制代码

 

jQuery的接口更加的直接,直接挂在在对应的原型链上或是静态链

 


封装

为什么要封装?

因为我们不关心它是如何实现的,我们只关心如何使用

手机,电脑,我们接触的形形色色的东西,其实我们一直都只是在使用它

同样的道理,我们放到程序设计中也是如此,封装实现的细节 ,降低对象之间的耦合程序,保持数据的完整性与修改的约束

所以说一个设计良好的API可以让开发者赏心悦目

 

javascript实现封装的手段

对于JS语法规则,我们要牢牢抓住3点

  • JS函数是一等对象
  • JS是函数级的作用域,意味着函数内部的变量不能被外部访问
  • JS是词法性质的静态作用域,换句话说,即便在执行期作用域还是在定义的时候就预先分配好了

根据这3个规则我们就可以干很多别的语言干不了的事了

我们来模拟一个完整的封装

私有属性和方法

复制代码
var encapsulation = function(){
    //私有属性
    var name = 'aaron'
   //私有方法
    var getName = function(){
        alert(name)
    }
}

alert(name) //
复制代码

函数作用域实现变量与方法私有化,外边自然无法方法,当然这种完全没有实际意义了,我们要配合后面的处理

 

特权属性和方法

简单的说就能够让实例直接有权力访问内部的属性与方法,所以在设计上要修改下实现手法,

复制代码
var encapsulation = function(){
    //特权
    this.name = 'aaron'
    this.getName = function(){
        alert(name)
    }
}
alert(new encapsulation().name) //aaron
复制代码

弊端也显而易见,每次new一个新的对象,特权属性与方法都会被重新拷贝一份,也就是需要单独占据一块堆内存

 

共有属性和方法

*注意了,命名函数与函数表达式在本质上虽然没什么区别,但是在处理上还是有很大不同

函数表达式

复制代码
var encapsulation = function(){
    //静态属性方法
    encapsulation.name = 'aaron'
    encapsulation.getName = function(){
        alert(name)
    }
}

console.log( encapsulation.name ) //
复制代码

 

命名函数

即便方法不执行,静态属性也能访问到,就证明了JS有一种预解析的机制,也就是常说的函数提升了

复制代码
function encapsulation(){
    //静态属性方法
    encapsulation.name = 'aaron'
    encapsulation.getName = function(){
        alert(name)
    }
}

console.log( encapsulation.name ) // aaron
复制代码

 

共有静态属性和方法

这是最最常用的,JS 的prototype原型特性,可以共享所有属性与方法,当然,属性是引用类型的时候就会存在问题了

复制代码
var encapsulation = function(){
    //共享属性方法
    encapsulation.prototype.name = 'aaron'
    encapsulation.prototype.getName = function(){
        alert(name)
    }
}

console.log(new encapsulation().name ) // aaron
console.log(new encapsulation().name ) // aaron
复制代码

 

 


继承

大型设计中,代码肯定是很多的,所以我们需要减少重复性的代码,尽可能的弱化对象之间的耦合:

通过几种手段

  • 类式继承
  • 原型式继承
  • 掺元混入

当然并非所有的重复代码都可以使用一种继承方法,所以我们一般要区分共性是否一致

我项目中使用的继承手段

image

呵呵,眼熟吧,借鉴EXT的处理机制,有继承与混入,自己改了点,毕竟那种分层结构,倒序调用,还有点不合适套用,下文会介绍实现

 

类式继承

现在的框架,库大多继承的手段都是类式继承,而且继承的处理方式也基本一致,但是里面的细节问题可能大家没注意到,我们一起分析下

我们看看几个框架的继承实现

mootools

Class

 

Backbone

extend

 

ext

extend

ext比较特殊,因为是通过注入的手法,实现继承了类名转化继承混入静态扩充

 

对比几个框架,我们红线部分的相同之处没?实现原理确是一样的,包括EXT也一样,只是在具体实现上增加了各自的方案,

 


设计的原理

抛开复杂的框架,我们看看设计的底层原理是什么,又会有什么问题?

原型链作为JS实现继承的主要方法,其根本的思路利用原型链让一个引用类型,继承另一个引用类型

原型与实例的关系:

构造器有一个原型对象,原型对象有一个指针指向构造器,那么实例则是包含一个指向原型的指针

所以实例只与原型有关系

 
 
实现中的细节:
 
复制代码
function SuperType(){ //父类
   this.property = true; 
} 

SuperType.prototype.getSuperValue = function(){
  return this.property; 
}; 

function SubType(){ //子类
    this.subproperty = false; 
} 

//继承了 SuperType 
SubType.prototype = new SuperType(); //实现原型继承,引用 

SubType.prototype.getSubValue = function (){ 
     return this.subproperty; 
}; 

var instance = new SubType(); 

alert(instance.getSuperValue());      //true
复制代码
 
 
演示文稿1
 
1 继承的本质是引用,那么N多组实例其实都是操作的同一个引用,那么问题来了,如果父类中有个一引用属性,那么一个子类操作修改了,所有的子类都会被影响
 
复制代码
function SuperType(){ 
    this.colors = ["red", "blue", "green"]; 
}

function SubType(){             
} 
 
//继承了 SuperType 
SubType.prototype = new SuperType(); 
 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
alert(instance1.colors);     //"red,blue,green,black" 
 
var instance2 = new SubType(); 
alert(instance2.colors);       //"red,blue,green,black"
复制代码
 
 
2 在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)
 
复制代码
function SuperType(){ 
    this.colors = ["red", "blue", "green"]; 
} 
 
function SubType(){   
    //继承了 SuperType 
    SuperType.call(this); 
} 
 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
alert(instance1.colors);    //"red,blue,green,black" 
 
var instance2 = new SubType(); 
alert(instance2.colors);    //"red,blue,green"
复制代码

通过使用 call()方法(或 apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会具有自己的 colors 属性的副本了

借用构造函数的问题 
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的

 

通过SubType.prototype = new SuperType(); 的方式实现引用的继承,看似很简单,但是里面还有几个问题

  • 不管什么情况,都把父类的实例属性与方法都复制给了子类
  • 如果子类修改了原型共享的引用属性,倒置所有继承的会受引向
  • 借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了

 

看看Backbone的如何处理

var ctor = function () {

 };
ctor.prototype = parent.prototype;

 child.prototype = new ctor();

 child.prototype.constructor = child;

可见backbone引用一个中介ctor函数

其实就是一种代理继承

proxy来实现继承,这样就避免了在classical继承中出现的parent的constructor被使用两次带来的效率问题和在原型链中再次继承this的属性
function obj (o){
    var f = {}
    f.prototype = o
    return new f();
}

最后还要修正一下constructor的指针,因为是继承ctor了

至于原型式继承可以参考高级程序设计3, 比较详细了


本文转自艾伦 Aaron博客园博客,原文链接:http://www.cnblogs.com/aaronjs/p/3390683.html,如需转载请自行联系原作者

相关文章
|
设计模式 API 数据安全/隐私保护
探索设计模式的魅力:外观模式简化术-隐藏复杂性,提供简洁接口的设计秘密
外观模式是一种关键的设计模式,旨在通过提供一个简洁的接口来简化复杂子系统的访问。其核心价值在于将复杂的内部实现细节封装起来,仅通过一个统一的外观对象与客户端交互,从而降低了系统的使用难度和耦合度。在软件开发中,外观模式的重要性不言而喻。它不仅能够提高代码的可读性、可维护性和可扩展性,还能促进团队间的协作和沟通。此外,随着业务需求和技术的发展,外观模式能够适应变化,通过修改外观对象来灵活调整客户端与子系统之间的交互方式。总之,外观模式在软件设计中扮演着举足轻重的角色,是构建高效、稳定且易于维护的软件系统的关键
415 1
探索设计模式的魅力:外观模式简化术-隐藏复杂性,提供简洁接口的设计秘密
|
设计模式
二十三种设计模式全面解析-装饰器模式-超越继承的灵活装扮
二十三种设计模式全面解析-装饰器模式-超越继承的灵活装扮
181 0
|
设计模式 数据库连接 PHP
PHP编程中的面向对象与设计模式
在PHP编程世界中,掌握面向对象编程(OOP)和设计模式是提升代码质量和开发效率的关键。本文将深入浅出地介绍如何在PHP中应用OOP原则和设计模式,以及这些实践如何影响项目架构和维护性。通过实际案例,我们将探索如何利用这些概念来构建更健壮、可扩展的应用程序。
|
设计模式
**工厂模式与抽象工厂模式**都是创建型设计模式,用于封装对象创建,减少耦合
【6月更文挑战第23天】**工厂模式与抽象工厂模式**都是创建型设计模式,用于封装对象创建,减少耦合。工厂模式专注于单个对象,通过具体工厂创建具体产品,适用于简单对象创建;抽象工厂则关注一系列相关产品,提供创建一族对象的接口,适用于处理多个不兼容产品族。选择模式基于问题域的复杂性,单个产品需求时用工厂模式,多产品族时用抽象工厂模式。
227 5
|
设计模式 算法 搜索推荐
抽象类与接口:设计模式的基石
在面向对象设计中,抽象类和接口是多态和封装的核心。抽象类是不能实例化的类,提供子类共享的实现和抽象方法,确保子类间的共性。接口仅定义方法签名,强制实现类提供具体实现,促进松耦合。两者在实现方式、继承和设计目的上有所不同,常用于工厂、策略等设计模式,如策略模式中通过接口动态选择算法,增强了代码灵活性。【6月更文挑战第16天】
267 8
|
设计模式 算法 架构师
【搞懂设计模式】设计模式与面向对象原则
【搞懂设计模式】设计模式与面向对象原则
257 1
|
设计模式 Java 数据库
Java设计模式:桥接模式实现灵活组合,超越单一继承的设计之道(十)
Java设计模式:桥接模式实现灵活组合,超越单一继承的设计之道(十)
|
设计模式
设计模式六大原则之 接口分离原则
设计模式六大原则之 接口分离原则
|
设计模式 存储 Java
JavaSE——面向对象高级二(2/4)-final关键字、常量、抽象类(认识抽象类、抽象类的好处、应用场景-模板方法设计模式)
JavaSE——面向对象高级二(2/4)-final关键字、常量、抽象类(认识抽象类、抽象类的好处、应用场景-模板方法设计模式)
109 0
|
设计模式 缓存 安全
探索设计模式的魅力:从单一继承到组合模式-软件设计的演变与未来
组合模式:构建灵活树形结构的艺术。 组合模式旨在解决如何将对象组合成树形结构,隐藏具体实现,使客户端对单个对象和复合对象的使用具有一致性。通过将对象组合成树形结构,组合模式提供了层次化的结构,使系统更灵活、可扩展。 核心思想在于统一叶节点和组合节点。叶节点代表具体的对象,而组合节点则是其他对象的容器。该设计允许我们以统一的方式处理叶子和组合,简化了许多操作。实践中,组合模式适用于具有树形结构并且希望保持结构灵活的系统。它不仅提高了代码的可重用性和可维护性,还使得添加新功能变得简单,无需修改现有代码。...
241 0