菜菜读设计模式设计模式——设计原则:面向对象
Posted callmewhat
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了菜菜读设计模式设计模式——设计原则:面向对象相关的知识,希望对你有一定的参考价值。
1.面向对象语言(OOP)
面向对象语言最基本的概念就是类与对象,只有拥有这两个概念的语言才是面向对象语言
一般来说面向对象语言拥有四个特征:封装、继承、抽象、多态
但并不是必须具备这四种特性的语言才能成为面向对象语言,比如说 Go 语言,它没有继承的特性,但我们仍认为它是面向对象语言
2.封装、抽象、继承、多态
封装:类通过暴露有限的访问接口,授权外部仅能以类提供的函数来访问内部信息或数据。
实现封装的机制:访问权限控制 (publicprotectdefaultprivate)
同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 | |
public | √ | √ | √ | √ |
protect | √ | √ | √ | |
default(默认空值) | √ | √ | ||
private | √ |
封装的意义:限制对类中属性的访问,可以避免属性被恶意修改,且防止修改逻辑散落在代码的各处,影响代码的可读性、可维护性;类通过有限的方法暴露必要的操作,能提高类的易用性,让调用者不必了解太多背后的业务细节。
抽象:借助编程语言提供的接口类或抽象类来隐藏方法的具体实现,让调用者只需要关心方法提供哪些功能,并不需要关心功能如何实现
实现抽象机制:interface 接口关键字和 abstract 抽象类关键字
抽象为类提供了具有共性的方法但不提供具体的实现方式。同样的,抽象也可以用函数来实现,调用者用函数的时候,并不需要研究函数内部的实现逻辑,仅需要了解其功能即可使用。也正是因此,抽象并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一语法机制,没有很强的 OOP 特异性。
抽象的意义:抽象只关注功能点而不关注实现的设计思路,帮助过滤掉大量的非必要信息。
继承:表示 is-a 关系。分为单继承(Java)和多继承(C++)
Java 不支持多重继承原因:多重继承的副作用:菱形继承。
假设类 B 和类 C 继承自类 A,且都重写了类 A 中的同一个方法,而类 D 同时继承了类 B 和类 C,此时类 D 继承的类 B、C 的方法,对于那个均继承自 A 的方法,不知道继承哪一个。
但 Java 支持多接口实现,因为接口中的方法是抽象的,就算接口中出现同名方法,实现接口的时候特需要自己来实现,因此不会出现二义性。
继承的意义:解决代码复用;继承是真实世界的客观存在的关系,符合认知。
多态:子类可以替换父类,在实际的代码运行中,调用子类的方法实现。比如
1 class Animal{ 2 public int month = 2; 3 public void eat(){ 4 System.out.println("动物吃东西"); 5 } 6 } 7 8 class Dog extends Animal{ 9 public int month = 3; 10 11 public void eat() { 12 System.out.println("小狗吃肉"); 13 } 14 } 15 16 class Cat extends Animal{ 17 public int month = 4; 18 19 public void eat() { 20 System.out.println("小猫吃鱼"); 21 } 22 } 23 24 public class Test { 25 public static void main(String[] args){ 26 Animal a = new Dog(); 27 Animal b = new Cat(); 28 29 a.eat(); 30 System.out.println(a.month); 31 32 b.eat(); 33 System.out.println(b.month); 34 35 } 36 }
此时eat结果不同,但实例变量 mouth=2 不具备多态性
多态的意义:提高代码的可扩展性和复用性
实例变量:定义在类中但不在任何方法之中的变量;
局部变量:定义在方法之中的变量;
引用变量:a是在栈中定义的引用变量,new Dog()是堆中创建的对象。
3.违反面向对象编程风格的典型代码设计
滥用 getter、setter 方法:为每个属性均定义 getter、setter 方法,不利于类的封装,因为外部可以利用 setter 方法修改私有(private)属性。对于基本类型来说,getter 方法不会修改数据,仅能访问数据,但对于返回类型是集合容器的属性来说,getter 方法是可以在获取这个容器后,操作容器内部数据的。eg: list.clear()。如果想避免获取属性的值又避免外部修改属性,可以通过 Collections.unmodifiableCollection() 方法,返回一个无法被修改的 Collection 容器,若调用修改容器数据的方法,代码会抛出 UnsupportedOperationException 异常。
滥用全局变量和全局方法:
Contants 类是常量类,即将自己常用到的常量定义到一个类中,但这样不利于代码的可维护性,假如一个项目开发人员很多,则向 Contants 类会随着添加常量越来越大,查找某一常量会很费时且会增加提交代码冲突的概率,不仅如此,Contants 类越大,依赖这个类的代码会越多,每次修改 Contants 类都会导致依赖它的类文件重新编译,浪费时间与开发效率,此外,还会影响代码复用性,假如有另外一个项目需要复用本项目的某个类,则需要把整个 Contants 类一并引入,即使包括很多无关常量。
改进措施:1.将 Contants 类拆解为功能更为单一的多个类,如 mysqlContants、RedisContants、等;2.不设计 Contants 类,在每个类的开始定义该类所需要的常量。
Utils 类是用来封装常用的功能逻辑,避免代码重复的静态方法类。弊端与改进措施类比于 Contants 类。
定义数据和方法分离的类:贫血模型是典型的数据和方法分离的类的开发模式。贫血模型:domain/object 仅有属性的 getter/setter 方法的纯数据类,将所有类的行为放到 service 层。流行原因:实现简单、上手快。
4.抽象类和接口
抽象类特性:
1.抽象类不允许被实例化,只能被继承
2.抽象类可以包含属性与方法,方法可以实现也可以不实现(抽象方法)
3.子类继承抽象类必须继承抽象类中的所有抽象方法
接口特性:
1.接口无属性
2.接口只能声明方法,不能实现;在 Java 8 中,接口可以使用关键字 default 来实现一个默认方法,解决了在向接口中新增一个方法时将所有实现类中实现一遍的麻烦。
3.类实现接口必须实现接口中声明的所有方法
抽象类的作用:实现代码复用,虽然可以把抽象类改为普通的父类,抽象方法定义为空,但实现结果不够好,原因如下:阅读该父类的时候需要了解其子类才清晰的了解父类中空方法的设计意图;一个父类代码量很多,子类可能会忘记实现父类空方法;父类会被实例化,增加了类被复用的风险。
接口的作用:解耦,隔离接口和具体的实现,降低代码间的耦合性,提高代码的扩展性。
使用抽象类还是接口:抽象类强调 is-a 关系,接口强调 has-a 关系。
5.基于接口而非实现编程
接口应具有足够的概括性,抽象性,通用性,脱离某一具体实现,才能够应对需求变化而减少对原有代码的重构。
6.多用组合少用继承
1 public interface Flyable { 2 void fly(); 3 } 4 public class FlyAbility implements Flyable { 5 @Override 6 public void fly() { //... } 7 } 8 //省略Tweetable/TweetAbility/EggLayable/EggLayAbility 9 10 public class Ostrich implements Tweetable, EggLayable {//鸵鸟 11 private TweetAbility tweetAbility = new TweetAbility(); //组合 12 private EggLayAbility eggLayAbility = new EggLayAbility(); //组合 13 //... 省略其他属性和方法... 14 @Override 15 public void tweet() { 16 tweetAbility.tweet(); // 委托 17 } 18 @Override 19 public void layEgg() { 20 eggLayAbility.layEgg(); // 委托 21 } 22 }
通过组合和委托解决了代码重复问题。
如果类之间的继承结构比较稳定且继承关系简单,可以使用继承;继承结构越不稳定,关系越复杂,越推荐使用组合。
以上是关于菜菜读设计模式设计模式——设计原则:面向对象的主要内容,如果未能解决你的问题,请参考以下文章
跟着盒子的代码设计示例,一起对面向对象的设计模式之SOLID原则加深理解