抽象工厂
在上一篇中,我们讲到了工厂方法,其中,父类(或者接口)定义创建对象的公共的接口,而具体创建什么样的对象是在它的子类(若是接口,则应该是实施者)中实现的。这大大方便了我们进行业务拓展,生产哪种产品,就加入哪种产品的工厂。但是,当我们加入的业务过多时,工厂的数量也变的多起来了,这时,我们可以引入抽象工厂解决这一问题。
即在工厂方法模式中,客户端每次都只能创建一个同类型的对象,当有多个抽角色时,客户端只需要维护包含多个抽象角色的接口就好了(即抽象工厂),因为根据里氏代换原则,父类出现的地方,子类应该可以。换句话来说,客户不需要关注太多具体的产品。
抽象工厂(AbstractFactory):客户端直接引用,由未实现的工厂方法组成,子类必须实现其工厂方法创建产品家族。
具体工厂(ConcreteFactory):实现抽象工厂接口,负责实现工厂方法,一个具体工厂可以创建一组产品。
抽象产品(AbstractProduct):产品家族的父类,由此可以衍生很多子产品。
具体产品(Product):衍生自抽象产品,由工厂方法直接创建。
这个图想比大家都很熟悉了,建议初学时,自己画一个工厂就好,这样就不会太乱。图中是有两个具体工厂的即ConcreteFact ory1和ConcreteFactory2。然后,具体工厂又可以产生具体产品A和B的实例,而具体产品A和B又分别是抽象产品A和B的实现。这样,如果客户端在调用时,只需以来抽象工厂就可以直接创建A和B了。
//代码(后续补上)
构造者
构造者是来解决初始化的对象较为复杂时的情形的。如下:
public class BuilderAddress {
String address;
String name;
String tel;
public BuilderAddress(String address, String name){
//...
}
public BuilderAddress(String tel, String name){
//...
}
}
如上图所示,我们需要这两的两个构造函数进行对象初始化工作,但此时,编译器即将报错,因为参数类型、个数是完全一样的,不能视为重载。而在构造者模式中,将对象的初始化工作移至到一个所谓的“Bulider”类中。
package builder;
public class BuilderAddress {
String address;
String name;
String tel;
//申明为私有的,这样外部不能直接new创建
private BuilderAddress(){
}
//设置内部类,将BuilderAddress的初始化工作移至到Bulider内部类中
public static class Bulider{
private String address;
private String name;
private String tel;
public Bulider(){
}
//为各个属性字段设置值,方便将它们赋值到外部类的各个相同属性中
public Bulider setAddress(String address) {
this.address = address;
return this;
}
public Bulider setName(String name) {
this.name = name;
return this;
}
public Bulider setTel(String tel) {
this.tel = tel;
return this;
}
//这个方法就是核心了,初始化BuilderAddress对象后并返回给调用者
public BuilderAddress bulider(){
BuilderAddress addr = new BuilderAddress();
addr.address = address;
addr.name = name;
addr.tel = tel;
return addr;
}
}
调用方式
public static void main(String[] args) {
//根据实际需要,设置相应的值就可以完成初始化工作了
BuilderAddress test = new BuilderAddress.Bulider().setAddress("XXXX").bulider();
}
原型
“工厂方法”和“抽象工厂”模式都允许客户端不必承担创建具体类的职责。“原型”模式提供了另一种灵活的方式以获得相同的结果。原型”模式的使用场景如下:(1)当客户端需要创建对象集时,其中对象是相似的,或仅在某些状态有所不同;并且创建此类对象代价较大,所涉及的处理也过多。(2)作为建立大量工厂的替代方法,其中工厂反映要实例化的类*
在上述情况下,“原型”模式建议:先创建一个对象,然后将其指定为原型对象。通过复制原型对象并进行状态修改,以创建其他对象。
默认情况下,所有Java对象都会继承顶层java.lang.Object类的内置clone()方法。内置clone()方法会将原对象的复制项创建为浅复制项。此时,复制完后的对象和原来对象在内存中的位置是一样的,只是引用不一样,这个地方也是很多人经常容易忘记的。此时,如果你修改了复制后的对象里的内容,那么原来对象也将跟着改变。
如下:
package top.fang.pratice;
/**
* 构造一个教师类,只有一个职称的属性
*/
public class TestTeacherClone {
public String profession;
public TestTeacherClone(){
}
public TestTeacherClone(String profession){
this.profession = profession;
}
}
package top.fang.pratice;
/**
* 构造一个学生类,实现了Cloneable接口,有名字name和年龄age属性,并内置一个教师类对象teacher
*/
public class TestStudentClone implements Cloneable{
private String name;
private String age;
TestTeacherClone teacher;
public TestStudentClone(String name, String age, TestTeacherClone teacher){
this.name = name;
this.age = age;
this.teacher = teacher;
}
/**
* 重写了Object中的clone方法,进行对象的复制
* @return
*/
public Object clone(){
TestStudentClone stu = null;
try {
stu = (TestStudentClone) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
return stu;
}
public static void main(String[] args) {
// 新建一个教师类对象
TestTeacherClone teacher1 = new TestTeacherClone("教授");
// 构造第一个学生对象,学生一stu1的老师职称是教授
TestStudentClone stu1 = new TestStudentClone("fang","50", teacher1);
// 将stu1对象拷贝给stu2,此时,进行的是浅复制,为啥?往下看
TestStudentClone stu2 = (TestStudentClone) stu1.clone();
// 修改了学生2,stu2的姓名,年龄,和他老师的职称
stu2.name = "liu";
stu2.age = "78";
stu2.teacher.profession = "讲师";
//此时打印出学生一stu1的姓名,年龄,职称
System.out.printf(" stu1的姓名:%s%n stu1的年龄:%s%n 老师职称:%s%n",
stu1.name, stu1.age, stu1.teacher.profession);
}
}
//打印结果
stu1的姓名:fang
stu1的年龄:50
老师职称:讲师
很明显,直接调用object中的clone,虽然实现了复制,但是如果像TestStudent类那样,包含教师类的引用,这个引用经过复制后,它的内存地址是一样的,所以造成了上面的情况,修改了学生二stu2的姓名,年龄,和老师的职称后,对于学生一stu1而言,姓名,年龄不影响,但老师职称变了。正确方法如下:
在TestTeacherClone类中,添加如下代码,记得实现Cloneable接口,不然会报错CloneNotSupported:
修改TestStudentClone类代码:
重新打印结果
**浅复制对应的UML图如下:原顶层对象与所有Primitive成员(可理解为上述代码中的基本变量)都被复制。顶层对象包含的任何Reference(即引用)对象都不会被复制,仅复制这些对象的引用。这将使原对象和复制对象引用底层对象的同一个复制项。
**
深复制对应的UML图:我们可以看到,在底层中,内存为新复制的对象重新分配了一块地址。