参考资料
设计模式是解决问题的方案,学习现有的设计模式可以做到经验复用。
https://blog.csdn.net/love905661433/article/details/84482912
http://c.biancheng.net/view/1319.html
很多实例,不错!
https://blog.csdn.net/doymm2008/article/details/13288067
这篇文章总结思考的点比较契合我的口味。
设计原则(单接依里开迪合)
Open And Close:内部关闭是防止破坏原有代码的体系结构,使代码更加安全。
Liskov Substitution Principle:子类可以扩展父类的功能,但不能改变父类原有的功能。----尽量不要重写。试着以“几维鸟不是鸟”为例来说明里氏替换原则
Dependence Inversion Principle:要面向接口编程,不要面向实现编程。“顾客购物程序”
Single Responsibility Principle:最容易理解也最难应用,“大学学生工作管理程序”。
Interface Segregation Principle:如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。“学生成绩管理程序”
Law of Demeter:“明星与经纪人”,第三方,弱耦合,第三方本身固化。
创建型(5种)
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”
一个值得思考的问题:创建对象的方式有几种,分别是哪些?
单例模式
参考资料:
https://baijiahao.baidu.com/s?id=1631611708486223320&wfr=spider&for=pc
https://github.com/CyC2018/CS-Notes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#1-%E5%8D%95%E4%BE%8Bsingleton
效果:多次new对象返回的是同一个对象的引用
好处:节省内存,信息交流。-----当然有时候每个对象都有自己独特的信息就别乱用了。
应用:任务管理器;工具类;线程池…
懒汉式:在main()中主动new时才创建对象。
饿汉式:类加载直接创建对象。
欢迎大家访问麦田里的码农
懒汉式
public class LazySingleton {
private static LazySingleton lazy;//单例不开放
private LazySingleton(){//单例不开放
}
public static LazySingleton getLazy() {
if(lazy==null){//没有就创建
return lazy=new LazySingleton();
}
return lazy;//有的话就单例
}
}
坏处:线程不安全,可能创建多实例
饿汉式
public class HungrySingleton {
private static HungrySingleton hungry=new HungrySingleton();//直接创建
private HungrySingleton(){
}
public static HungrySingleton getHungry(){
return hungry;
}
}
好处:线程安全
坏处:占用内存空间(从某种角度上也是好处其实,看应用场景!)
多线程创建懒汉单例的双检索机制
public class DoubleCheck {
//volatile保持可见性,不然刚开始创建对象释放锁后有的
//线程可能还以为会空呢!
private static volatile DoubleCheck doubleCheck;
//volatile实现了轻度锁----其实它没有加锁的,通常采取的是CAS策略----我比较欣赏这种风格.
//由于sychronized这一重度锁的出现,volatile就没啥用了----这是先前错误的认知。不加volatile还是会有点小问题的,详见上面的CyC2018大神的博客,禁止指令重排序影响还是很大的,用灵活和速度来换安全!
//要是不采取双检锁,volatile可以基本解决多线程突发的状况了。
private DoubleCheck() {
}
public static DoubleCheck getInstance() {
if(doubleCheck==null){//多线程检测到无实例时都试图创建一个实例
synchronized (DoubleCheck.class){//锁住一个一个依次执行创建
//要不是第一个执行执行创建的线程,那就跳出去;
//要是第一个执行执行创建的线程,那就创建呀!
if(doubleCheck==null){
return doubleCheck=new DoubleCheck();
}
}
}
return doubleCheck;
}
}
好处:线程安全。不会优先占用内存空间(在某些应用场景下也会稍微慢一点,在第一次创建对象时)
坏处:在某些应用场景下也会稍微慢一点,具体说就是在第一次创建对象时。
思考:
只有volatile可以吗?不行,可见性虽然很多时候解决了一些麻烦,但还是不OK,比如有两个线程都度过了判空这个过程,不用双检锁就会创建两个对象的。
只用双检锁可不可以?不行,这个思考起来还是难度要大一点。比如线程A执行创建时,分为3步:
(1)开辟内存(2)初始化(3)返回引用-----不使用volatile禁止指令重排序可能会导致指令按132的顺序运行。线程A执行完13以后,会释放锁。线程B获得锁以后发现doubleCheck已经有引用了,会直接返回这个还没有执行第(2)步的doubleCheck,用的数据就不对了啊!
当然以上思考是建立在第(1)(3)步执行完就能释放锁的前提下,实际上这和我之前的认知有点相背离。我也不知道咋验证,真尴尬!
静态内部类实现饿汉单例----没觉得多好
public class HungrySingleton2 {
private static class HungrySingletonHolder{
//静态内部类-----我是没懂使用静态内部类的原因,不如饿汉吧,不过也的确是一种实现方式!
//新想法:使用静态内部类可以实现延迟初始化,适合用于某些场景。
private static final HungrySingleton2 hungry=new HungrySingleton2();
//注意这个final,关键在于它被初始化以后就不再可被更改-----防止多线程。
//饿汉不是延迟初始化而是立即初始化,所以没啥关系。其实只是立即初始化也不能保证线程安全,类加载机制JVM层次上的才是保证线程初始化的根本原因!
}
private HungrySingleton2(){}
public static final HungrySingleton2 getHungry(){
return HungrySingletonHolder.hungry;
}
}
发现盲区了,我原来以为只是一个饿汉,没想到是一个懒汉!!!
那这样的话还OK,实现起来效果和双重检验锁差不多,同样也可以延迟初始化。
枚举,这个厉害!
public enum SingletonEnum {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 单例测试
SingletonEnum firstSingleton = SingletonEnum.INSTANCE;//获取一个枚举单例叫firstSingleton
firstSingleton.setObjName("firstName");//设置单例属性
System.out.println(firstSingleton.getObjName());//打印firstSingleton的objName属性
SingletonEnum secondSingleton = SingletonEnum.INSTANCE;//再获取一个枚举单例叫secondSingleton
secondSingleton.setObjName("secondName");//设置单例属性
System.out.println(firstSingleton.getObjName());//打印firstSingleton的objName属性
System.out.println(secondSingleton.getObjName());//打印secondSingleton的objName属性
// 反射获取实例测试
try {
SingletonEnum[] enumConstants = SingletonEnum.class.getEnumConstants();//反射获取类中定义的所有枚举常量
for (SingletonEnum enumConstant : enumConstants) {//遍历之
System.out.println(enumConstant.getObjName());//打印枚举常量的objName属性
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
firstName
secondName
secondName
secondName
好处:该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止多次实例化的代码。该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。
该实现在多次序列化和反序列化之后,不会得到多个实例。而其它实现需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。
原型模式
copy总比原创更简单
效果:不是重新生成而是复制一个相同或相似的对象
结构:原型模式包含以下主要角色。
(1)抽象原型类:规定了具体原型对象必须实现的接口。
(2)具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
(3)访问类:使用具体原型类中的 clone() 方法来复制新的对象。
拓展:深克隆、浅克隆&&序列化
clone实现prototype
//具体原型类
class Realizetype implements Cloneable//这里的 Cloneable 接口就是抽象原型类。
{
Realizetype()
{
System.out.println("具体原型创建成功!");
}
public Object clone() throws CloneNotSupportedException
{
System.out.println("具体原型复制成功!");
return (Realizetype)super.clone();
}
}
//测试类(或者说是访问类)
public class PrototypeTest
{
public static void main(String[] args)throws CloneNotSupportedException
{
Realizetype obj1=new Realizetype();//具体原型类
Realizetype obj2=(Realizetype)obj1.clone();//克隆
System.out.println("obj1==obj2?"+(obj1==obj2));//判断是否同一
}
}
序列化实现prototype
https://blog.csdn.net/qq_37805900/article/details/96104360
这篇文章用代码简明地描述了序列化和反序列化的过程。
https://blog.csdn.net/ai_bao_zi/article/details/81355823
对ByteArrayOutputStream的介绍
具体原型类(实现接口Serializable)
import java.io.Serializable;
/**
* 序列化就是将对象以字节流的形式输出保存到本地或网络
* 反序列化就是读取
* 只有实现了Serializable接口的类才可以序列化
*/
public class Student implements Serializable {
// private static final long serialVersionUID = 8735455341L;
private int id;
private String name;
private String sex;
transient private String school;
private Student friend;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", school='" + school + '\'' +
", friend=" + friend +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
public Student getFriend() {
return friend;
}
public void setFriend(Student friend) {
this.friend = friend;
}
public Student() {
}
public Student(int id, String name, String sex, String school, Student friend) {
this.id = id;
this.name = name;
this.sex = sex;
this.school = school;
this.friend = friend;
}
}
测试类,注意我这里偷懒没有写抽象原型类了,其实可以把 writeObj()和readObj()方法合并在一起形成writeAndRead(Object obj)来构成一个对传入obj引用对象的深复制(也就是抽象原型类)。然后在测试类里调用才行
public class SerializableDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
writeObj();
Student s1=(Student) readObj();
Student s2=(Student) readObj();
Student s3=(Student) readObj();
System.out.println("s1.getFriend().hashCode()="+s1.getFriend().hashCode());
System.out.println("s2.getFriend().hashCode()="+s2.getFriend().hashCode());
System.out.println("s3.getFriend().hashCode()="+s3.getFriend().hashCode());
}
/**
* 将JVM对象数据以对象形式转换成文件输出到硬盘来持久化
*/
public static void writeObj() throws IOException {
Student s= new Student(2015210890,"沈澍","男","nefu",null);
Student s2= new Student(2010000000,"张三","男","nefu",null);
s.setFriend(s2);
// s2.setFriend(s);这句一定要注释掉,不然会发生循环引用,导致栈溢出!!!
//new FileOutputStream(String destination)表明了要输出的目的地,没有的话会创建一个文件
//再用ObjectOutputStream包装的作用是:
// An ObjectOutputStream writes primitive data types and graphs of Java objects to an OutputStream.
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));
oos.writeObject(s);//writeObject(Object obj)表明了jvm要序列化/持久化的对象
oos.close();
}
/**
* JVM读取文件数据并将数据转换成JVM对象
*/
public static Object readObj() throws IOException, ClassNotFoundException {
ObjectInputStream ois;
ois=new ObjectInputStream(new FileInputStream("obj.txt"));
Object object=ois.readObject();//可能会出现ClassNotFoundException
Student student=(Student)object;
System.out.println("反序列化result:"+student);
return student;
}
}
思考改进优化的可能性
引入原型管理器,这个类的核心是Map<String,ProtoType>,本质上其实就是一个工厂模式,避免了Client自己使用原型同时还有创建原型(有可能只是创建一个类似的对象,所以要修改一些属性,使用工厂实现轻耦合是一个好办法!)。再者你可以想想每创建一个原型都要分配一个原型id,这也算是广义上的属性了,所以用工厂模式也就是原型管理器还是一个很棒的方法。严格来讲,它采用的是简单工厂设计模式,不过考虑到专为一类原型设计(包括创建put、查询get、销毁remove)的情况,不需要扩展时,自然不会违背OCP,那么使用它是完全OK的!
工厂模式(2种,简单工厂不算)
simpleFactory
https://mp.weixin.qq.com/s?src=11×tamp=1563195426&ver=1730&signature=lNkrKLA8G2B3LhzB30wawqIOLcKMkLImIHa1rGtg34cfGQF0PwKzLOSrk0G1XfIxrWQQt4MU3NADH0cYUhg2fMNUwjW5ccuqGxajVf21IQagSkxNEjzCkQmoWRwLWDoC&new=1
这篇是对简单工厂的理解,结合了Spring IoC特性,吹爆!简单工厂违背了开闭原则,所以需要通过使用配置文件+反射的方式来避免(微信链接失效概率贼高,已经保存图片,上面链接已失效)。
https://blog.csdn.net/jason0539/article/details/23020989
https://blog.csdn.net/jason0539/article/details/44976775
这篇文章写的也很棒!
https://blog.csdn.net/Fly_as_tadpole/article/details/88326807
这篇文章指出了三大工厂的优缺点。
简单工厂的优点是好看,https://www.cnblogs.com/vigorz/p/10501955.html
工厂类是整个模式的关键。
包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。
通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,将create(这里里值的是initial,比如不只是new,还要set一堆属性,真烦呀!)和use分离出来,有利于整个软件体系结构的优化----------------轻耦合。
缺点是由于工厂类集中了所有实例的创建逻辑(一堆if-else if-else if …-else),这违反了高内聚责任分配原则(违背OCP),将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了,这违反了开闭原则。
思考
Q1:问题是spring如何避免在使用simplefactory时避免违反OCP的(所以它才被踢出GOF)?
A:属性文件+反射。
Q2:simpleFactory的体系结构?
A:简单产品、抽象产品、具体工厂。
Q3:简单工厂到普通工厂到抽象工厂发展的历史脉络?
A:参见上面的博文。
普通工厂
向上转型
智能分配产品和工厂?
模式的结构
工厂方法模式的主要角色如下。
抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
package FactoryMethod;
public class AbstractFactoryTest
{
public static void main(String[] args)
{
try
{
Product a;
AbstractFactory af;
af=(AbstractFactory) ReadXML1.getObject();
a=af.newProduct();
a.show();
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
}
//抽象产品:提供了产品的接口
interface Product
{
public void show();
}
//具体产品1:实现抽象产品中的抽象方法
class ConcreteProduct1 implements Product
{
public void show()
{
System.out.println("具体产品1显示...");
}
}
//具体产品2:实现抽象产品中的抽象方法
class ConcreteProduct2 implements Product
{
public void show()
{
System.out.println("具体产品2显示...");
}
}
//抽象工厂:提供了厂品的生成方法
interface AbstractFactory
{
public Product newProduct();
}
//具体工厂1:实现了厂品的生成方法
class ConcreteFactory1 implements AbstractFactory
{
public Product newProduct()
{
System.out.println("具体工厂1生成-->具体产品1...");
return new ConcreteProduct1();
}
}
//具体工厂2:实现了厂品的生成方法
class ConcreteFactory2 implements AbstractFactory
{
public Product newProduct()
{
System.out.println("具体工厂2生成-->具体产品2...");
return new ConcreteProduct2();
}
}
优点是包含逻辑判断,实现轻耦合(将创建初始化和使用分离),遵循OCP(这是通过创建一个抽象工厂类来实现面向接口编程以提高稳定性来完成的。)
缺点是每新增一个产品也要新增一个具体工厂,所以有的时候类太多也是一件很麻烦的事情。
抽象工厂
引例:
小米、阿里、腾讯等公司现在都算复合型公司了,承办不同的业务。他们就是抽象工厂下的3个具体工厂。不同的业务比如说衣食住行就是四个抽象产品,食这个抽象产品下头又有饿了么、美团、蜂鸟等具体产品。
应用:
抽象工厂模式最早的应用是用于创建属于不同操作系统的视窗构件。(具体的操作系统----具体工厂;各种具体构件----具体产品)
小结
无论是简单工厂模式,工厂方法模式,还是抽象工厂模式,他们都属于工厂模式,在形式和特点上也是极为相似的,他们的最终目的都是为了解耦。在使用时,我们不必去在意这个模式到底工厂方法模式还是抽象工厂模式,因为他们之间的演变常常是令人琢磨不透的。经常你会发现,明明使用的工厂方法模式,当新需求来临,稍加修改,加入了一个新方法后,由于类中的产品构成了不同等级结构中的产品族,它就变成抽象工厂模式了;而对于抽象工厂模式,当减少一个方法使的提供的产品不再构成产品族之后,它就演变成了工厂方法模式。
所以,在使用工厂模式时,只需要关心降低耦合度的目的是否达到了。