面向对象设计原则面试
概述spring
对于面向对象软件系统的设计而言,在支持可维护性的同时,提升系统的可复用性是一个相当重要的问题,如何同时提升一个软件系统的可维护性和可复用性是面向对象设计须要解决的核心问题之一。在面向对象设计中,可维护性的复用是以设计原则为基础的。每个原则都蕴含一些面向对象设计的思想,能够从不一样的角度提高一个软件结构的设计水平。 面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在不少设计模式中,它们是从许多设计方案中总结出的指导性原则。面向对象设计原则也是咱们用于评价一个设计模式的使用效果的重要指标之一,在设计模式的学习中,你们常常会看到诸如“XXX模式符合XXX原则”、“XXX模式违反了XXX原则”这样的语句。编程
最多见的7种面向对象设计原则以下表所示:后端
1.单一职责原则设计模式
单一职责定义bash
一个类只负责一个功能领域中的相应职责,或者能够定义为:就一个类而言,应该只有一个引发它变化的缘由微信
从定义中不难思考,一个类的所作的事情越多,也就越难以复用,由于一旦作的事情多了,职责的耦合度就变高了因此咱们根据这个原则应该将不一样职责封装在不一样类中,不一样的变化封装在不一样类中。从咱们日常的开发中不难发现,若是一个类或者方法接口等等只作一件事,那么可读性很高,而且复用性也很高,而且一旦需求变化,也容易维护,假如你一个类糅杂多个职责,那么很难维护。多线程
单一职责举例分析架构
从实际业务来剥离一个例子:如今有这么一种状况,某租车平台我的模块类涉及多个方法,有以下登陆、注册、支付宝押金支付、微信押金支付、支付宝套餐支付、微信套餐支付、整个结构以下:并发
/**
* 我的模块
*/
@Controller
public class userController{
/**
* 登陆
*/
public void login(){
}
/**
* 注册
*/
public void register(){
}
/**
* 押金支付(阿里)
*/
public void payAliDeposit(){
}
/**
* 押金支付(微信)
*/
public void payWXDeposit(){
}
/**
* 套餐支付(阿里)
*/
public void payAliPackage(){
}
/**
* 套餐支付(微信)
*/
public void payWXPackage(){
}
}复制代码
咱们能够看到不少功能都糅杂在一块儿,一个类作了那么多事情,很臃肿,别提维护,就连找代码都很困难,因此咱们能够对这个UserController进行拆解,与此同时咱们应该分包,好比这个应该在xxx.xxx.userMoudule下面,可能支付相关的有公共的方法,登陆抑或也有公共的方法,那边抽成公共服务去调用。
public class LoginController(){}
public class registerController(){}
public class depositPayController(){
// 支付宝支付
// 微信支付
}
public class packagePayController(){
// 支付宝支付
// 微信支付
}复制代码
整个方案实现的目的就是为了解决高耦合,代码复用率低下的问题。单一职责理解起来不难,可是实际操做须要根据具体业务的糅杂度来切割,实际上很难运用。
2.开闭原则
开闭原则简介
开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则,定义以下:
一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽可能在不修改原有代码的状况下进行扩展。
软件实体包括如下几个部分:
注意:开闭原则是指对扩展开放,对修改关闭,并非说不作任何的修改。
开闭原则的优点
如何使用开闭原则
案例
某公司开发的租车系统有一个押金支付功能,支付方式有支付宝、阿里支付,后期可能还有银联支付、易支付等等,原始的设计方案以下:
// 客户端调用-押金支付选择支付手段
public class DepositPay {
void pay(String type){
if(type.equals("ali")){
AliPay aliPay = new AliPay();
aliPay.pay();
}else if(type.equals("wx")){
WXPay wxPay = new WXPay();
wxPay.pay();
}
}
}
// 支付宝支付
public class AliPay {
public void pay() {
System.out.println("正在使用支付宝支付");
}
}
// 微信支付
public class WXPay{
public void pay() {
System.out.println("正在使用微信支付");
}
}复制代码
在以上代码中,若是须要增长银联支付,如YLPay,那么就必需要修改DepositPay中的pay方法的源代码,增长新的判断逻辑,违反了开闭原则(对修改关闭,对扩展开放,注意这边的银联支付至关于扩展,因此它没有违反规则),因此如今必须重构此代码,让其遵循开闭原则,作法以下:
重构后的图以下所示:
在上图中咱们引入了接口Pay,定义了pay方法,而且DepositPay是针对接口编程,经过setPayMode()由客户端来实例化具体的支付方式,在DepositPay的pay()方法中调用payMode对象来支付。若是须要增长新的支付方式,好比银联支付,只须要让它也实现Pay接口,在配置文件中配置银联支付便可,依赖注入是实现此开闭原则的一种手段,在这里不赘述,源码以下:
public interface Pay {
// 支付
void pay();
}
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("正在使用支付宝支付");
}
}
public class WXPay implements Pay{
@Override
public void pay() {
System.out.println("正在使用微信支付");
}
}
// 客户端调用-押金支付选择支付手段
public class DepositPay {
// 支付方式 (这边能够经过依赖注入的方式来注入)
// 支付方式能够写在配置文件中
// 如今无论你选用何种方式,我都不须要更改
@Autowired
Pay payMode;
void pay(Pay payMode){
payMode.pay();
}
}复制代码
由于配置文件能够直接编辑,且不须要编译,因此通常不认为更改配置文件是更改源码。若是一个系统能作到只须要修改配置文件,无需修改源码,那么复合开闭原则。
3.里氏代换原则
里氏替换原则简介
Barbara Liskov提出:
标准定义:若是对每个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的全部程序P在全部的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
上面的定义可能比较难以理解,简单理解就是全部引用基类(父类的)地方均可以用子类来替换,且程序不会有任何的异常。可是反过来就不行,全部使用子类的地方则不必定能用基类来替代,很简单的例子狗是动物,不能说动物是狗,由于可能还有猫。。。。
里氏替换原则是实现开闭原则的重要方式之一,因为使用基类的全部地方均可以用子类来替换,所以在程序中尽可能使用基类来定义对象,在运行时肯定其子类类型。
里氏替换原则约束
因此咱们在运用里氏替换原则的时候,尽可能把父类设计为抽象类或者接口,让子类继承父类或者实现接口并实如今父类中声明的方法,运行时,子类实例替换父类实例,咱们能够很方便地扩展系统的功能,同时无须修改原有子类的代码,增长新的功能能够经过增长一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
里氏替换原则实战
某租车系统客户分为普通用户(customer)和VIP客户(VIPCustomer),系统须要提供一个根据邮箱重置密码的功能。原始设计图:
在编写重置密码的时候发现,业务逻辑是同样的,存在着大量的重复代码,并且还可能增长新的用户类型,为了减小代码重复性,使用里氏替换原则进行重构:
图上重置密码交由ResetPassword类去处理,只须要传入Customer类便可,无论任何类型的Customer类,只要继承自Customer,均可以使用里氏替换原则进行替换,假若有新的类型,咱们只须要在配置文件中注入新的类型便可。代码以下(简单意会一下):
// 抽象基类
public abstract class Customer {
}
public class CommonCustomer extends Customer{
}
public class VIPCustomer extends Customer{
}
// 重置密码逻辑在这里实现,只须要传入对应的类型便可
public class ResetPassword {
void resetPassword(Customer customer){
}
}复制代码
里氏替换原则是实现开闭原则不可或缺的手段之一,在本例中,经过传递参数使用基类对象,针对抽象编程,从而知足开闭原则。
4.依赖倒转原则
依赖倒转原则简介
依赖倒转原则(Dependency Inversion Principle, DIP):抽象不该该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
能够通俗的定义为两种:
要求咱们在设计程序的时候尽可能使用层次高的抽象层类,即便用接口和抽象类进行变量的声明、参数类型声明、方法返回类型声明以及数据类型转换等等,同时要注意一个具体类应该只实现抽象类或者接口中存在的方法,不要给出多余的方法,这样抽象类将没法调用子类增长的方法.咱们能够经过配置文件来写入具体类,这样一旦程序行为改变,可直接改变配置文件,而不须要更改程序,从新编译,经过依赖倒转原则来知足开闭原则。
在实现依赖倒转原则时,咱们须要针对抽象层编程,而将具体类的对象经过依赖注入(DependencyInjection, DI)的方式注入到其余对象中,依赖注入是指当一个对象要与其余对象发生依赖关系时,经过抽象来注入所依赖的对象。经常使用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入
依赖倒转原则实例
这部分能够参照上面开闭原则案例,能够从那例子中看出,开闭原则,依赖倒转原则,里氏替换原则同时出现了,能够说`开闭原则是咱们要实现的目标,而里氏替换原则是实现手段之一,而同时里氏替换原则又是依赖倒转原则实现的基础,由于加入没有这个理论,依赖倒转原则是不成立的,没法针对抽象编程,要注意这3个原则基本都是同时出现的。Java后端学习交流圈:834962734 面向2-6年Java开发人员,进群可免费获取一份Java架构进阶技术精品视频。(高并发+Spring源码+JVM原理解析+分布式架构+微服务架构+多线程并发原理+BATJ面试宝典)以及架构思惟导图。
5.接口隔离原则
接口隔离原则简介
接口隔离原则的两个定义:
1:使用多个专门的接口,而不使用单一的总接口,即客户端不该该依赖那些它不须要的接口
2:类间的依赖关系应该创建在最小的接口上
接口的含义:
根据接口隔离原则,咱们可明白,每一个接口都应只承担一种相对独立的角色,不干不应干的事情.
实例演示
场景:模拟动物平时的动做,固然也包括人,最初的设计就是一个总接口IAnimal,里面定义动物会有的一些动做。
代码以下:
public interface IAnimal{
/**
* 吃饭
*/
void eat();
/**
* 工做
*/
void work();
/**
* 飞行
*/
void fly();
}
public class Tony implements IAnimal{
@Override
public void eat() {
System.out.println("tony吃");
}
@Override
public void work() {
System.out.println("tony工做");
}
@Override
public void fly() {
System.out.println("tony不会飞");
}
}
public class Bird implements IAnimal{
@Override
public void eat() {
System.out.println("鸟吃");
}
@Override
public void work() {
System.out.println("鸟工做");
}
@Override
public void fly() {
System.out.println("鸟飞");
}
}复制代码
根据上面的写法发现Tony须要实现飞的接口,这很明显不只仅是多余,并且不合理,所以须要经过接口隔离原则进行重构:
/**
* 抽象动物的行为
*/
public interface IAnimal {
/**
* 吃饭
*/
void eat();
/**
* 睡觉
*/
void sleep();
}
/**
* 高级动物人 的行为
*/
public interface IAdvancedAnimalBehavior {
/**
* 打牌
*/
void playCard();
/**
* 骑车
*/
void byBike();
}
/**
* 低级动物的行为
*/
public interface IJuniorAnimalBehavior {
/**
* fly
*/
void fly();
}
/**
* 实现高级动物人的共通方法
*/
public class AbstractAdvancedAnimal implements IAnimal {
@Override
public void eat() {
System.out.println("人吃");
}
@Override
public void sleep() {
System.out.println("人睡");
}
}
/**
* 实现低级动物人的共通方法
*/
public class AbstractJuniorAnimal implements IAnimal {
@Override
public void eat() {
System.out.println("动物吃");
}
@Override
public void sleep() {
System.out.println("动物睡");
}
}
// tony
public class Tony extends AbstractAdvancedAnimal implements IAdvancedAnimalBehavior {
@Override
public void playCard() {
System.out.println("tony打牌");
}
@Override
public void byBike() {
System.out.println("tony骑车");
}
}
// 鸟
public class Bird extends AbstractJuniorAnimal implements IJuniorAnimalBehavior{
@Override
public void fly() {
System.out.println("鸟飞");
}
}复制代码
重构以后,首先定义了一个总的动物接口的大类,而后分别使用了两个抽象类(一个是高级动物,一个是低级动物)分别去实现这些公共的方法,实现中能够抛出异常,代表继承此抽象类的类能够选择性的重写,可不重写。以后再定义了两个行为接口代表高级动物和低级动物所特有的,这样使得接口之间彻底隔离,动物接口再也不糅杂各类各样的角色,固然接口的大小尺度仍是要靠经验来调整,不能过小,会形成接口泛滥,也不能太大,会背离接口隔离原则。
6.合成复用原则
合成复用原则简介
合成复用原则(Composite Reuse Principle, CRP):尽可能使用对象组合,而不是继承来达到复用的目的。
经过合成复用原则来使一些已有的对象使之成为对象的一部分,通常经过组合/聚合关系来实现,而尽可能不要使用继承。由于组合和聚合能够下降类之间的耦合度,而继承会让系统更加复杂,最重要的一点会破坏系统的封装性,由于继承会把基类的实现细节暴露给子类,同时若是基类变化,子类也必须跟着改变,并且耦合度会很高。