tujunxiong · 2020年02月13日

优雅的设计模式-面向对象基础(上)

如何理解面向对象

面向对象的特性中抽象是封装、继承、多态的前提基础。合理的抽象源于对业务主题合理分析和合理认识。合理的抽象应该是自洽的,易理解的
关于组合和聚合的关系:最核心的区别就是生命周期的区别。组合关系中,整体和个体是一个整体,离开了整体,个体就没有意义,同时产生,同时销毁。而聚合关系中,部分单独个体存在也是具有存在的意义,即使脱离整理,个体也可以单独存在。
别滥用继承:继承的作用更多的时候是为了多态特性服务的
面向对象两个最基础的概念:类和对象

区分什么是"面向对象编程"和什么是"面向对象编程语言"

面向对象就是一个编程规范或编程风格。它是以类和对象为组织代码的基本单元,并通过抽象、封装、继承、多态四个特性最为代码设计和实现的基石

面向对象编程语言是支持类和对象的语法机制,并有现成的语法糖(语法机制),能够方便的实现面向对象编程四大特性的编程语言
区分什么是"面向过程编程"和什么是"面向对象编程"

面向过程编程: 

    以动词为主,分析出解决问题所需要的步奏,然后用函数把这些步奏一步一步实现,使用的时候一个一个依次调用就可以了。

面向对象编程:

    以名词为主,把构成问题事务分解各个对象,建立对象的目的不是为了完成一个步奏,而是为了描述某个事物在整个解决问题的步奏中的行为。

面向对象:狗.吃(骨头) 面向过程:吃.(狗,骨头)

面向对象编程特性基本原则

封装:将抽象出来的属性和方法封装在一起进而达到隐藏信息,保护数据

/*
    * @title 创建对象实现封装有四种方法
    * @method1 对象字面量方式{name: 'Mark_Zhang',key:'value'}  只能创建一次、复用率差、字面量多了代码冗余
    * @method2 内置构造函数创建 var parson =  new Object(); parson.name="内置构造函数创建对象"
    * @method3 简单的工厂函数 function createObj (name){let obj = new Object(); obj.name = name; return obj}
    * @method4  自定义构造函数
  */
  function Person (name, agei) {
    // 构造函数中多包含的属性和方法就可以理解抽象的一部分
    // public  公共属性
    this.name = name; // 实例可以直接通过对象点的方式直接访问
    // private  私有属性
    let age = agei;
    this.getAge = function(){
      return age;
    }
    this.setAge = function(a){
      age = a;
    }
  }
  Person.prototype.show = function(){
    return  this.name + ' 今年' + this.getAge() + '岁';
  }
  // 必须通过 new 关键字创建对象,否则 this 指向的就是 window 对象
  let p1 = new Person('Mark_Zhang',18)
  console.info(p1.name, p1.age) // Mark_Zhang undefined
  // 调用对象方法 (调用对象暴露的接口)
  p1.setAge(30);
  console.info(p1.getAge());
  // 调用原型方法
  console.info(p1.show())
  let p2 = new Person();
  // 利用对象动态性来添加属性
  p1.n = 'sunLine'
  console.info(p2.n) //sunLine
  
  /**
   * @title 通过【构造函数】和【原型法】添加成员的区别
   * @区别一: 通过【原型法】分配的函数(引用类型即可) 所有对象共享
   * @区别二: 通过【原型法】分配的属性( 基本类型)独立
   * @区别2.2: 我们还可以在类的外部通过. 语法进行添加,因为在实例化对象的时候,并不会执行到在类外部通过. 语法添加的属性,所以实例化之后的对象是不能访问到. 语法所添加的对象和属性的,只能通过该类访问。
   * @区别三: 若希望所有对象使用同一个函数,建议使用原型法添加函数,节省内存 
   * @区别四: 通过prototype给所有对象添加方法,不能访问所在类的私有属性和方法
   */

抽象:如何隐藏方法的具体实现,让调用者只需要关系方法提供了那些功能,并不需要知道这些功能具体是怎么实现的。
继承:继承最大的一个好处就是代码复用

继承:子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。

类式继承
所谓的类式继承就是使用的原型方式,将方法添加在父类的原型上,然后子类的原型父类的一个实例化对象

// 声明父类
let SuperClass function  (){
  let id = 1;
  this.name = ["继承"];
  this.superValue = function () {
    console.info("superValue is true")
    console.info('id---->',id)
  }
}
// 通过原型给父类添加方法
SuperClass.prototype.getSuperValue = function() {
  return this.superValue();
}
// 声明子类
let SubClass = function () {
  this.subValue = function () {
    console.info("this is subValue")
  }
}
// 子类继承父类
SubClass.prototype = new SuperClass();
// 为子类添加公有方法
SubClass.prototype.getSubValue = function() {
  return this.subValue();
}
let sub = new SubClass(),
    sub2 = new SubClass();
    
sub.getSuperValue(); // superValue is true
sub.getSubValue(); // this is subValue
​
console.info(sub.id)  // undefined
console.info(sub.name) // 继承
​
sub.name.push('类式继承')
console.info(sub2.name) // ['继承','类式继承']

其中最核心的一句代码是SubClass.prototype = new SuperClass();
类的原型对象prototype对象的作用就是为类的原型添加共有方法的,但是类不能直接访问这些方法,只有将类实例化之后,新创建的对象复制了父类构造函数中的属性和方法,并将原型__proto__ 指向了父类的原型对象。这样子类就可以访问父类的public 和protected 的属性和方法,同时,父类中的private 的属性和方法不会被子类继承。
敲黑板,如上述代码的最后一段,使用类继承的方法,如果父类的构造函数中有引用类型,就会在子类中被所有实例共用,因此一个子类的实例如果更改了这个引用类型,就会影响到其他子类的实例。
构造函数继承

正式因为有了上述的缺点,才有了构造函数继承,构造函数继承的核心思想就是SuperClass.call(this,id),直接改变this的指向,使通过this创建的属性和方法在子类中复制一份,因为是单独复制的,所以各个实例化的子类互不影响。但是会造成内存浪费的问题

// SubClass.prototype = new SuperClass();
function SubClass(id) {
  SuperClass.call(this,id)
}

类继承
构造函数继承
核心思想
子类原型是父类实例化对象
SuperClass.call(this,id)
优点 子类实例化对象的属相和方法都指向父类的原型

每个实例化的子类都是个体互不影响
缺点
子类之间可能相互影响
内存浪费(子列公有的属性和方法通过原型在父类上追加)
组合式继承

针对上面两种继承方式,组合式继承汲取了两者的优点,即避免了内存的浪费,又使得每个实例化的子类互不影响。

// 组合式继承
// 声明父类
let SuperClass = function(name) {
  this.name = name ;
  this.books = ['js','html','css']
}
// 声明父类原型上的f方法
SuperClass.prototype.showBooks = function(){
  console.info(this.books)
}
// 声明子类
let SubClass = function (name) {
  SuperClass.call(this,name);
}
// 子类继承父类(链式继承)
SubClass.prototype = new SuperClass();
​
let subclass1 = new SubClass('java');
let subclass2 = new SubClass('php');
subclass2.showBooks();
subclass1.books.push('ios');    //["js", "html", "css"]
console.log(subclass1.books);  //["js", "html", "css", "ios"]
console.log(subclass2.books);   //["js", "html", "css"]

寄生组合继承

那么问题又来了~组合式继承的方法固然好,但是会导致一个问题,父类的构造函数会被创建两次(call()的时候一遍,new的时候又一遍),所以为了解决这个问题,又出现了寄生组合继承。
刚刚问题的关键是父类的构造函数在类继承和构造函数继承的组合形式中被创建了两遍,但是在类继承中我们并不需要创建父类的构造函数,我们只是要子类继承父类的原型即可。所以说我们先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了
//原型式继承
//原型式继承其实就是类式继承的封装,实现的功能是返回一个实例,改实例的原型继承了传入的o对象

function inheritObject(o) {
    //声明一个过渡函数对象
    function F() {}
    //过渡对象的原型继承父对象
    F.prototype = o;
    //返回一个过渡对象的实例,该实例的原型继承了父对象
    return new F();
}

//寄生式继承
//寄生式继承就是对原型继承的第二次封装,使得子类的原型等于父类的原型。并且在第二次封装的过程中对继承的对象进行了扩展

function inheritPrototype(subClass, superClass){
    //复制一份父类的原型保存在变量中,使得p的原型等于父类的原型
    var p = inheritObject(superClass.prototype);
    //修正因为重写子类原型导致子类constructor属性被修改
    p.constructor = subClass;
    //设置子类的原型
    subClass.prototype = p;
}
//定义父类
var SuperClass = function (name) {
    this.name = name;
    this.books = ['javascript','html','css']
};
//定义父类原型方法
SuperClass.prototype.getBooks = function () {
    console.log(this.books)
};
​
//定义子类
var SubClass = function (name) {
    SuperClass.call(this,name)
}
​
inheritPrototype(SubClass,SuperClass);
​
var subclass1 = new SubClass('php')

多态:最大的一个好处就是提高代码的可拓展性和复用性
总结:

封装

What:隐藏信息,保护数据访问。
How:暴露有限接口和属性,需要编程语言提供访问控制的语法。
Why:提高代码可维护性;降低接口复杂度,提高类的易用性。

抽象

What: 隐藏具体实现,使用者只需关心功能,无需关心实现。
How: 通过接口类或者抽象类实现,特殊语法机制非必须。
Why: 提高代码的扩展性、维护性;降低复杂度,减少细节负担。

继承

What: 表示 is-a 关系,分为单继承和多继承。
How: 需要编程语言提供特殊语法机制。例如 Java 的 “extends”,C++ 的 “:” 。
Why: 解决代码复用问题。

多态

What: 子类替换父类,在运行时调用子类的实现。
How: 需要编程语言提供特殊的语法机制。比如继承、接口类、duck-typing。
Why: 提高代码扩展性和复用性。

3W 模型的关键在于 Why,没有 Why,其它两个就没有存在的意义。从四大特性可以看出,面向对象的终极目的只有一个:可维护性。易扩展、易复用,降低复杂度等等都属于可维护性的实现方式。
图片

推荐阅读
关注数
1
文章数
29
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息