最近依然在应对面试,有人问到设计模式的题目,之前自己看过,但是都忘记了,现在整理一下。
有时,允许自由创建某个类的实例没有意义,反而可能会导致系统性能下降。例如:数据库引擎访问点、Hibernate的SessionFactory都只需要一个实例即可,此时可以使用单例模式。
如果一个类始终只能创建一个实例,则称这个类为单例类,这种模式为单例模式。
Spring中框架中可以直接在配置时通过制定scope=”singleton”实现单例模式。
Java代码可以自己实现单例模式,如下:
class Singleton{
//类变量缓存曾经创建的实例
private static Singleton instance;
//构造方法私有
private Singleton(){}
//静态方法,返回类的实例
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
思路:把构造方法设为私有,这样在类外就无法调用构造器生成对象的实例。然后在类中声明一个类变量用来缓存生成的实例。这样的话,还有个问题是无法生成这个对象的实例,因此需要写一个方法生成那个单例的对象实例。做法:声明一个静态方法,判断类变量对否为null,如果是null说明没有生成实例,就需要调用私有的构造方法生产一个实例;如果有了的话,就不需要如何操作。最后返回这个实例。
开发过程中经常遇到A实例需要调用B实例。这个时候大多数做法是直接new一个B出来。这样的一个缺点就是代码耦合了,因为A中调用了B类的类名(硬编码耦合)。后果就是如果我们后来重构了,需要用C来替换B,这样的后果就是每个B都需要修改为C。工作量很大。
解决方法:因为我们只是调用了B中的方法,不需要关心B的创建、实现过程,因此可以使用一个接口:IB,让B实现接口IB。这样A依赖的就不是具体的类了,而是具体的接口。然后创建一个工程类:IBFactory,该工厂负责产生IB的实例。A只需要调用IBFactory里面的方法就可以拿到IB的实例了。
将来如果出现重构:C代替B的情况,只需要让C也实现IB接口,然后修改IBFactory里面代码,让工厂产生C实例就可以了。
代码:
IB接口。
public interface IB {
public void show();
}
B类
public class B implements IB {
@Override
public void show() {
System.out.println("this is B!!");
}
}
C类
public class C implements IB {
@Override
public void show() {
System.out.println("this is C!!");
}
}
IBFactory。工厂类,返回实例。
public class IBFactory {
public IB getIB() {
//如果是return new B();则返回B的实例
//重构只需要修改这里。
return new C();
}
}
A类。
public class A {
private IB ib;
public A(IB ib){
this.ib = ib;
}
public void show(){
ib.show();
}
public static void main(String[] args) {
IBFactory ibFactory = new IBFactory();
A a = new A(ibFactory.getIB());
a.show();
}
}
(1)工厂方法:
在上面2中,系统通过工厂方法生产了对象的实例,在工厂类中决定生产那个对象的实例。但是这样也不太完美。修改生产的产品是,需要去工厂里面直接修改代码,这样不太好。希望在调用工厂的时候(即在A中),直接可以判断需要生产什么产品。换言之,重构时,只需要修改A中代码就可以了。
解决办法,可以设计一个工厂的接口,程序为不同的产品设计不同的工厂。
代码:
Factory类。生产不同工厂的工厂类。
public interface Factory {
IB getIB();
}
BFactory.java。生产B产品的工厂。
public class BFactory implements Factory {
@Override
public IB getIB() {
return new B();
}
}
CFactory.java。生产C产品的工厂。
public class CFactory implements Factory {
@Override
public IB getIB() {
return new C();
}
}
IB.java,B.java,C.java与前面简单工厂类一样,不在赘述。
A.java代码。
public class A {
private IB ib;
public A(IB ib){
this.ib = ib;
}
public void show(){
ib.show();
}
public static void main(String[] args) {
//使用CFactory子类创建Factory
Factory factory = new CFactory();
A a = new A(factory.getIB());
a.show();
}
}
(2)抽象工厂
对于上面的工厂方法,依然存在一种耦合:客户端代码与不同的工厂类耦合。解决这个办法可以再增加一个工厂类,这个类来制造不同的工厂:
代码:
添加类FactoryFactory.java
public class FactoryFactory {
public static Factory getFactory(String type){
if(type.equalsIgnoreCase("b"))
return new BFactory();
else
return new CFactory();
}
}
A.java修改代码:
public class A {
private IB ib;
public A(IB ib){
this.ib = ib;
}
public void show(){
ib.show();
}
public static void main(String[] args) {
//通过传参来确定生产的工厂
Factory factory = FactoryFactory.getFactory("B");
A a = new A(factory.getIB());
a.show();
}
}
“抽象工厂”与“简单工厂”区别:简单工厂直接生产对被调用对象;抽象工厂生产工厂对象。
Spring框架的IoC容器可以认为是抽象工厂。可以管理Bean实例,也可以管理工厂实例。
当客户端代码需要调用某个对象的时候,客户端实际上不关心是否准确得到了这个对象,只要一个能提供该功能的对象即可,此时可以返回对象的代理。
这种设计模式下,系统会为某个对象生成一个代理,由这个代理控制源对象的引用。这种情况下,客户端代码仅仅持有一个被代理对象的接口,变为面向接口编程。
其实这里可以看看动态代理、静态代理的知识。
代码:
BigImage.java。模拟大的图片。
public class BigImage implements Image {
public BigImage(){
try {
//暂停3秒,模拟系统开销
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void show() {
System.out.println("image show...");
}
}
ProxyImage.java,图片的代理类。
public class ProxyImage implements Image {
private Image image;
public ProxyImage(Image image){
this.image = image;
}
@Override
public void show() {
if(image == null){
image = new BigImage();
}
image.show();
}
}
测试类。
public class ProxyTest {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
Image image = new ProxyImage(null);
System.out.println("加载图片耗时:"+(System.currentTimeMillis()-startTime));
//调用show方法时,才会真正执行加载
long startTime2 = System.currentTimeMillis();
image.show();
System.out.println("加载图片真正耗时:"+(System.currentTimeMillis()-startTime2));
}
}
Hibernate框架使用了代理模式,比如它的延迟加载。加入A、B实体存在关联关系,在加载A实体时,B应该被加载。采取延迟加载,系统会生产一个B的代理,只有A真正需要访问B中变量的时候,才会真正加载B实体。从而节约开销。
很多时候,我们需要一个方法完成一个功能,而且,这个功能大多数步骤我们已知并确定,但是有少量具体的步骤,需要在执行这个方法的时候,才能确定。这个时候可以使用命令模式。比如:我们有一个方法遍历数组的每一个元素,但是对元素的具体操作(迭代输出、累加)需要在调用该方法的时候才能确定,则需要在调用该方法时指定具体的处理行为。
代码:
ProcessArray.java,里面有一个each方法,用于处理数组。具体操作不知道,所以需要传入一个Command参数。
public class ProcessArray {
public void each(int[] target, Command cmd) {
cmd.process(target);
}
}
Command.java,里面的process方法定义了对于数组的处理行为。这是一个接口
public interface Command {
void process(int[] target);
}
CommandTest.java。测试命令方法。
public class CommandTest {
public static void main(String[] args) {
ProcessArray processArray = new ProcessArray();
int[] target = {5, 2, 8, 4};
processArray.each(target, new Command() {
@Override
public void process(int[] target) {
for (int temp : target) {
System.out.println("迭代输出:" + temp);
}
}
});
}
}
如果需要对数组进行其他的操作,只需要修改传入的匿名类的实例。不同的Command实例封装了不同的操作。
如果现在有这样一个需求,有一件商品,不同的用户(普通会员、VIP)所享受到的折扣不一样。需要针对不同的打折需求计算不同的价格。以往的做法就是switch()语句解决。但是一个弊端就是,如果某天我们增加了一个新的打折情况,则需要在switch中添加新的case,并添加新的语句,然后针对新的打折。现在使用策略模式,我们只需要新建一个类,实现统一的接口,描述这种打折的具体操作就可以了,不需要修改之前的代码。
代码:
首先我们写打折接口。DiscountStrategy.java。
public interface DiscountStrategy {
double getDiscount(double originPrice);
}
其次两个打折具体类,实现这个接口。
普通用户打折。CommonDiscount .java。
public class CommonDiscount implements DiscountStrategy {
@Override
public double getDiscount(double originPrice) {
System.out.println("普通用户打折...");
return originPrice * 0.7;
}
}
VIP用户的打折。VIPDiscount .java。
public class VIPDiscount implements DiscountStrategy {
@Override
public double getDiscount(double originPrice) {
System.out.println("VIP 打五折...");
return originPrice * 0.5;
}
}
选择策略算法、返回价格的类。DiscountContext.java。
public class DiscountContext {
private DiscountStrategy strategy;
public DiscountContext(DiscountStrategy strategy){
this.strategy = strategy;
}
public double getDiscount(double price){
return strategy.getDiscount(price);
}
//改变策略
public void changeStrategy(DiscountStrategy strategy){
this.strategy = strategy;
}
}
测试
public class Test {
public static void main(String[] args) {
//null代表普通用户
DiscountContext discountContext = new DiscountContext(new CommonDiscount());
double price = 998.99;
System.out.println("普通用户价格:" + discountContext.getDiscount(price));
discountContext.changeStrategy(new VIPDiscount());
System.out.println("VIP用户价格:" + discountContext.getDiscount(price));
}
}
之后的代码修改中,如果新增加一种打折方式,只需要写一个实现了打折接口的类,然后在测试类中添加
discountContext.changeStrategy(new /*打折类*/);
就可以了。
Hibernate中数据库方言Dialect就是使用的这种模式。
很多情况下,我们进行一个操作的步骤是固定的。比如餐馆点餐吃饭,首先会有点餐,其次厨师做饭,最后服务员上菜…这些步骤都是一定的了。但是每次这样一个过程都需要调用三个类(订餐、做饭、上菜),比较麻烦,我们可以写一个类(门面),类中一次调用这三个类。在以后顾客点餐的时候,直接调用这个门面。
代码:
PayMent.java。订餐。
public class Payment {
public String pay(String food) {
System.out.println("点餐:" + food + "一份!");
return food;
}
}
Cook.java。厨师类。
public class Cook {
public String cook(String food) {
System.out.println("厨师正在烹调:" + food);
return food;
}
}
Serve.java。服务类。
public class Serve {
public void serve(String food) {
System.out.println("服务员上菜:" + food);
}
}
Facade.java。门面类。
public class Facade {
Payment payment;
Cook cook;
Serve serve;
public Facade() {
this.payment = new Payment();
this.cook = new Cook();
this.serve = new Serve();
}
public void serveFood(String food){
payment.pay(food);
cook.cook(food);
serve.serve(food);
}
}
测试
public class Test {
public static void main(String[] args) {
Facade facade = new Facade();
facade.serveFood("牛肉面");
}
}
我们在门面里面封装了要执行的步骤,在程序中可以直接调用门面完成一系列操作。
Hibernate框架中,hibernateTemplate就是用了这种模式。例如save操作,封装了SessionFactory、session等门面。
开发中,遇到两个维度的结构模式,仅仅使用继承无法实现。比如面条,可能有材料维度(牛肉、羊肉、猪肉),也有口味维度(清淡、微辣)等等。这个时候可以这样解决。
设置一个接口,专门记录口味的。不同的口味,对应一个实现类。
设置一个抽象类,该类有一个变量记录面条的口味。然后对于具体的材料(猪肉、牛肉等)继承这个抽象类。
这样对于添加口味维度或者材料维度,都只需要添加一个类就可以。
代码:
口味维度的两个类。
public class PepperyStyle implements Peppery {
@Override
public String style() {
return "辣口味";
}
}
public class PlainStyle implements Peppery {
@Override
public String style() {
return "清淡口味";
}
}
面条抽象类。
public abstract class AbstractNoodle {
Peppery style;
public AbstractNoodle(Peppery style){
this.style = style;
}
public abstract void eat();
}
牛肉面以及猪肉面类。
public class BeefNoodle extends AbstractNoodle {
public BeefNoodle(Peppery style) {
super(style);
}
@Override
public void eat() {
System.out.println("牛肉面,口味:" + super.style.style());
}
}
public class ProkyNoodle extends AbstractNoodle {
public ProkyNoodle(Peppery style) {
super(style);
}
@Override
public void eat() {
System.out.println("猪肉面,口味:" + super.style.style());
}
}
测试类。
public class Test {
public static void main(String[] args) {
AbstractNoodle noodle1 = new BeefNoodle(new PepperyStyle());
noodle1.eat();
AbstractNoodle noodle2 = new ProkyNoodle(new PlainStyle());
noodle2.eat();
}
}
比较常见的一种模式。
定义了对象间一对多的依赖关系:一个或者多个观察者对象观察一个主题对象。当主题对象发生变化时,系统通知所有的观察此对象的观察者。
一般包括四个角色:
被观察者的基类对象:持有多个观察者的引用。
观察者接口:所有观察者必须实现的一个接口。
被观察者实现类:继承被观察者的基类对象。
观察者实现类:实现观察者接口。