网站建设公司大型,易企营销型网站建设企业,公司设计品牌公司,wordpress导出导入一、设计模式前言
面向对象
主流的编程范式或者是编程风格有三种#xff0c;它们分别是面向过程、面向对象和函数式编程。面向对象这种编程风格又是这其中最主流的。现在比较流行的编程语言大部分都是面向对象编程语言。大部分项目也都是基于面向对象编程风格开发的。面向对…一、设计模式前言
面向对象
主流的编程范式或者是编程风格有三种它们分别是面向过程、面向对象和函数式编程。面向对象这种编程风格又是这其中最主流的。现在比较流行的编程语言大部分都是面向对象编程语言。大部分项目也都是基于面向对象编程风格开发的。面向对象编程因为其具有丰富的特性封装、抽象、继承、多态可以实现很多复杂的设计思路是很多设计原则、设计模式编码实现的基础。
面向对象的四大特性封装、抽象、继承、多态 面向对象编程与面向过程编程的区别和联系 面向对象分析、面向对象设计、面向对象编程 接口和抽象类的区别以及各自的应用场景 基于接口而非实现编程的设计思想 多用组合少用继承的设计思想 面向过程的贫血模型和面向对象的充血模型
设计模式
23 种经典的设计模式。它们又可以分为三大类创建型、结构型、行为型。
创建型 常用的有单例模式、工厂模式工厂方法和抽象工厂、建造者模式。 不常用的有原型模式。结构型 常用的有代理模式、桥接模式、装饰者模式、适配器模式。 不常用的有门面模式、组合模式、享元模式。行为型 常用的有观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式。 不常用的有访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
代码重构
在开发初期除非特别必须我们一定不要过度设计应用复杂的设计模式。而是当代码出现问题的时候我们再针对问题应用原则和模式进行重构。这样就能有效避免前期的过度设计。
对于重构这部分内容你需要掌握以下几个知识点 重构的目的why、对象what、时机when、方法how 保证重构不出错的技术手段单元测试和代码的可测试性 两种不同规模的重构大重构大规模高层次和小重构小规模低层次。
二、面向对象
面向对象编程的英文缩写是 OOP全称是 Object Oriented Programming。对应地面向对象编程语言的英文缩写是 OOPL全称是 Object Oriented Programming Language。
1、面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元并将封装、抽象、继承、多态四个特性作为代码设计和实现的基石 。 2、面向对象编程语言是支持类或对象的语法机制并有现成的语法机制能方便地实现面向对象编程四大特性封装、抽象、继承、多态的编程语言。
封装Encapsulation
封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口授权外部仅能通过类提供的方式或者叫函数来访问内部信息或者数据。
下面这段代码是金融系统中一个简化版的虚拟钱包的代码实现。在金融系统中我们会给每个用户创建一个虚拟钱包用来记录用户在我们的系统中的虚拟货币量。
public class Wallet {private String id;private long createTime;private BigDecimal balance;private long balanceLastModifiedTime;// ...省略其他属性...public Wallet() {this.id IdGenerator.getInstance().generate();this.createTime System.currentTimeMillis();this.balance BigDecimal.ZERO;this.balanceLastModifiedTime System.currentTimeMillis();}// 注意下面对get方法做了代码折叠是为了减少代码所占文章的篇幅public String getId() { return this.id; }public long getCreateTime() { return this.createTime; }public BigDecimal getBalance() { return this.balance; }public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; }public void increaseBalance(BigDecimal increasedAmount) {if (increasedAmount.compareTo(BigDecimal.ZERO) 0) {throw new InvalidAmountException(...);}this.balance.add(increasedAmount);this.balanceLastModifiedTime System.currentTimeMillis();}public void decreaseBalance(BigDecimal decreasedAmount) {if (decreasedAmount.compareTo(BigDecimal.ZERO) 0) {throw new InvalidAmountException(...);}if (decreasedAmount.compareTo(this.balance) 0) {throw new InsufficientAmountException(...);}this.balance.subtract(decreasedAmount);this.balanceLastModifiedTime System.currentTimeMillis();}
}之所以这样设计是因为从业务的角度来说id、createTime 在创建钱包的时候就确定好了之后不应该再被改动所以我们并没有在 Wallet 类中暴露 id、createTime 这两个属性的任何修改方法比如 set 方法。而且这两个属性的初始化设置对于 Wallet 类的调用者来说也应该是透明的所以我们在 Wallet 类的构造函数内部将其初始化设置好而不是通过构造函数的参数来外部赋值。 对于钱包余额 balance 这个属性从业务的角度来说只能增或者减不会被重新设置。所以我们在 Wallet 类中只暴露了 increaseBalance() 和 decreaseBalance() 方法并没有暴露 set 方法。对于 balanceLastModifiedTime 这个属性它完全是跟 balance 这个属性的修改操作绑定在一起的。只有在 balance 修改的时候这个属性才会被修改。所以我们把 balanceLastModifiedTime 这个属性的修改操作完全封装在了 increaseBalance() 和 decreaseBalance() 两个方法中不对外暴露任何修改这个属性的方法和业务细节。这样也可以保证 balance 和 balanceLastModifiedTime 两个数据的一致性。 对于封装这个特性我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制。例子中的 private、public 等关键字就是 Java 语言中的访问权限控制语法。private 关键字修饰的属性只能类本身访问可以保护其不被类之外的代码直接访问。如果 Java 语言没有提供访问权限控制语法所有的属性默认都是 public 的那任意外部代码都可以通过类似 wallet.id123; 这样的方式直接访问、修改属性也就没办法达到隐藏信息和保护数据的目的了也就无法支持封装特性了。
封装的意义是什么它能解决什么编程问题 如果我们对类中属性的访问不做限制那任何代码都可以访问、修改类中的属性虽然这样看起来更加灵活但从另一方面来说过度灵活也意味着不可控属性可以随意被以各种奇葩的方式修改而且修改逻辑可能散落在代码中的各个角落势必影响代码的可读性、可维护性。比如某个同事在不了解业务逻辑的情况下在某段代码中“偷偷地”重设了 wallet 中的 balanceLastModifiedTime 属性这就会导致 balance 和 balanceLastModifiedTime 两个数据不一致。 除此之外类仅仅通过有限的方法暴露必要的操作也能提高类的易用性。如果我们把类属性都暴露给类的调用者调用者想要正确地操作这些属性就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反如果我们将属性封装起来暴露少许的几个必要的方法给调用者使用调用者就不需要了解太多背后的业务细节用错的概率就减少很多。这就好比如果一个冰箱有很多按钮你就要研究很长时间还不一定能操作正确。相反如果只有几个必要的按钮比如开、停、调节温度你一眼就能知道该如何来操作而且操作出错的概率也会降低很多。
抽象Abstraction
封装主要讲的是如何隐藏信息、保护数据而抽象讲的是如何隐藏方法的具体实现让调用者只需要关心方法提供了哪些功能并不需要知道这些功能是如何实现的。 在面向对象编程中我们常借助编程语言提供的接口类比如 Java 中的 interface 关键字语法或者抽象类比如 Java 中的 abstract 关键字语法这两种语法机制来实现抽象这一特性。
public interface IPictureStorage {void savePicture(Picture picture);Image getPicture(String pictureId);void deletePicture(String pictureId);void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}public class PictureStorage implements IPictureStorage {// ...省略其他属性...Overridepublic void savePicture(Picture picture) { ... }Overridepublic Image getPicture(String pictureId) { ... }Overridepublic void deletePicture(String pictureId) { ... }Overridepublic void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... }
}在上面的这段代码中我们利用 Java 中的 interface 接口语法来实现抽象特性。调用者在使用图片存储功能的时候只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了不需要去查看 PictureStorage 类里的具体实现逻辑。 实际上抽象这个特性是非常容易实现的并不需要非得依靠接口类或者抽象类这些特殊语法机制来支持。换句话说并不是说一定要为实现类PictureStorage抽象出接口类IPictureStorage才叫作抽象。即便不编写 IPictureStorage 接口类单纯的 PictureStorage 类本身就满足抽象特性。 之所以这么说那是因为类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑这本身就是一种抽象。调用者在使用函数的时候并不需要去研究函数内部的实现逻辑只需要通过函数的命名、注释或者文档了解其提供了什么功能就可以直接使用了。比如我们在使用 C 语言的 malloc() 函数的时候并不需要了解它的底层代码是怎么实现的。 除此之外在上一节课中我们还提到抽象有时候会被排除在面向对象的四大特性之外当时我卖了一个关子现在我就来解释一下为什么。 抽象这个概念是一个非常通用的设计思想并不单单用在面向对象编程中也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持只需要提供“函数”这一非常基础的语法机制就可以实现抽象特性、所以它没有很强的“特异性”有时候并不被看作面向对象编程的特性之一。 抽象的意义是什么它能解决什么编程问题 实际上如果上升一个思考层面的话抽象及其前面讲到的封装都是人类处理复杂性的有效手段。在面对复杂系统的时候人脑能承受的信息复杂程度是有限的所以我们必须忽略掉一些非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路正好帮我们的大脑过滤掉许多非必要的信息。 除此之外抽象作为一个非常宽泛的设计思想在代码设计中起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想比如基于接口而非实现编程、开闭原则对扩展开放、对修改关闭、代码解耦降低代码的耦合性等。我们在讲到后面的内容的时候会具体来解释。 换一个角度来考虑我们在定义或者叫命名类的方法的时候也要有抽象思维不要在方法定义中暴露太多的实现细节以保证在某个时间点需要改变方法的实现逻辑的时候不用去修改其定义。举个简单例子比如 getAliyunPictureUrl() 就不是一个具有抽象思维的命名因为某一天如果我们不再把图片存储在阿里云上而是存储在私有云上那这个命名也要随之被修改。相反如果我们定义一个比较抽象的函数比如叫作 getPictureUrl()那即便内部存储方式修改了我们也不需要修改命名。
继承Inheritance
学习完了封装和抽象两个特性我们再来看继承特性。如果你熟悉的是类似 Java、C 这样的面向对象的编程语言那你对继承这一特性应该不陌生了。继承是用来表示类之间的 is-a 关系比如猫是一种哺乳动物。从继承关系上来讲继承可以分为两种模式单继承和多继承。单继承表示一个子类只继承一个父类多继承表示一个子类可以继承多个父类比如猫既是哺乳动物又是爬行动物。 为了实现继承这个特性编程语言需要提供特殊的语法机制来支持比如 Java 使用 extends 关键字来实现继承C 使用冒号class B : public APython 使用 parentheses ()Ruby 使用 。不过有些编程语言只支持单继承不支持多重继承比如 Java、PHP、C#、Ruby 等而有些编程语言既支持单重继承也支持多重继承比如 C、Python、Perl 等。 为什么有些语言支持多重继承有些语言不支持呢这个问题留给你自己去研究你可以针对你熟悉的编程语言在留言区写一写具体的原因。 继承特性的定义讲完了我们再来看继承存在的意义是什么它能解决什么编程问题 继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法我们就可以将这些相同的部分抽取到父类中让两个子类继承父类。这样两个子类就可以重用父类中的代码避免代码重复写多遍。不过这一点也并不是继承所独有的我们也可以通过其他方式来解决这个代码复用的问题比如利用组合关系而不是继承关系。 如果我们再上升一个思维层面去思考继承这一特性可以这么理解我们代码中有一个猫类有一个哺乳动物类。猫属于哺乳动物从人类认知的角度上来说是一种 is-a 关系。我们通过继承来关联两个类反应真实世界中的这种关系非常符合人类的认知而且从设计的角度来说也有一种结构美感。 继承的概念很好理解也很容易使用。不过过度使用继承继承层次过深过复杂就会导致代码可读性、可维护性变差。为了了解一个类的功能我们不仅需要查看这个类的代码还需要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。还有子类和父类高度耦合修改父类的代码会直接影响到子类。 所以继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该尽量少用甚至不用。关于这个问题在后面讲到“多用组合少用继承”这种设计思想的时候我会非常详细地再讲解这里暂时就不展开讲解了。
多态Polymorphism
学习完了封装、抽象、继承之后我们再来看面向对象编程的最后一个特性多态。多态是指子类可以替换父类在实际的代码运行过程中调用子类的方法实现。对于多态这种特性纯文字解释不好理解我们还是看一个具体的例子。
public class DynamicArray {private static final int DEFAULT_CAPACITY 10;protected int size 0;protected int capacity DEFAULT_CAPACITY;protected Integer[] elements new Integer[DEFAULT_CAPACITY];public int size() { return this.size; }public Integer get(int index) { return elements[index];}//...省略n多方法...public void add(Integer e) {ensureCapacity();elements[size] e;}protected void ensureCapacity() {//...如果数组满了就扩容...代码省略...}
}public class SortedDynamicArray extends DynamicArray {Overridepublic void add(Integer e) {ensureCapacity();int i;for (i size-1; i0; --i) { //保证数组中的数据有序if (elements[i] e) {elements[i1] elements[i];} else {break;}}elements[i1] e;size;}
}public class Example {public static void test(DynamicArray dynamicArray) {dynamicArray.add(5);dynamicArray.add(1);dynamicArray.add(3);for (int i 0; i dynamicArray.size(); i) {System.out.println(dynamicArray.get(i));}}public static void main(String args[]) {DynamicArray dynamicArray new SortedDynamicArray();test(dynamicArray); // 打印结果1、3、5}
}多态这种特性也需要编程语言提供特殊的语法机制来实现。在上面的例子中我们用到了三个语法机制来实现多态。 第一个语法机制是编程语言要支持父类对象可以引用子类对象也就是可以将 SortedDynamicArray 传递给 DynamicArray。 第二个语法机制是编程语言要支持继承也就是 SortedDynamicArray 继承了 DynamicArray才能将 SortedDyamicArray 传递给 DynamicArray。 第三个语法机制是编程语言要支持子类可以重写override父类中的方法也就是 SortedDyamicArray 重写了 DynamicArray 中的 add() 方法。 通过这三种语法机制配合在一起我们就实现了在 test() 方法中子类 SortedDyamicArray 替换父类 DynamicArray执行子类 SortedDyamicArray 的 add() 方法也就是实现了多态特性。 对于多态特性的实现方式除了利用“继承加方法重写”这种实现方式之外我们还有其他两种比较常见的的实现方式一个是利用接口类语法另一个是利用 duck-typing 语法。不过并不是每种编程语言都支持接口类或者 duck-typing 这两种语法机制比如 C 就不支持接口类语法而 duck-typing 只有一些动态语言才支持比如 Python、JavaScript 等。 接下来我们先来看如何利用接口类来实现多态特性。我们还是先来看一段代码。
public interface Iterator {boolean hasNext();String next();String remove();
}public class Array implements Iterator {private String[] data;public boolean hasNext() { ... }public String next() { ... }public String remove() { ... }//...省略其他方法...
}public class LinkedList implements Iterator {private LinkedListNode head;public boolean hasNext() { ... }public String next() { ... }public String remove() { ... }//...省略其他方法...
}public class Demo {private static void print(Iterator iterator) {while (iterator.hasNext()) {System.out.println(iterator.next());}}public static void main(String[] args) {Iterator arrayIterator new Array();print(arrayIterator);Iterator linkedListIterator new LinkedList();print(linkedListIterator);}
}在这段代码中Iterator 是一个接口类定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator。我们通过传递不同类型的实现类Array、LinkedList到 print(Iterator iterator) 函数中支持动态的调用不同的 next()、hasNext() 实现。 具体点讲就是当我们往 print(Iterator iterator) 函数传递 Array 类型的对象的时候print(Iterator iterator) 函数就会调用 Array 的 next()、hasNext() 的实现逻辑当我们往 print(Iterator iterator) 函数传递 LinkedList 类型的对象的时候print(Iterator iterator) 函数就会调用 LinkedList 的 next()、hasNext() 的实现逻辑。 刚刚讲的是用接口类来实现多态特性。现在我们再来看下如何用 duck-typing 来实现多态特性。我们还是先来看一段代码。这是一段 Python 代码。
class Logger:def record(self):print(“I write a log into file.”)class DB:def record(self):print(“I insert data into db. ”)def test(recorder):recorder.record()def demo():logger Logger()db DB()test(logger)test(db)从这段代码中我们发现duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系既不是继承关系也不是接口和实现的关系但是只要它们都有定义了 record() 方法就可以被传递到 test() 方法中在实际运行的时候执行对应的 record() 方法。 也就是说只要两个类具有相同的方法就可以实现多态并不要求两个类之间有任何关系这就是所谓的 duck-typing是一些动态语言所特有的语法机制。而像 Java 这样的静态语言通过继承实现多态特性必须要求两个类之间有继承关系通过接口实现多态特性类必须实现对应的接口。 多态特性存在的意义是什么它能解决什么编程问题
多态特性能提高代码的可扩展性和复用性。为什么这么说呢我们回过头去看讲解多态特性的时候举的第二个代码实例Iterator 的例子。 在那个例子中我们利用多态的特性仅用一个 print() 函数就可以实现遍历打印不同类型Array、LinkedList集合的数据。当再增加一种要遍历打印的类型的时候比如 HashMap我们只需让 HashMap 实现 Iterator 接口重新实现自己的 hasNext()、next() 等方法就可以了完全不需要改动 print() 函数的代码。所以说多态提高了代码的可扩展性。 如果我们不使用多态特性我们就无法将不同的集合类型Array、LinkedList传递给相同的函数print(Iterator iterator) 函数。我们需要针对每种要遍历打印的集合分别实现不同的 print() 函数比如针对 Array我们要实现 print(Array array) 函数针对 LinkedList我们要实现 print(LinkedList linkedList) 函数。而利用多态特性我们只需要实现一个 print() 函数的打印逻辑就能应对各种集合数据的打印操作这显然提高了代码的复用性。 除此之外多态也是很多设计模式、设计原则、编程技巧的代码实现基础比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。
关于封装特性 封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口授权外部仅能通过类提供的方式来访问内部信息或者数据。它需要编程语言提供权限访问控制语法来支持例如 Java 中的 private、protected、public 关键字。封装特性存在的意义一方面是保护数据不被随意修改提高代码的可维护性另一方面是仅暴露有限的必要接口提高类的易用性。 2. 关于抽象特性 封装主要讲如何隐藏信息、保护数据那抽象就是讲如何隐藏方法的具体实现让使用者只需要关心方法提供了哪些功能不需要知道这些功能是如何实现的。抽象可以通过接口类或者抽象类来实现但也并不需要特殊的语法机制来支持。抽象存在的意义一方面是提高代码的可扩展性、维护性修改实现不需要改变定义减少代码的改动范围另一方面它也是处理复杂系统的有效手段能有效地过滤掉不必要关注的信息。 3. 关于继承特性 继承是用来表示类之间的 is-a 关系分为两种模式单继承和多继承。单继承表示一个子类只继承一个父类多继承表示一个子类可以继承多个父类。为了实现继承这个特性编程语言需要提供特殊的语法机制来支持。继承主要是用来解决代码复用的问题。 4. 关于多态特性 多态是指子类可以替换父类在实际的代码运行过程中调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现比如继承、接口类、duck-typing。多态可以提高代码的扩展性和复用性是很多设计模式、设计原则、编程技巧的代码实现基础。
总结
争哥对面向对象的总结完美符合 What/How/Why 模型我按照模型作下梳理。
封装 What隐藏信息保护数据访问。 How暴露有限接口和属性需要编程语言提供访问控制的语法。 Why提高代码可维护性降低接口复杂度提高类的易用性。
抽象 What: 隐藏具体实现使用者只需关心功能无需关心实现。 How: 通过接口类或者抽象类实现特殊语法机制非必须。 Why: 提高代码的扩展性、维护性降低复杂度减少细节负担。
继承 What: 表示 is-a 关系分为单继承和多继承。 How: 需要编程语言提供特殊语法机制。例如 Java 的 “extends”C 的 “:” 。 Why: 解决代码复用问题。
多态 What: 子类替换父类在运行时调用子类的实现。 How: 需要编程语言提供特殊的语法机制。比如继承、接口类、duck-typing。 Why: 提高代码扩展性和复用性。
3W 模型的关键在于 Why没有 Why其它两个就没有存在的意义。从四大特性可以看出面向对象的终极目的只有一个可维护性。易扩展、易复用降低复杂度等等都属于可维护性的实现方式。
Java 不支持多重继承的原因
多重继承有副作用钻石问题(菱形继承)。 假设类 B 和类 C 继承自类 A且都重写了类 A 中的同一个方法而类 D 同时继承了类 B 和类 C那么此时类 D 会继承 B、C 的方法那对于 B、C 重写的 A 中的方法类 D 会继承哪一个呢这里就会产生歧义。 考虑到这种二义性问题Java 不支持多重继承。但是 Java 支持多接口实现因为接口中的方法是抽象的从JDK1.8之后接口中允许给出一些默认方法的实现这里不考虑这个就算一个类实现了多个接口且这些接口中存在某个同名方法但是我们在实现接口的时候这个同名方法需要由我们这个实现类自己来实现所以并不会出现二义性的问题。