文章目录
设计模式-软件设计七大原则
1.开闭原则(Open Closed Principle)
1.1 说明
定义
:一个软件实体如类、模块和函数应该对外扩展开放,对修改关闭。
- 用
抽象
构建框架,用实现
扩展细节 - 本质就是面向抽象的接口编程
优点
:提高软件系统的可复用性及可维护性
1.2 Code Demo
以一个Demo为例,阐述开闭原则。
假设现在有个银行的支付接口
IBankPay,有三个方法。付款、收款、获取余额。所有的第三方支付工具都应该遵守这个接口规范。
1.2.1 IBankPay接口
/**
* 银行提供支付接口
* @author MuZiYu
*/
public interface IBankPay {
/**
* 付款接口
*/
void pay(BigDecimal payMoney);
/**
* 收款接口
*/
void collect(BigDecimal collectMoney);
/**
* 获取账户余额
*/
BigDecimal getBalance();
}
假设现在有两个支付平台,比如我们熟悉的支付宝和微信。分别实现银行的支付接口IBankPay
1.2.2 Alipay
/**
* 支付宝实现类
* @Author MuZiYu
*/
public class Alipay implements IBankPay{
private BigDecimal balance;
public Alipay(BigDecimal balance) {
this.balance = balance;
}
@Override
public void pay(BigDecimal payMoney) {
this.balance = balance.subtract(payMoney);
System.out.println("支付宝付款成功,付款金额"+payMoney);
}
@Override
public void collect(BigDecimal collectMoney) {
this.balance = balance.add(collectMoney);
System.out.println("支付宝收款成功,收款金额"+collectMoney);
}
@Override
public BigDecimal getBalance() {
return this.balance;
}
}
1.2.3 WeiChatPay
/**
* 微信支付
* @Author MuZiYu
*/
public class WeiChatPay implements IBankPay{
private BigDecimal balance;
public WeiChatPay(BigDecimal balance) {
this.balance = balance;
}
@Override
public void pay(BigDecimal payMoney) {
this.balance = balance.subtract(payMoney);
System.out.println("微信支付成功,付款金额"+payMoney);
}
@Override
public void collect(BigDecimal collectMoney) {
this.balance = balance.add(collectMoney);
System.out.println("微信收款成功,收款金额"+collectMoney);
}
@Override
public BigDecimal getBalance() {
return this.balance;
}
}
1.2.4 Test
那么,我们有个Test类,去测试这两种实现方式的方法。我们同样地初始余额设置为1000元,分别支付500元,再收款200元,看最终余额。
public class Test {
public static void main(String[] args) {
IBankPay payWay1 = new Alipay(new BigDecimal("1000"));
payWay1.pay(new BigDecimal("500"));
payWay1.collect(new BigDecimal("200"));
System.out.println("AliPay Balance:"+payWay1.getBalance());
IBankPay payWay2 = new WeiChatPay(new BigDecimal("1000"));
payWay2.pay(new BigDecimal("500"));
payWay2.collect(new BigDecimal("200"));
System.out.println("WeChatPay Balance:"+payWay2.getBalance());
}
}
1.3 需求变动
假设现在需求需要变动,支付宝或者微信需要实现一个新功能,比如支付时需要收取不同费率的手续费。我们需要在各自的pay方法修改实现。
/**
* 支付宝收取手续费0.5%
*/
@Override
public void pay(BigDecimal payMoney) {
this.balance = balance.subtract(payMoney.multiply(new BigDecimal("1.005")));
System.out.println("支付宝付款成功,付款金额"+payMoney);
}
/**
* 微信收取手续费1%
*/
@Override
public void pay(BigDecimal payMoney) {
this.balance = balance.subtract(payMoney.multiply(new BigDecimal("1.01")));
System.out.println("微信支付成功,付款金额"+payMoney);
}
但是如果此时又有一个需求到来,支付宝或者微信要支持使用优惠券支付。如果我们直接修改接口,增加一个方法payByUseCoupon
,那么很多实现IBankPay接口的其它支付工具都需要去实现这个方法。可是他们并不支持这个方法。
需要注意:当接口定义好,应当尽量避免直接修改接口。
所以我们可以使用继承
,用实现
去扩展细节
。
1.3.1 CouponAlipay
public class CouponAlipay extends Alipay {
public CouponAlipay(BigDecimal balance) {
super(balance);
}
/**
* 使用优惠券支付
*/
public void payByUserCoupon(BigDecimal payMoney, BigDecimal couponMoney){
super.pay(payMoney.subtract(couponMoney));
}
}
1.3.2 Test
public static void main(String[] args) {
IBankPay payWay1 = new Alipay(new BigDecimal("1000"));
payWay1.pay(new BigDecimal("500"));
System.out.println("AliPay Balance:"+payWay1.getBalance());
System.out.println("----------------------");
IBankPay payWay3 = new CouponAlipay(new BigDecimal("1000"));
CouponAlipay pay = (CouponAlipay) payWay3;
pay.payByUserCoupon(new BigDecimal("500"),new BigDecimal("100"));
System.out.println("AliPay Balance:"+pay.getBalance());
System.out.println("Pay Money Count By Using Coupon:"+pay.getCouponPayMoneyCount());
}
1.3.3 UML类图
1.4 总结
开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
2.依赖倒置原则(Dependency Inversion Principle)
2.1 定义
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 针对
接口
编程,不要针对实现编程
2.2 优点
减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。
2.3 Code Demo
依赖倒置原则的核心就是针对接口编程
现在来看一个Demo,假设计算机学科的学生需要学习多门语言课程。如果是面向实现
编程,则需要不断地修改这个ComputerStudent类,增加方法。
public class ComputerStudent {
public void studyJava(){
System.out.println("学习java课程");
}
public void studyPython(){
System.out.println("学习python课程");
}
......
}
public class Test {
public static void main(String[] args) {
ComputerStudent student = new ComputerStudent();
student.studyJava();
student.studyPython();
......
}
}
Test这个类和ComputerStudent相比,相当于高层模块
。此时该高层模块依赖低层模块ComputerStudent,并且ComputerStudent面向实现编程,导致强耦合。
2.4 抽取接口,面向接口编程
2.4.1 ICourse接口
public interface ICourse {
void studyCourse();
}
2.4.2 JavaCourse实现类
public class JavaCource implements ICourse {
@Override
public void studyCourse() {
System.out.println("学习java课程");
}
}
2.4.3 PythonCourse实现类
public class PythonCourse implements ICourse{
@Override
public void studyCourse() {
System.out.println("学习python课程");
}
}
2.4.4 ComputerStudent改造
public class ComputerStudent {
public void study(ICourse iCourse){
iCourse.studyCourse();
}
}
可以通过方法传参的方式传入ICourse接口的实现类,具体实现取决于传入的ICourse接口的实现类。【接口方法方式注入】
2.4.5 Test
public class Test {
public static void main(String[] args) {
// ComputerStudent student = new ComputerStudent();
// student.studyJava();
// student.studyPython();
ComputerStudent student = new ComputerStudent();
student.study(new JavaCource());
student.study(new PythonCourse());
}
}
此时不论计算机学生需要扩展学习其他任何的课程,ComputerStudent这个类都不需要去动。只需要不断地实现ICourse接口的实现类即可。
此时,ComputerStudent在学习任何课程的时候,具体的实现类交给高层模块Test去决定(传具体的课程学习对象),而不是针对ComputerStudent面向实现编程。
也就是说,我们是面向ICourse接口编程,而不是面向具体的ComputerStudent编程。
2.4.6 UML
此时只需要水平扩展
即可。ComputerStudent和Test是解耦的,同时也是和ICourse的具体实现类是解耦的。
但是ComputerStudent和ICourse是依赖关系。
2.4.7 改造:通过构造注入
public class ComputerStudent {
// 其实这和Spring的依赖注入就已经很相似了
private ICourse iCourse;
public ComputerStudent(ICourse iCourse) {
this.iCourse = iCourse;
}
public void study(){
iCourse.studyCourse();
}
// public void study(ICourse iCourse){
// iCourse.studyCourse();
// }
}
public class Test {
public static void main(String[] args) {
// ComputerStudent student = new ComputerStudent();
// student.studyJava();
// student.studyPython();
// ComputerStudent student = new ComputerStudent();
// student.study(new JavaCource());
// student.study(new PythonCourse());
ComputerStudent student1 = new ComputerStudent(new JavaCource());
student1.study();
ComputerStudent student2 = new ComputerStudent(new PythonCourse());
student2.study();
}
}
不过这样有点不好,就是每次都得重新new一个ComputerStudent对象才行。所以可以再改造,提供Getter/Setter修改ICourse实现类。
public class ComputerStudent {
private ICourse iCourse;
public ComputerStudent(ICourse iCourse) {
this.iCourse = iCourse;
}
public void study(){
iCourse.studyCourse();
}
public ICourse getICourse() {
return iCourse;
}
public void setICourse(ICourse iCourse) {
this.iCourse = iCourse;
}
}
public class Test {
public static void main(String[] args) {
// ComputerStudent student = new ComputerStudent();
// student.studyJava();
// student.studyPython();
// ComputerStudent student = new ComputerStudent();
// student.study(new JavaCource());
// student.study(new PythonCourse());
// ComputerStudent student1 = new ComputerStudent(new JavaCource());
// student1.study();
// ComputerStudent student2 = new ComputerStudent(new PythonCourse());
// student2.study();
ComputerStudent student = new ComputerStudent(new JavaCource());
student.study();
student.setICourse(new PythonCourse());
student.study();
}
}
2.5 总结
依赖倒置原则的本质就是通过抽象(接口或抽象类)
使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。需要遵循以下的几个规则。
每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。
任何类都不应该从具体类派生
如果一个项目处于开发状态,确实不应该有从具体类派生出子类的情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在所难免的,因此一些情况下的继承都是可以忍受的。
特别是项目维护阶段,维护工作基本上都是进行扩展开发,修复行为。通过一个继承关系,覆写一个方法就可以修正一个很大的Bug,何必去继承最高的基类呢?(当然这种情况尽量发生在不甚了解父类或者无法获得父类代码的情况下。)
尽量不要覆写基类的方法
如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。
类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。
结合里氏替换原则使用
接口负责定义public属性和方法,并且声明与其他对象的依赖关系。
抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
3.单一职责(Single Responsibility Principle)
3.1 定义
- 不要存在
多于一个
导致类变更
的 原因- 一个类/接口/方法 只负责 一项职责
3.2 优点
降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险
3.3 类的单一职责Code
3.3.1 多职责的类
在Bird类里定义一个移动方式的方法move
public class Bird {
public void move(String name){
System.out.println(name+":用翅膀飞");
}
}
public class Test {
public static void main(String[] args) {
Bird bird = new Bird();
bird.move("大雁");
bird.move("鸵鸟");
}
}
很明显,鸵鸟用翅膀飞就不合理。大雁和鸵鸟虽然都是鸟,但是它们的移动方式完全不同,所以用一个类强行表现这多种情况,就会让这个类的职责变得非常复杂。
public class Bird {
public void move(String name){
if("鸵鸟".equals(name)){
System.out.println(name+":用脚走");
}else{
System.out.println(name+":用翅膀飞");
}
}
}
3.3.2 拆分职责
我们针对可以飞的鸟和用脚走路的鸟,拆分成两种类
public class FlyBird {
public void move(String name){
System.out.println(name+":用翅膀飞");
}
}
public class FootBird {
public void move(String name) {
System.out.println(name + ":用脚走");
}
}
public class Test {
public static void main(String[] args) {
// Bird bird = new Bird();
// bird.move("大雁");
// bird.move("鸵鸟");
FlyBird flyBird = new FlyBird();
flyBird.move("大雁");
FootBird footBird = new FootBird();
footBird.move("鸵鸟");
}
}
3.4 接口的单一职责
3.4.1 多职责的接口
现在有一个接口,FoodStore 食品店的接口。
public interface FoodStore {
// 生产食物
void product();
// 加工食物
void process();
//销售
void sale();
}
接口既负责生产、加工这些属于工厂操作的职责
,又要负责销售这个职责。所以这个接口的职责不是单一的,那么这是不易扩展的。想象一下,在实际生活中,假如一个商店的规模越来越大,那么它还能既负责生产加工、又负责销售吗?所以最好将其分离开来,形成两个接口。这样生产加工和销售两不误。
3.4.2 拆分职责的接口
将食物生产加工的方法抽取到一个FoodStoreFactory接口中,将食物销售的接口抽取到FoodStoreSale接口中
public interface FoodStoreFactory {
// 生产食物
void product();
// 加工食物
void process();
}
public interface FoodStoreSale {
//销售
void sale();
}
此时,新建一个实现类SteamedBunFoodStore馒头食品店的类,同时继承两个接口就具有了两种接口的行为,既可以生产加工,也可以销售。如果后期业务扩展,可以只继承一个FoodStoreSale或者 FoodStoreFactory接口,只专注单向业务。
public class SteamedBunFoodStore implements FoodStoreFactory,FoodStoreSale {
@Override
public void product() {
}
@Override
public void process() {
}
@Override
public void sale() {
}
}
3.5 方法的单一职责
3.5.1 多职责的方法
其实我们很多时候有如下操作。根据传入的boolean的值不同,执行不同的方法。这就是一个方法中有两个职责,此时可以将其拆分出来,维护起来也更加明显。
public class UserMethod {
public void method(String arg1,String arg2,boolean b){
if(b){
//do something
}else{
//do something
}
}
}
3.6 总结
总的来说,最好保证
接口
和方法
的单一职责原则,尽量保证类
。不过实际开发中对于类的职责还是要分实际情况来决定。
4.接口隔离( Interface Segregation Principle)
4.1 定义
用多个专门的接口,而
不使用单一的总接口
,客户端不应该依赖它不需要的接口
- 一个类对一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
注意适度原则,一定要适度
4.2 优点
符合高内聚、低耦合的设计思想,从而使类具有很好的可读性、可扩展性、可维护性。
4.3 Code Demo
4.3.1 未遵循接口隔离原则的接口
public interface IAnimalAction {
void swim();
void fly();
void run();
}
4.3.2 实现类
public class Dog implements IAnimalAction {
@Override
public void swim() {
System.out.println("Dog Swimming...");
}
@Override
public void fly() {
//无实现
}
@Override
public void run() {
System.out.println("Dog Running...");
}
}
public class Bird implements IAnimalAction {
@Override
public void swim() {
//无实现
}
@Override
public void fly() {
System.out.println("Bird Fly....");
}
@Override
public void run() {
//无实现
}
}
可以看到,Dog继承IAnimalAction接口,实现了swim、fly、run三个方法。但是fly方法是被强制去依赖实现的,但是Dog根本不需要这个方法。
同理,Bird继承IAnimalAction接口,swim和run方法对其是多余的。
所以这个IAnimalAction接口设计的就很有问题,是一个“胖接口”,需要遵循接口隔离原则。
4.4.3 遵循接口隔离原则后的改造
我们按照接口隔离原则,将IAnimalAction接口改造为三个接口。
public interface IFlyAnimalAction {
void fly();
}
public interface IRunAnimalAction {
void run();
}
public interface ISwimAnimalAction {
void swim();
}
public class Dog implements IRunAnimalAction,ISwimAnimalAction {
@Override
public void swim() {
System.out.println("Dog Swimming...");
}
@Override
public void run() {
System.out.println("Dog Running...");
}
}
public class Bird implements IFlyAnimalAction {
@Override
public void fly() {
System.out.println("Bird Fly....");
}
}
遵循了接口隔离原则,避免了 客户端 依赖它不需要的接口这个问题。
4.4 与单一职责原则的区别
- 接口隔离针对的层面是接口,强调的是它的实现类不应该被强制实现一些根本不需要的方法(
客户端不应该被迫依赖于他们不使用的接口
),是一种对接口层面的约束。希望一个接口方法越少越好,最好是单一接口(比如有一个IWorker接口,有eat方法和work方法,工人类去实现这个接口自然没什么问题。但如果有个机器人类也实现这个接口,就需要被迫实现eat方法,但实际上这个接口方法被实现后是没任何实际逻辑的(默认实现或空实现)。所以这个接口被污染了 - 而单一职责主要强调是职责,是业务逻辑上的划分,针对的是接口的实现和细节。比如上述的食品店接口,既负责生产加工,又负责销售。该接口具备两样职责,不利于后续维护。再比如,一个IUser接口,既包含了获取用户信息的相关接口,又包含了对用户信息操作的接口,这就是一个接口有多个职责,应当对职责进行拆分。变成一个只获取用户信息的接口IUserInfo,一个操作用户信息的接口IUserDAO
4.5 总结
用于处理胖接口(fat interface)所带来的问题。如果类的接口定义暴露了过多的行为,则说明这个类的接口定义内聚程度不够好。换句话说,类的接口可以被分解为多组功能函数的组合,每一组都服务于不同的客户类,而不同的客户类可以选择使用不同的功能分组。
接口隔离必须平衡适度
,如果划分地过细,导致接口数量过多,也是不利于维护的。
5.迪米特原则(最少知道原则)(Least Knowledge Principle)
5.1 定义
一个对象应当对其他对象保持最少的了解。
- 尽量降低类与类之间的耦合
- 强调只和朋友交流,不和陌生人交流(什么是朋友?出现在
成员变量
、方法的输入
、方法输出
的类,就是成员朋友类。出现在方法体内部的类不属于朋友类,该类如果出现代表不满足迪米特法则,这个类是陌生朋友
)
其实就是,对外部引用的类越少越好。类自身尽量多使用private
、protected
关键字
5.2 优点
降低类与类之间的耦合
5.3 Code Demo
5.3.1 一个违背迪米特原则的例子
public class Boss {
/**
* 获取雇员的数量
*/
public int getEmployeeCount(DepartmentManager departmentManager){
//模拟从数据库查询---此时Employee不是Boss的直接朋友类
List<Employee> employees = new ArrayList<>();
for (int i = 0; i < 20; i++) {
employees.add(new Employee());
}
return departmentManager.countEmployees(employees);
}
}
public class DepartmentManager {
/**
* 计算雇员的数量
*/
public int countEmployees(List<Employee> employees){
return employees.size();
}
}
public class Employee {
}
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
System.out.println(boss.getEmployeeCount(new DepartmentManager()));
}
}
在上述的例子中。Boss想要统计自己公司的员工人数,他只需要和部门主管DepartmentManager打交道即可,不应该和员工Employee产生直接的耦合。在Boss类中,Employee属于方法体内的类,不是成员朋友类,因此Boss不应该和Employee直接打交道。
5.3.2 遵守迪米特原则
public class Boss {
public int getEmployeeCount(DepartmentManager departmentManager){
return departmentManager.countEmployees();
}
}
public class DepartmentManager {
public int countEmployees(){
//模拟从数据库查询
List<Employee> employees = new ArrayList<>();
for (int i = 0; i < 20; i++) {
employees.add(new Employee());
}
return employees.size();
}
}
其实就是把Boss对Employee的依赖舍去,将相关操作和依赖放到DepartmentManager中。这就实现了Boss与Employee的解耦。
5.4 总结
迪米特原则与实际开发中比较贴切的就是MVC分层开发,Controller层应该减少相关的耦合,尽量不要直接出现DAO的类,将相关依赖和业务处理放在Service中完成。
迪米特只是要求尽量降低耦合,但是并不是杜绝。完全的没有依赖基本不可能
在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?
你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
6.里氏替换原则(Liskov Substitution Principle)
6.1 定义
- 所有引用基类的地方必须能透明地使用其子类的对象。
通俗点讲,就是只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
- 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
6.2 优点
- 为良好的继承提供了规范
6.3 注意
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚合、组合等关系代替继承。
6.4 Code demo
6.4.1 违背里氏替换原则
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
@Override
public void fun(int a,int b){
System.out.println(a+"-"+b+"="+(a-b));
}
}
public class demo {
public static void main(String[] args){
System.out.println("父类的运行结果");
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println("子类替代父类后的运行结果");
B b=new B();
b.fun(1,2);
}
}
子类运行的结果很明显和父类的fun方法调用结果不一致,这就违背了里氏替换原则。此处父类A并不能被子类B完全替代。
6.4.2 子类可以有特有方法
public class Father {
public void f(HashMap map){
System.out.println("Father f() invoke...");
}
}
public class Son extends Father {
public void f2(){
System.out.println("Son f2() invoke...");
}
}
public class Test {
public static void main(String[] args) {
HashMap map = new HashMap();
Father father = new Father();
father.f(map);
System.out.println("-------------使用子类替换--------------");
Son son = new Son();
son.f(map);
son.f2();
}
}
打印结果:
Father f() invoke...
-------------使用子类替换--------------
Father f() invoke...
Son f2() invoke...
6.4.3 子类覆盖或实现父类方法时,子类方法形参 要比 父类方法形参 范围大
public class Father {
public void f(HashMap map){
System.out.println("Father f() invoke...");
}
}
public class Son extends Father {
// @Override---这里使用@Override注解会报错,因为这是方法重载而不是重写
public void f(Map map){
System.out.println("Son f() invoke...");
}
// @Override---这是方法重写
// public void f(HashMap map){
// System.out.println("Son f() invoke...");
// }
}
public class Test {
public static void main(String[] args) {
HashMap map = new HashMap();
Father father = new Father();
father.f(map);
System.out.println("-------------使用子类替换--------------");
Son son = new Son();
son.f(map);
}
}
让我们想象一下结果,这会违背里氏替换原则吗?
Father f() invoke...
-------------使用子类替换--------------
Father f() invoke...
答案是不会
,因为子类在方法定义时,形参使用的是Map,它是重载从父类继承过来的形参为HashMap的方法f(HashMap map)。所以当传入HashMap类型的参数时,并没有调用这个重载的方法,而是调用的是从父类继承过来的原先的方法。
假如我们调换下范围,让父类方法的形参是小范围(HashMap),子类方法的形参是大范围(Map)。又会怎样?
public class Father {
public void f(Map map){
System.out.println("Father f() invoke...");
}
}
public class Son extends Father {
public void f(HashMap map){
System.out.println("Son f() invoke...");
}
}
public class Test {
public static void main(String[] args) {
HashMap map = new HashMap();
Father father = new Father();
father.f(map);
System.out.println("-------------使用子类替换--------------");
Son son = new Son();
son.f(map);
}
}
Father f() invoke...
-------------使用子类替换--------------
Son f() invoke...
假设我们传入的参数类型是Map类型
public class Test {
public static void main(String[] args) {
Map map = new HashMap();
Father father = new Father();
father.f(map);
System.out.println("-------------使用子类替换--------------");
Son son = new Son();
son.f(map);
}
}
Father f() invoke...
-------------使用子类替换--------------
Father f() invoke...
则符合里氏替换原则。
除此以外,使用继承时,子类方法的返回值类型不能比父类方法返回值类型大,否则报错。
6.5 总结
-
里氏替换原则要求任何时候父类替换为子类时,能够保证程序不出错,并且不改变原有的逻辑。那就
要求子类不要重写父类的方法
。这和多态的出发点不一样,里氏替换原则就是为了避免继承的一些缺陷。而多态就是为了扩展父类,让子类有更多的行为,让子类拥有更多的个性,所以要求重写父类方法。 -
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
- 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
- 增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。有时修改了一点点代码都有可能需要对打断程序进行重构。
-
里氏替换原则中,子类
不应该去实现父类已经实现好的方法
,但需要实现父类的抽象方法。 -
子类可以有自己的个性,就是子类可以在自己的类中定义别的方法
7.合成/复用原则(组合/复用原则)
原则就是使用 组合和聚合代替继承
为了解决继承的强耦合(is a
),使用组合和聚合去代替(has a
)
设计原则的核心思想
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程
- 为了交互对象之间的松耦合设计而努力