面试过程中,总会被问到用过什么设计模式,用的最多的就是工厂模式(spring中用到很多)、单例模式(好记还好用),设计模式就是为了将复杂、逻辑不清晰的代码通过设计模式的思想,转化成为简洁优雅的代码。设计模式其实是基于软件七大设计模式演变出来的,软件本身的七大设计原则,今天我们一起分析、探讨。
开闭原则
开闭原则(OPC:Open-Closed Principle)什么是开闭呢?是针对拓展和修改的一个原则,指的是一个软件实体(类、函数、功能模块)可以进行功能拓展,但是不可以修改。强调的是用抽象构建框架,用实现拓展细节,这样可以提高系统的复用性及可维护性。
开闭原则是软件面向对象设计中的最基本设计原则,它指导我们如何建立稳定灵活的软件系统。例如:一个功能模块需求变动,尽可能的保证原有功能不被改变,通过新增功能区解决问题。这样就可以避免影响到之前好使的功能。
实现开闭原则的核心思想就是面向对象编程,举个例子
public interface Car{//创建一个汽车接口
Integer getIid();//主键id
String getBrand();//汽车品牌
BigDecimal getPrice();//汽车价格
}
汽车品牌有Chrysler、Audi、JEEP等,线程创建一个Chrysler的类ChryslerCar
public class ChryslerCar implements Car {
private Integer Iid;
private String brand;
private BigDecimal price;
public ChryslerCar(Integer iid,String brand,BigDecimal price){
this.Iid = iid;
this.brand = brand;
this.price = price;
}
public Integer getIid() {
return this.Iid;
}
public String getBrand() {
return this.brand;
}
public BigDecimal price() {
return this.price;
}
}
假设现在进行促销活动,直接修改ChryslerCar 中的getPrice方法可能会影响到其他应用,所以就需要新建一个ChryslerDiscountCar方法,用来实现打折的价格。
public class ChryslerDiscountCar extends ChryslerCar {//继承ChryslerCar类
public ChryslerDiscountCar(Integer iid, String brand, BigDecimal price) {//实现父类方法
super(iid, brand, price);
}
public BigDecimal getDiscountPrice(){
BigDecimal discount = new BigDecimal(0.5);//折扣。可以改为传参的方式,不写固定值,方便以后业务拓展
return super.getPrice().multiply( discount);
}
public BigDecimal getPrimevalPrice(){//保留一个获取原始价格的方法,
return super.getPrice();
}
}
单一职责原则
单一职责原则(SRP:Simple Responsiblity pinciple )一个类应该只存在一个影响他的原因,即不要存在多于一个导致类发生变更的原因。通俗的说,一个类只负责一项职责,应该仅有一个引起它变化的原因。
假设有一个类实现了两个功能,一旦需求变更需要修改其中一个功能时,很可能会影响到另一个功能,这样的结果不是我们想看到的。所以我们就需要把功能拆分,将两个功能用两个类分别实现,这样就互不影响了。这样不仅仅降低代码耦合度、提供类的可读性(每个类的代码比之前一个类中的代码少了很多,而且看得更清晰)同时提高了系统的可维护性降低需求变更带来的风险。
举个例子:现在的4s店几乎都有vip体系,vip和feivip人去4s店会有不同的人接待,去不同的接待室(和银行的vip窗口类似)这时候新建一个接待室类
public class ReceptionRoom {
public void receptionist(String status){
if("VIP".equals(status)){
System.out.println("vip用户,带去vip接待室");
}else{
System.out.println("非vip用户,带去非vip接待室");
}
}
}
public class SimpleTest {
public static void main(String[] args) {
ReceptionRoom receptionRoom = new ReceptionRoom();
receptionRoom.receptionist("VIP");
receptionRoom.receptionist("CC");
}
}
上面的代码可以看出ReceptionRoom 类承担了两种职责,一个是会员的处理,另一个是非会员的处理。如果现在对于vip用户和非vip用户要提供优惠福利,如果vip和非vip的待遇不一样,那么现在就需要修改代码。修改代码逻辑会相互影响彼此,可能造成不可控的风险。所以我们需要对职责进行解耦,拆分成两个类做处理。分别创建VipPrivilege and NormalPrivilege两个类
public class VipPrivilege {
public void vipDiscount(String status){
System.out.println(status+"5折折扣");
}
}
public class NormalPrivilege {
public void normalDiscount(String status){
System.out.println(status+"8折折扣");
}
}
调用
public class SimpleTest {
public static void main(String[] args) {
VipPrivilege vipPrivilege = new VipPrivilege();
vipPrivilege.vipDiscount("vipname");
NormalPrivilege normalPrivilege = new NormalPrivilege();
normalPrivilege.normalDiscount("susei");
}
}
我们在实际开发中项目会有依赖、聚合,依托于框架开发的过程中,也会有一些不可控的非单一原则代码。我们可以做的就是在编写代码的过程中,尽可能地让接口和方法保持单一职责,这样对于代码的可读性、拓展性和后期系统的维护会有很大的帮助。
依赖倒置原则
依赖倒置原则(DIP:Dependence Inversion Principle)设计代码结构时,高层模块不依赖低层模块,应该依赖于抽象,不能依赖于具体实现。这样的好处是降低代码耦合度,提高代码可读性和可维护性同时提高系统的稳定性,这样会降低修改代码程序所带来的风险。
举个例子
新建一个销售人员类,假设现在他可以去销售克莱斯勒和别克类型的汽车
public class Saler {
public void saleForChrysler(){
System.out.println("销售克莱斯勒汽车!");
}
public void saleForBuick(){
System.out.println("销售别克汽车!");
}
}
.验证一下
public class ForExample {
public static void main(String[] args) {
Saler saler = new Saler();
saler.saleForChrysler();
saler.saleForBuick();
}
}
这个时候如果分配了新的车型,鞠秀英修改代码,会带来相应风险。所以我们根据依赖倒置原则重新设计代码
新建一个品牌接口,只提供一个销售方法
public interface IBrand {
public void sale(); //只有一个销售方法
}
再创建两个类,分别实现品牌接口
public class ChryslerBrand implements IBrand {
public void sale() {
System.out.println("销售克莱斯勒汽车");
}
}
public class BuickeBrand implements IBrand{
public void sale() {
System.out.println("销售别克汽车");
}
}
把之前的销售人员类做些改动
public class Saler {
public void sale(IBrand iBrand){
iBrand.sale();
}
}
验证一下
public class ForExample {
public static void main(String[] args) {
Saler saler = new Saler();
saler.sale(new BuickeBrand());//别克
saler.sale(new ChryslerBrand());//特莱斯勒
}
}
这样改动后,以后不论这个销售人员的销售车型做多少修改,只需要创建新的汽车品牌类就可以,不会改变原有代码。这实际上是我们非常熟悉的方式:依赖注入。依赖注入有构造器方法和setter方法,
构造器注入方式举例:修改saler类
public class Saler {
private IBrand iBrand;
public Saler(IBrand iBrand){
this.iBrand = iBrand;
}
public void sale(){
iBrand.sale();
}
}
验证一下
public class ForExample {
public static void main(String[] args) {
Saler saler = new Saler(new BuickeBrand());
Saler saler1 = new Saler(new ChryslerBrand());
saler.sale();
saler1.sale();
}
}
构造器注入的方式,每次都需要创建实例。如果saler是单例,我们就可以采用setter模式。修改saler类
public class Saler {
private IBrand iBrand;
public void setiBrand(IBrand iBrand){
this.iBrand = iBrand;
}
public void sale(){
iBrand.sale();
}
}
验证一下
public class ForExample {
public static void main(String[] args) {
Saler saler = new Saler();
saler.setiBrand(new BuickeBrand());
saler.sale();
saler.setiBrand(new ChryslerBrand());
saler.sale();
}
}
这一块自己照着敲几遍,一定要自己验证、实践。给大家一些开发建议:以抽象为基准比以细节为基准搭建起来的架构要稳定得多,因此大家在拿到需求之后,要面向接口编程,先顶层再细节来设计代码结构。对代码好好设计一番对于自己能力的提升和后期代码维护是十分重要的。
接口隔离原则
接口隔离原则(ISP:Interface Segregation Principle)类之间如果存在依赖关系,应该通过接口去实现关联,所以在我们设计接口的时候应注意以下几点:
1、一个类对一类的依赖应该建立在最小的接口之上。
2、建立单一接口,不要建立业务过于复杂,代码冗余臃肿的接口。
3、尽量细化接口,接口中的方法尽量少(不是越少越好但一定要适度)
可以看出这个原则最符合我们常说的高内聚低耦合的设计思想,也为我们提了醒,在设计接口的时候要多花时间去思考接口功能的设计,根据业务模型去构思,同时也要对于功能可拓展的地方进行保留设计。接口的好处就是可以规范代码,因为接口中的方法没有具体实现,通过具体实现类去实现接口使得程序拓展性极强。
下面举个例子:
假设现在定义一个IAnimal接口,里面有三个动物的行为eat、sleep、fly
public interface IAnimal {
void eat();
void sleep();
void fly();
}
创建一个bird类
public class Bird implements IAnimal {
public void eat() {
}
public void sleep() {
}
public void fly() {
}
}
创建一个dog类,但是狗不应该有fly(飞翔)的属性
public class Dog implements IAnimal {
public void eat() {
}
public void sleep() {
}
public void fly() {//狗狗一定不会飞,不应该有这个属性
}
}
所以用IAnimal接口是不满足现有需求的,所以应该分别设计IEatAnimal、ISleep、IFlyAnimal接口
public interface IEatAnimal {
void eat();
}
public interface ISleepAnimal {
void sleep();
}
public interface IFlyAnimal {
void fly();
}
这样设计的话,不同动物有什么行为只需要实现对应行为的接口就可以,方便程序功能拓展也易于后期维护。
public class Dog implements IEatAnimal,ISleepAnimal {
public void eat() {
}
public void sleep() {
}
}
里式替换原则
里式替换原则(LSP:Liskov Substitution Principle)一个软件实体如果适用一个父类的话,那一定是适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,我们总结一下:子类可以扩展父类的功能,但不能改变父类原有的功能。
1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2、子类中可以增加自己特有的方法。
3、当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
4、当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。
里式替换原则的好处:
1、防止继承过多,符合开闭原则
2、降低需求变更时带来的风险,提高程序的可维护性、拓展性。
迪米特原则
迪米特原则(LOD:Low Of Demeter)指一个对象应该对其他对象保持最少的了解,所以又叫最少知道原则,就是尽量降低类与类之间的耦合。
迪米特原则主要强调的是类只和朋友交流,不和陌生人说话。体现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类,而出现在方法体内部的类不属于朋友类。
现在设计一个功能:boss查看汽车的销售量。
Boss—>Leader统计---->Leader反馈统计数据
Car类
public class Car{
}
Leader类
public class Leader{
public void checkNumberOfCar(List<Car> carList){
System.out.println("目前销售数量为"+carList.size()):
}
}
Boss类
public class Boss{
public void consultNumber(Leader leader){
//模拟分页
List<Car> carlist = new ArrayList<Car>();
for(int i = 0 ;i < 5 ; i ++){
carlist.add(new Car());
}
leader.checkNumberOfCar(carlist);
}
}
测试一下
public static void main(String[] args){
Boss boss = new Boss();
Leader leader = new Leader();
boss,consultNumber(leader);
}
此时功能基本实现,但是根据迪米特原则来看,boss只需要看统计结果即可, 不需要和car有任何关联,所以改一下
Leader类
public class Leader{
public void checkNumberOfCar(){
List<Car> carlist = new ArrayList<Car>():
for(int i = 0 ;i < 5 ; i++){
carlist.add(new Car());
}
System.out.println("目前销售汽车数量:"+carlist.size());
}
}
Boss类
public class Boss{
public void consultNumber(Leader leader){
leader.checkNumberOfCar;
}
}
现在boss类和car类以及没有关联了,学习软件设计原则重要的就是学以致用,碰到复杂场景要灵活应对。
合成复用原则
合成复用原则(CARP:Composite/Aggregate Reuse Principle)指尽量使用对象组合/聚合,而不是用继承关系达到软件复用的目的。这样可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类的影响相对较小。
继承又叫做白箱复用,相当于把所有实现的细节暴露给子类。组合/聚合也称之为黑箱复用,对类以外的对象是无法知道实现细节的。做代码设计要根据具体的业务场景进行设计,也就是需要遵循oop模型。
举个例子:
创建DBConnection类
public class DBConnection {
public String getConnection(){
return "MySQL 数据库连接";
}
}
创建ProductDao类
public class ProductDao{
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct(){
String conn = dbConnection.getConnection();
System.out.println("使用"+conn+"增加产品");
}
}
这是一种非常典型的合成复用原则应用场景。但是就目前的设计来看,DBConnection太过具体,不是一种抽象,不方便做系统拓展。假设现在系统要做兼容,数据库操作层要支持Oracle数据库,虽然我们可以在DBConnection类中添加对于的Oracle方法,但是这违背开闭原则。其实我们可以这样做:
将DBConnection修改为abstract
public abstract class DBConnection{
public abstract String getConnection();
}
然后抽离MySql
public class MySqlConnection extends DBConnection{
@Override
public String getConnection(){
return "MySql connection";
}
}
在创建Oracle数据库连接
public class OracleConnection extends DBConnection {
@Override
public String getConnection() {
return "Oracle connection";
}
}
总结
学习设计原则是学习设计模式的基础。在实际开发过程中,并不是一定要求所有代码都遵循设计原则,我们要考虑人力、时间、成本、质量,不是刻意追求完美,要在适当的场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更加优雅的代码结构。