⑤ 多态 (Polymorphism)
子类可以替换父类
,一个多态的简单例子:
public class IBrother { void doSomething() { System.out.println("搞事"); } } public class TeaBrother extends IBrother { @Override void doSomething() { System.out.println("给大佬递茶"); } } public class Test { public static void main(String[] args) { IBrother brother = new TeaBrother(); brother.doSomething(); // 输出结果:给大佬递茶 } }
从上面这个例子,可以看到多态这种需要编程语言支持下述三个语法机制:
父类可以引用子类对象 → 支持继承 → 子类可以重写(Override)父类中的方法。
上述代码是Java中的 运行时多态,还有一个编译时多态,通过 方法重载(Overload) 实现。
多态还有两种较常见的实现方式:接口类语法(注入) 和 duck-typing语法(Python中类具有相同的方法即可实现多态)。
多态的意义:
提高代码的扩展性和复用性,很多设计原则、设计模式、编程技巧的代码实现基础。
⑥ 接口与抽象类的区别
抽象类(is-a)
:不允许被实例化,只能被继承,可以包含属性和方法,子类继承需实现所有抽象方法;接口(has-a)
:不能包含属性(成员变量),只能声明方法,不能含代码实现,实现接口时需实现声明的所有方法;
使用抽象类更多是为了 代码复用
,强制子类实现所有抽象方法,减少类误用导致报错。
而接口侧重于 解耦
,对行为的一种抽象(协议/契约),调用者只需关注抽象接口,无需了解具体实现,约定与实现分离,降低代码间的耦合,提高代码的扩展性。
⑦ 为何基于接口而非实现编程
接口和实现分离,封装不稳定的实现,暴露稳定的接口,上游系统面向接口而非面向实现编程,不依赖不稳定的实现细节,当实现发生改变时,上游代码不许改动,降低耦合,提高扩展性。
如何将原则应用到实践中:
- 函数命名不暴露任何实现细节 (如:uploadToQiniuYun(×) → upload(√) )
- 封装具体的实现细节 (如:上传流程不该暴露给调用者,应在类内部分封装)
- 为实现类定义抽象的接口 (依赖统一的接口定义,使用者依赖接口,而不是具体实现类来编程)
做软件开发时,一定要有抽象、封装和接口意识,接口的定义只表明做什么,而不是怎么做。设计接口时多思考这样的接口设计是否足够通用,是否能够做到在替换具体接口实现时,不需要任何接口定义的改动。
是否需要为每个类都定义接口?
凡事都讲究一个 度,滥用这条原则非得给每个类都定义接口,接口满天飞会导致不必要的开发负担。回归设计初衷,如果在业务场景中,某个功能只有一种实现方式,未来也不可能被其他方式替代,就没必要为其设置接口,直接用实现类就好了。
⑧ 为何说要多用组合少用继承
继承层次过审、过复杂,会影响到代码的可读性和可维护性,看一个子类跳一堆父类,父类修改影响所有子类逻辑。
举个例子,设计一个奶茶店茶的类,先定义一个抽象类 AbstractTea,然后是各种奶茶,珍珠奶茶,椰果奶茶,波霸奶茶等
AbstractTea → 珍珠奶茶 → 椰果奶茶 → 波霸奶茶
但,有些店除了卖奶茶还卖纯茶,为了区分需要在AbstractTea的基础上派生出两个更加细分的抽象类:
AbstractTea → AbstractMilkTea → 珍珠奶茶 → 椰果奶茶 → 波霸奶茶 → AbstractPureTea → 纯四季春 → 纯绿妍 → 纯金凤茶王
继承关系从两层变成三层,层次还算浅,如果再加一个条件呢,冷热:
AbstractTea → AbstractMilkTea → AbstractMilkColdTea → 冰珍珠奶茶 → 冰椰果奶茶 → 冰波霸奶茶 → AbstractMilkHotTea → 热珍珠奶茶 → 热椰果奶茶 → 热波霸奶茶 → AbstractPureTea → AbstractPureColdTea → 冰纯四季春 → 冰纯绿妍 → 冰纯金凤茶王 → AbstractPureHotTea → 热纯四季春 → 热纯绿妍 → 热纯金凤茶王
如果再加一个条件,是否加糖,2333,五层直接原地裂开。
上述的问题其实可以通过 组合(Composition)、接口和委托(Delegation) 这三种技术手段来解决。
public interface IProduct { void product(); } public interface ITemperature { void temperature(); } public interface ISweet { void sweet(); } // 实现上述接口 public class ColdSugarPearlMilkTea implements IProduct, ITemperature, ISweet { @Override public void product() { System.out.println("原料是:牛奶+茶"); } @Override public void sweet() { System.out.println("加糖"); } @Override public void temperature() { System.out.println("冷"); } }
接口只声明方法,不定义实现,每种茶都要实现接口中的方法,有些实现逻辑是一样的,代码重复,引入组合委托来消除此问题:
public class MilkTea implements IProduct { @Override public void product() { System.out.println("原料是:牛奶+茶"); } } public class Sugar implements ISweet{ @Override public void sweet() { System.out.println("加糖"); } } public class Cold implements ITemperature{ @Override public void temperature() { System.out.println("冷"); } } public class ColdSugarPearlMilkTea implements IProduct, ITemperature, ISweet { // 组合 private MilkTea milkTea = new MilkTea(); private Cold cold = new Cold(); private Sugar sugar = new Sugar(); @Override public void product() { milkTea.product(); // 委托 } @Override public void sweet() { sugar.sweet(); } @Override public void temperature() { cold.temperature(); } }
实际开发中要根据具体情况选择继承还是组合,类间继承结构稳定、层次较浅,关系不复杂,可以大胆地使用继承。反制,就尽量使用组合替代继承。除此之外一些设计模式、特殊应用场景,会固定使用继承或组合。
⑨ 贫血模型和充血模型
MVC → Model(数据层)、View(展示层)、Controller(逻辑层),很多项目并不会100%遵从这种固定的分层方式。
现在很多Web或App项目都是前后端分离的,一般将后端项目分为下面几层:
- Repository层 → 负责数据访问;
- Service层 → 负责业务逻辑;
- Controller层 → 负责暴露接口;
一个充血模型的示例如下:
////////// Controller+VO(View Object) ////////// public class UserController { private UserService userService; //通过构造函数或者IOC框架注入 public UserVo getUserById(Long userId) { UserBo userBo = userService.getUserById(userId); UserVo userVo = [...convert userBo to userVo...]; return userVo; } } public class UserVo {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Service+BO(Business Object) ////////// public class UserService { private UserRepository userRepository; //通过构造函数或者IOC框架注入 public UserBo getUserById(Long userId) { UserEntity userEntity = userRepository.getUserById(userId); UserBo userBo = [...convert userEntity to userBo...]; return userBo; } } public class UserBo {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Repository+Entity ////////// public class UserRepository { public UserEntity getUserById(Long userId) { //... } } public class UserEntity {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; }
像UserBo这样,只包含数据,不包含业务逻辑的类,就叫做贫血模型 (Anemic Domain Model),同理UserVo 和UserEntity也是贫血模型。而 充血模型 (Rich Domain Model) 正好相反,类中 既包含数据,又包含业务逻辑。
贫血模型的Service层包含Service类和BO类(贫血模型),业务逻辑集中在Service类中; 重学模型的Service层包含Service类和Domain类(充血模型,包含数据和业务),Service类较单薄。
Tips:这部分只是看是明白了,先暂时理解成:贫血模型 → 只有属性的类,好处是容易看懂,充血模型 → 有属性也有业务逻辑的类,好处是代码复用,坏处是成本较高。