设计原则
面向对象是一种编程思维,是我们分析问题设计模型的一种思路。从本质上看,是没有绝对的对与错的。但是,不同的设计方式还有高低之分的。而判断的标准是什么呢?
1、最基本的标准是功能实现;
2、在都能够实现功能的前提下,我们的判断标准就叫做“高内聚、低耦合”。
“内聚度” --- 体现的是每一个基本模块自己本身的功能完整度。高内聚指的是一个模块本身的任务就应该在它内部完成,不要分散到别的地方。
基本模块根据粒度,可以是一个子系统,也可以是一个包,也可以是一个类,还可以是一个方法。
“耦合度” --- 体现的是模块与模块的关联关系。低耦合指的是尽量关联需要使用到的其他模块,无关模块不要关联;关联的时候尽量采用松散易解除或易修改的关联方式。
“高内聚”和“低耦合”仍然是一个范围比较大的概念,对代码设计的具体指导意义不大。所以,软件业又提出了一些设计原则,供我们在具体分析和设计的时候作为指导性原则:
单一职责
模块的职责要单一,不能将太多的职责放在一个模块中。 具体到我们能够理解的范围内:
1、一个方法只应该完成一个功能,方法名就应该是对这个功能的完整描述;
2、一个类只应该描述一种对象;
3、一个包只应该包含跟这个包描述的功能相符的功能类和接口;
开闭原则
开闭原则是其他各种原则的核心,或者说其他的原则都是为了在某一方面达到开闭效果而提出来具体原则。
Software should be opened for Extendtion, but closed for Modification。 软件应该对扩展进行开放,对修改进行关闭。
里氏替换
这是一个专门用来判断该不该做继承的原则。 我们之前判断该不该做继承,我们用的是一种叫做"is-a"(是一个)的判断方式。
可惜,is-a方式是基于我们的生活场景提出来的。但是,在软件系统中,这种经验有可能是错误的。
她提出来判断两个类是否应该做继承的标准是:凡是父类对象在本系统中能够正确工作的位置,都能够替换成子类对象且不引起额外的错误。
依赖倒转
在设计类与类的关联的时候,尽量关联对方的抽象(抽象类与接口),不要直接去关联对象的实现类。 这样做的好处是为了能够达到多态的效果。因为抽象类(接口)的引用可以指向它所有的实现子类对象。那么,我们如果需要更改实现,只需要增加新的子类或新的实现类,而不需要更改关联处的代码。
接口隔离
尽量定义职责单一的小接口,少定义职责过多的大接口。
愿义:上层接口的设计不应该污染下层接口(或实现类)的职责,要做好职责隔离。
比如:上层接口有三个行为,下层子接口或实现类只需要用到其中的两个或1个。但是,通过接口实现的语法,导致下层的接口或实现类也有3个行为,多了他们不需要的,这就是“污染”。那么在这种情况下,我们最好做三个上层接口,各自拥有一个行为,让子接口或实现类根据自己的业务需要可以自主选择。
误区:有人会认为为了不违反接口隔离原则,那每个接口都只定义一个行为。 如果我们判断出某些行为是同时出现或同时不出现的,完全可以把它们写在一个接口当中。
在三层架构当中,我们的表示类关联业务层接口,然后通过配置去产生业务类对象交给这个接口的引用;同理,我们的业务类关联持久层接口,然后通过配置去产生持久类对象交给这个接口引用。这是典型的“上层不要关联下层的具体实现,而应该关联下层的抽象”---依赖倒转。
这个过程,我们后面还要继续通过实践来进一步讲解和领悟。
组合聚合原则
少用继承,多用组合和聚合。
继承的方式是一种硬代码的关联,一旦发生变化,我们必须去修改extends 后面的父类
组合和聚合是支持多态的,然后再配合我们的反射技术,通过配置文件和注解,可以在不改变java代码的情况下,直接绑定子类对象。
所以从动态性,以及开闭原则来看,组合聚合比继承更好。
迪米特法则
也叫“最少知识原则”。 类与类之间的绑定关系,尽量的简洁,只绑定跟你相关的类,无关的别去绑定。 因为你绑定的越多,那么引起这类变化的原因也就越多。
模式
在上面讲解当中所涉及到的设计原则,这些原则非常重要,特别是对于一个设计人员来说。但是编码实现的开发人员来说,往往局限于眼界或经验并不能完全体会到它们的实用性,或者在处理具体问题域的时候不知道该如何来设计或实现自己代码从而满足上面的原则要求。由此,产生了“模式”。
我们在编程的过程中,随着参与的项目越来越多,大家会发现:有时候很多问题并不是某一个项目中独有的,而是反反复复在不同的项目中出现。于是先人们就把这些问题总结出来,起了名字,给出了统一的标准的经过印证的解决“套路”。
在软件工程当中,模式有两大类:
1、设计模式; 更偏向于某个具体的问题域的解决,是代码级别的解决方案;-- 微观
2、架构模式; 更偏向于工程的组织架构的设计,对于一个项目中拥有的众多的类和接口,如何进行分工,如何进行关联;-- 宏观
设计模式
一共有23种,分为三大类:
1、创建模式 -- 主要创建某种特殊的对象,或则在某种特殊要求下创建对象
2、结构模式 -- 主要是利用组合/聚合,或者是继承,让类与类能够形成某种关联结构,从而更适应某个问题域的解决。
3、行为模式 -- 探讨的是在某种问题域当中如何设计一个行为来适应这个场景。
单例模式 --- Singleton
当在一些问题域当中,需要我们去创建一种类,这个类能且只能产生一个对象。 单例模式有几种:
1、饿汉模式;
/**
单例模式 -- 饿汉模式
优点:
线程绝对安全
缺点:
由于对象的产生是写在静态属性的初始位置;
那么只要一加载这个类,不管是否调用到getInstance(),
就算是调用别的静态方法,也会把实例对象产生出来。--- 专业叫法:预加载
我们更多的时候是希望当我们真正需要获取实例对象的时候,
你才给我产生。什么时候是我真正需要?是当我调用getInstance()
的时候。
*/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
2、懒汉模式;
/*
单例模式 --- 懒汉模式
优点:
它是延迟加载的,只有当第一次我们真正调用getInstance(),表明
外部真的需要它的实例对象了,它才会在内部产生对象。
缺点:
如果不加控制,那么在多线程情况下,这个方式是线程不安全的。
要想让这个懒汉模式做到线程安全,那么就要给getInstance()
加synchronized关键字,让它能够做到线程同步。
而做到了线程同步,那么在多线程的情况下,它的效率就会下降。
*/
public class Singleton2 {
private static Singleton2 instance;
private Singleton2(){}
public synchronized static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
3、DCL模式;*
/*
单例模式 --- Double Check Lock 机制
优点:
1、延迟加载
2、线程安全
3、性能提升 -- 只有当instance还没有产生的时候,就有多个线程
同时调用getInstance(),那么只在这种情况下线程会排队;
一旦instance已经产生了,就算有多个线程同时调用
getInstance(),这些线程是不会排队的。
缺点:
JDK5之前,Java程序员只能看,不能用。当时的JVM的内存模型,不支持这种
语法,因为在Java当中new 对象()其实不是一条指令,这些指令不具有原子性,
还是可能在多线程的情况下被被的线程插入。
JDK5之后,我们可以用了,但是需要增加一个关键字,告知JVM,这个对象的产生
需要原子性,是不可插入的。而加了这个关键字,会让JVM多做一些工作,会部分丢
失性能。
*/
public class Singleton3 {
private volatile static Singleton3 instance;
private Singleton3(){}
public static Singleton3 getInstance(){
if(instance == null){
synchronized (Singleton3.class) {
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
4、内部类模式;*
/*
单例模式 --- 静态内部类实现
1、延迟加载
只有调用Singleton4的getInstance()的时候,才会用到
InnerClass,也才会加载InnerClass,在加载的时候才会
产生InnerClass的静态属性instance,也才会产生Singleton4
的实例对象。
2、线程安全
在InnerClass的加载期产生instance对象,运行期所有的线程都
只是获取到这个对象,不存在安全性问题。
3、支持高并发
整个代码中没有使用同步锁,所有线程都可以同时并发运行,不存在
排队。
*/
public class Singleton4 {
private Singleton4(){
}
public static Singleton4 getInstance(){
return InnerClass.instance;
}
private static class InnerClass{
private static Singleton4 instance = new Singleton4();
}
}
5、枚举的方式实现单例;
工厂模式 --- Factory
使用场景是:将对象的使用者和对象的生产者进行分离。
在所有的工厂模式当中,都有三种角色的类,我们把它们称之为"消费者","生产者",“产品”。消费者需要用到产品对象,但是不用自己去构建这个产品对象,而是找生产者要即可; 生产者提供方法,专门返回产品对象,无需关心这个产品对象被拿来做什么。
1、简单工厂模式 这种模式考虑的变化点是,同一个工厂,可以根据不同的情况,产生不同的产品对象。 由于变化点是在产品对象上,所以这个时候我们往往会抽象产品,然后给它提供不同的实现类。
//工厂类 产生不同产品
public static KFCProduct create(String order){
return switch (order){
case "汉堡" -> new KFCHanBao();
case "可乐" -> new KFCCola();
default -> null;
};
}
//产品类 -- 这个类的对象是外部需要使用的
public abstract class KFCProduct {
public abstract void taste();//品尝
}
public class KFCHanBao extends KFCProduct{
@Override
public void taste() {
System.out.println("鸡肉汉堡");
}
}
public class KFCCola extends KFCProduct{
@Override
public void taste() {
System.out.println("百事可乐");
}
}
//消费者类
public class Customer {
public static void main(String[] args) {
KFCProduct p = KFCProductFactory.create("可乐");
p.taste();//百事可乐
}
}
2、工厂方法模式 这种模式同一个产品,不同的工厂生产出来会有差异性。 这个时候的变化点是在工厂上,所以我们往往会抽象工厂。
//工厂的抽象
public abstract class Factory {
public abstract HanBao createHanBao();
public abstract Cola createCola();
}
//工厂的不同实现
public class KFCFactory extends Factory {
@Override
public HanBao createHanBao() {
return new HanBao("鸡肉");
}
@Override
public Cola createCola() {
return new Cola("百事");
}
}
public class MCDFactory extends Factory{
@Override
public HanBao createHanBao() {
return new HanBao("牛肉");
}
@Override
public Cola createCola() {
return new Cola("可口");
}
}
//产品类
public class HanBao {
private String content;
public HanBao(String content){
this.content = content;
}
public void tast(){
System.out.println("这是" + content + "汉堡");
}
}
public class Cola {
private String type;
public Cola(String type) {
this.type = type;
}
public void drink(){
System.out.println("这是" + type + "可乐");
}
}
//消费者
public class Customer {
public static void main(String[] args) {
HanBao hanBao = new MCDFactory().createHanBao();
hanBao.tast();
}
}
3、抽象工厂 工厂和产品都是变化点,所以我们既要抽象出工厂父类,也要抽象出产品父类。
原型模式 --- prototype
场景是:根据一个已经存在的对象,创建一个新的跟它一摸一样的对象。 --- 克隆
利用Object中的clone()
1、首先要让被克隆的类实现Cloneable接口;
2、然后要重写clone方法,提升它的访问修饰符为public;
3、最后的效果,是浅克隆的效果。
所谓的“浅克隆”是指,被clone的对象会产生一个新的,但是它的属性对象不产生。也就是说,clone方法只克隆一层。
要解决这个问题:只能一层层去克隆,需要让我们自己去克隆每一个关联对象,然后再把它们组装起来,才能够完成“深克隆”。很明显,这么做很麻烦。
直接利用对象的序列化和反序列化实现深克隆
1、让所有的自定义类型实现Serializable接口;
2、在最外层的类身上增加一个自定义方法,完成序列化和反序列化就可以了。
public class BasketballStar implements Cloneable, Serializable {
private String name;
private int age;
private double height;
private PlayerData myData;
public BasketballStar(String name, int age, double height) {
this.name = name;
this.age = age;
this.height = height;
}
public BasketballStar(String name, int age, double height, PlayerData myData) {
this.name = name;
this.age = age;
this.height = height;
this.myData = myData;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public PlayerData getMyData() {
return myData;
}
public void setMyData(PlayerData myData) {
this.myData = myData;
}
@Override
public Object clone() throws CloneNotSupportedException {
PlayerData playerData = (PlayerData) this.myData.clone();
BasketballStar basketballStar = (BasketballStar) super.clone();
basketballStar.setMyData(playerData);
return basketballStar;
}
public BasketballStar deepClone(){
BasketballStar newStar = null;
//try-with-Resource
try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);){
oos.writeObject(this);
try(ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis)){
newStar = (BasketballStar) ois.readObject();
}catch(Exception ex1){
ex1.printStackTrace();
}
}catch(Exception ex2){
ex2.printStackTrace();
}
return newStar;
}
}
public class PlayerData implements Cloneable, Serializable {
private int passing;
private int crossing;
private int jumping;
private int shooting;
public PlayerData(int passing, int crossing, int jumping, int shooting) {
this.passing = passing;
this.crossing = crossing;
this.jumping = jumping;
this.shooting = shooting;
}
public int getPassing() {
return passing;
}
public void setPassing(int passing) {
this.passing = passing;
}
public int getCrossing() {
return crossing;
}
public void setCrossing(int crossing) {
this.crossing = crossing;
}
public int getJumping() {
return jumping;
}
public void setJumping(int jumping) {
this.jumping = jumping;
}
public int getShooting() {
return shooting;
}
public void setShooting(int shooting) {
this.shooting = shooting;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static void main(String[] args) {
BasketballStar ym = new BasketballStar("姚明",40,2.26,new PlayerData(70,50,20,90));
BasketballStar xym = null;
// try {
// xym = (BasketballStar) ym.clone();
// } catch (CloneNotSupportedException e) {
// e.printStackTrace();
// }
xym = ym.deepClone();
System.out.println(xym == ym);//false
System.out.println(xym);
xym.getMyData().setJumping(80);
System.out.println(ym.getMyData().getJumping());
}