1、OOP 有什么不足之处?用什么思想设计可以改善不足?
当我们需要给已有业务代码增强某方面的能力的时候,会导致出现很多的重复代码;因为 OOP 是面向对象编程的,如果每一个对象都需要增加同一个功能,那么都需要写一遍重复的功能代码。
1.1 例子
下面学生会去上课,吃饭和睡觉三个日常生活点,但是现在老师非常关心学生是否准时去做上面的事情,那么想在学生做这三件事情打印一下日志。
增强前:
@Data
@Accessors(chain = true)
public class Student implements Serializable {
private static final long serialVersionUID = 2392021462238048699L;
private String name;
}
public interface StudentService {
void goToClass(Student student);
void eat(Student student);
void sleep(Student student);
}
public class StudentServiceImpl implements StudentService {
@Override
public void goToClass(Student student) {
System.out.println(student.getName()+" 去上课了");
}
@Override
public void eat(Student student) {
System.out.println(student.getName()+" 去吃饭了");
}
@Override
public void sleep(Student student) {
System.out.println(student.getName()+" 去睡觉了");
}
}
增强后:
public class StudentServiceEnhanceImpl implements StudentService {
@Override
public void goToClass(Student student) {
System.out.println("当前时间:"+new Date()+","+student.getName()+"去上课了");
System.out.println(student.getName()+" 去上课了");
}
@Override
public void eat(Student student) {
System.out.println("当前时间:"+new Date()+","+student.getName()+"去吃饭了");
System.out.println(student.getName()+" 去吃饭了");
}
@Override
public void sleep(Student student) {
System.out.println("当前时间:"+new Date()+","+student.getName()+"去睡觉了");
System.out.println(student.getName()+" 去睡觉了");
}
public static void main(String[] args) {
Student student = new Student();
student.setName("winfun");
StudentService studentService = new StudentServiceEnhanceImpl();
studentService.goToClass(student);
studentService.eat(student);
studentService.sleep(student);
}
}
利用工具类优化:
@AllArgsConstructor
public class StudentServiceEnhanceImplV2 implements StudentService {
@Override
public void goToClass(Student student) {
LogUtils.print(student.getName(),"去上课了");
System.out.println(student.getName()+" 去上课了");
}
@Override
public void eat(Student student) {
LogUtils.print(student.getName(),"去吃饭了");
System.out.println(student.getName()+" 去吃饭了");
}
@Override
public void sleep(Student student) {
LogUtils.print(student.getName(),"去睡觉了");
System.out.println(student.getName()+" 去睡觉了");
}
public static void main(String[] args) {
Student student = new Student();
student.setName("winfun");
StudentService studentService = new StudentServiceEnhanceImplV2();
studentService.goToClass(student);
studentService.eat(student);
studentService.sleep(student);
}
}
public class LogUtils {
public static void printNow(){
System.out.println("当前时间:" + new Date());
}
}
1.2 改善
即使上面使用了工具类统一封装增强功能,但是每个业务方法里面还是冗余了一些非业务性的代码。
当然了,此时我们可以利用 GoF 中的某些设计模式来优化此代码。
1.2.1 装饰者模式
在行为模式中,装饰者模式可以在原有的逻辑上扩展额外的功能:我们可以理解为装饰者里组合了原目标对象,并实现了原目标对象的接口,在实现方法里对原目标对象做增强。
代码:
@AllArgsConstructor
public class StudentServiceImplDecorate implements StudentService {
private StudentService target;
@Override
public void goToClass(Student student) {
LogUtils.print(student.getName(),"去上课了");
target.goToClass(student);
}
@Override
public void eat(Student student) {
LogUtils.print(student.getName(),"去吃饭了");
target.eat(student);
}
@Override
public void sleep(Student student) {
LogUtils.print(student.getName(),"去睡觉了");
target.sleep(student);
}
public static void main(String[] args) {
Student student = new Student();
student.setName("winfun");
StudentService studentService = new StudentServiceImpl();
StudentService studentServiceDecorate = new StudentServiceImplDecorate(studentService);
studentServiceDecorate.goToClass(student);
studentServiceDecorate.eat(student);
studentServiceDecorate.sleep(student);
}
}
这样虽然避免了原有业务逻辑里的非业务逻辑功能代码,但是每一个需要被增强的类都需要抽一个装饰者出来,这也是非常多的重复代码,并且代码量显得更多了。
1.2.2 模版模式
在行为模式中,可以抽取逻辑还有一个就是模版方法:我们需要抽象出一个抽象类,实现原目标对象的接口,对原目标对象需要增强的方法抽象多一个子方法,后续的子类都需要重写此方法来做业务逻辑。
代码:
/**
* 学生Service增强第四版:利用模版模式抽取增强代码
* @author winfun
**/
public abstract class StudentServiceTemplate implements StudentService {
@Override
public void goToClass(Student student) {
LogUtils.print(student.getName(),"去上课了");
doGoToClass(student);
}
protected abstract void doGoToClass(Student student);
@Override
public void eat(Student student) {
LogUtils.print(student.getName(),"去吃饭了");
doEat(student);
}
protected abstract void doEat(Student student);
@Override
public void sleep(Student student) {
LogUtils.print(student.getName(),"去睡觉了");
doSleep(student);
}
protected abstract void doSleep(Student student);
public static void main(String[] args) {
Student student = new Student();
student.setName("winfun");
}
}
/**
* 学生Service增强第四版:模版实现类
* @author winfun
**/
public class StudentServiceTemplateImpl extends StudentServiceTemplate {
@Override
protected void doGoToClass(Student student) {
System.out.println(student.getName()+" 去上课了");
}
@Override
protected void doEat(Student student) {
System.out.println(student.getName()+" 去吃饭了");
}
@Override
protected void doSleep(Student student) {
System.out.println(student.getName()+" 去睡觉了");
}
public static void main(String[] args) {
Student student = new Student();
student.setName("winfun");
StudentService studentService = new StudentServiceTemplateImpl();
studentService.goToClass(student);
studentService.eat(student);
studentService.sleep(student);
}
}
我们可以发现,即使这样优化,冗余代码也是一点都没变少,甚至还变多了;并且由于使用了继承,所以如果业务实现类想继续扩展其他功能,也就无计可施了。
2、动态代理如何解决相同逻辑的重复代码?
代理模式分为静态代理和动态代理:
而静态代理需要编写代理类,组合原有的目标对象,并实现原有目标对象的接口,以此来做原有对象的方法功能的增强。这么一看,静态代理的代码和装配者模式的基本一样,所以说,如果单纯使用静态代理,和使用装配者模式基本没啥区别。
动态代理:动态代理是在运行时动态将增强逻辑类组合原有的目标对象,然后生成代理对象,以此完成对目标对象方法的功能增强。
既然静态代理有对应的动态代理,那么如果装配者模式也有动态装配者模式的话,也是可以完美完成增强的
。
还是上面的例子,不过这次增强是简单的日志打印,在方法执行时,打印对应的参数和方法,借此记录学生的行为和对应的时间。
增强逻辑,实现 InvocationHandler 接口。
/**
* invocation handler
* @author winfun
**/
public class LogAdvisor implements InvocationHandler {
private Object target;
public LogAdvisor(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
LogUtils.print(method.getName(),args);
return method.invoke(target,args);
}
}
日志工具类:
/**
* 日志工具类
* @author winfun
**/
public class LogUtils {
public static void print(String action,Object... args){
StringJoiner argsMsg = new StringJoiner(",");
if (args.length > 0){
for (Object arg : args) {
argsMsg.add(arg.toString());
}
}
System.out.println("当前时间:" + new Date()+",参数:"+argsMsg+",方法:"+action);
}
}
创建代理类进行测试:
public class Application {
public static void main(String[] args) {
Student student = new Student();
student.setName("winfun");
StudentService studentService = new StudentServiceImpl();
// 利用Proxy创建代理类,ps:不能用 StudentService.class.getXxx()
StudentService proxy = (StudentService) Proxy.newProxyInstance(studentService.getClass().getClassLoader(), studentService.getClass().getInterfaces(), new LogAdvisor(studentService));
proxy.goToClass(student);
proxy.eat(student);
proxy.sleep(student);
}
}
运行结果:
当前时间:Thu Jul 29 22:36:53 CST 2021,参数:Student(name=winfun),方法:goToClass
winfun 去上课了
当前时间:Thu Jul 29 22:36:53 CST 2021,参数:Student(name=winfun),方法:eat
winfun 去吃饭了
当前时间:Thu Jul 29 22:36:53 CST 2021,参数:Student(name=winfun),方法:sleep
winfun 去睡觉了
3、如何理解面向切面编程?
横切面,英文表示为 Aspect ,它表示的是分布在一个 / 多个类的多个方法中的相同逻辑。利用动态代理,将这部分相同的逻辑抽取为一个独立的 Advisor 增强器,并在原始对象的初始化过程中,动态组合原始对象并产生代理对象,同样能完成一样的功能增强。在此基础上,通过指定增强的类名、方法名(甚至方法参数列表类型等),可以更细粒度的对方法增强。使用这种方式,可以在不修改原始代码的前提下,对已有任意代码的功能增强。而这种针对相同逻辑的扩展和抽取,就是所谓的面向切面编程(Aspect Oriented Programming,AOP)。
摘自 LinkedBear 作者
4、jdk 动态代理与 Cglib 动态代理有什么区别?
jdk 动态代理是基于接口来实现的,底层利用的是 Proxy 的 newInstance 方法来创建代理对象,而 Cglib 的动态代理是基于类实现的,会基于原目标对象生成子类。