gougouzhang 2019-12-13
看看这篇针对Java开发人员的SOLID设计原则简介。抽丝剥茧,细说架构那些事——【优锐课】
当你刚接触软件工程时,这些原理和设计模式不容易理解或习惯。我们都遇到了问题,很难理解SOLID + DP的思想,甚至很难正确实施它们。确实,“为什么要SOLID?”的整个概念,以及如何实施设计模式,这需要时间和大量实践。
我可以说实话,关于SOLID设计模式以及TDD等其他领域,从本质上讲,它们很难教。很难以正确的方式将所有这些知识和信息传授给年轻人。
在本文中,我将以尽可能简单的术语,通过简单易懂的示例来教授SOLID的每个字母。
S代表SRP(单一责任原则)。基本思想是应用关注点分离,这意味着你应尝试将关注点分离到不同的类中。一堂课应该专注于单个问题,逻辑或单个领域。当域,规范或逻辑发生变化时,它只影响一个类。
下面,我们违反了SRP。VehicleServiceResource
类实现了两种不同的方法,并以两种角色结束。如我们所见,该类具有两个标记其用法的注释。
一种是向客户端公开和提供HTTP终结点服务的角色。
第二个是车辆服务的角色,该服务从存储getVehicles()
中获取车辆并计算总值calculateTotalValue()
:
@EndPoint("vehicles") @Service public class VehicleServiceResource { … @GET public List getVehicles(){ } public double calculateTotalValue(){} … }
实现SRP的简单目标是将VehicleServiceResource
分为两个不同的类:一个用于端点,另一个用于服务。
我们要做的是获取VehicleServiceResource
类,并将其分为两个不同的类。
VehicleResource
类仅具有一项和一项工作。为了向客户端公开HTTP资源工具并向其提供服务,所有与业务逻辑相关的方法均导致VehicleService
类。
@EndPoint("vehicles") public class VehicleResource { @Service private VehicleService service; @GET public List getVehicles() { return this.service.getVehicles(); } ... }
我们创建了一个名为 VehicleService
的新类。此类实现所有与车辆有关的逻辑。
@Service public class VehicleService { ... public List getVehciles() {} public double calculateTotalValue(){} ... } SOLID的“O” O代表OCP(开闭原理)。开闭原则指出: " ... 软件实体(例如模块,类,功能等)应打开以进行扩展,但应关闭以进行修改。" 术语“开放扩展”是指我们可以在代码中扩展并包括额外的案例/功能,而不会更改或影响我们现有的实现。 术语“关闭以进行修改”表示在添加了附加功能之后,我们不应修改现有的实现。 一个简单的违反OCP的行为: public class VehicleValueCalculator { // lets assume a simple method to calculate the total value of a vehicle // with extra cost depending the type. public double calculateVehicle(Vehicle v){ double value = 0; if(v instanceof Car){ value = v.getValue() + 2.0; } else if(v instanceof MotorBike) { value = v.getValue() + 0.4; } return value; } }
当我们要包括一种新型车辆“卡车”时,就会违反OCP。需要对calculateVehicle
方法进行重构和代码修改。
public interface IVehicle { double calculateVehicle(); } public class Car implements IVehicle { @Override public double calculateVehicle() { return this.getValue() + 2.0; } } public class MotorBike implements IVehicle { @Override public double calculateVehicle() { return this.getValue() + 0.4; } } 我们的新卡车 public class Truck implements IVehicle { @Override public double calculateVehicle() { return this.getValue() + 3.4; } } 这样,通过使用一种接受IVehicle的方法,以后在每次添加新型车辆时都无需进行重构/代码修改。 范例程式码 public class Main { public static void main(String[] args){ IVehicle car = new Car(); IVhecile motorBike = new MotorBike(); //new addition IVhecile truck = new Truck(); double carValue = getVehicleValue(car); double motorBikeValue = getVehicleValue(motorBike); double truckValue = getVehicleValue(truck); } public double getVehicleValue(IVehicle v) { return v.calculateVehicle(); } }
L代表LSP(Liskov替代原理):
为了使这篇文章成为SOLID的介绍,而不会引起混淆,我将尝试使LSP尽可能简单,并排除很多具体的细节,因为LSP又是另一天的讨论和辩论。
LSP指出,当我们用任何子类型替换父类型时,该软件不应改变期望的结果。
LSP不仅仅是一个设计模式,更是一个问题定义,而我们可以做的是防止不良影响。
为了更清楚地说明这一点,我们将检查以下简单示例:
/** * The Base Rectangle class * This class defines the structure and properties of all types of rectangles */ public class Rectangle { private int width; private int height; public Rectangle(){} public Rectangle(int w,int h) { this.width = w; this.height = h; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getArea() { return this.height * this.width; } /** * LSP violation is case of a Square reference. */ public final static void setDimensions(Rectangle r,int w,int h) { r.setWidth(w); r.setHeight(h); //assert r.getArea() == w * h } }
/** * A Special kind of Rectangle */ public class Square extends Rectangle { @Override public void setHeight(int h){ super.setHeight(h); super.setWidth(h); } @Override public void setWidth(int w) { super.setWidth(w); super.setHeight(w); } }
在谈论LSP时,我们在Rectangle类中有setDimensions方法,该方法接受Rectangle对象的类型并设置宽度和高度。这是违规的,因为行为发生了变化,并且在传递方形引用时我们的数据不一致。
有很多解决方案。其中一些将应用“开放式封闭原则”和通过“合同”模式进行设计。
还有许多其他解决LSP违规问题的方法,但是在此不做解释,因为它不在本文讨论范围之内。
I代表ISP(接口隔离原理)。接口隔离原则是由Robert C. Martin在为Xerox咨询时定义的。他将其定义为:
“不应强迫客户依赖他们不使用的接口。”
ISP指出,我们应该将接口拆分为更小,更具体的接口。
以下是代表两个不同角色的界面示例。一个角色是处理打开和关闭之类的连接,另一个角色是发送和接收数据。
public interface Connection { void open(); void close(); byte[] receive(); void send(byte[] data); }
在应用ISP之后,我们得到了两个不同的接口,每个接口代表一个确切的角色。
public interface Channel { byte[] receive(); void send(byte[] data); } public interface Connection { void open(); void close(); }
D代表DIP(依赖性反转原理)。DIP声明我们应该依赖抽象(接口和抽象类),而不是具体的实现(类)。
接下来是违反DIP。我们有一个Emailer
类,具体取决于直接的SpellChecker
类:
public class Emailer{ private SpellChecker spellChecker; public Emailer(SpellChecker sc) { this.spellChecker = sc; } public void checkEmail() { this.spellChecker.check(); } }
SpellChecker
类:
public class SpellChecker { public void check() throws SpellFormatException { } }
目前可能可以使用,但是过了一会儿,我们要包含两种不同的SpellChecker
实现。我们有默认的SpellChecker
和新greek spellchecker
。
在当前的实现中,需要重构,因为Emailer类仅使用SpellChecker
类。
一个简单的解决方案是为不同的SpellChecker
创建要实现的接口。
// The interface to be implemented by any new spell checker. public interface ISpellChecker { void check() throws SpellFormatException; }
现在,Emailer
类在构造函数上仅接受ISpellChecker
引用。下面,我们将Emailer
类更改为不关心/不依赖于实现(具体的类),而是依赖于接口(ISpellChecker
)
public class Emailer{ private ISpellChecker spellChecker; public Emailer(ISpellChecker sc) { this.spellChecker = sc; } public void checkEmail() { this.spellChecker.check(); } }
我们为ISpellChecker
提供了许多实现:
public class SpellChecker implements ISpellChecker { @Override public void check() throws SpellFormatException { } } public class GreekSpellChecker implements ISpellChecker { @Override public void check() throws SpellFormatException { } }
这是另一个代码示例。无论实现是什么,我们都将ISpellChecker
类型传递给Emailer构造函数。
public static class Main{ public static void main(String[] a) { ISpellChecker defaultChecker = new SpellChecker(); ISpellChecker greekChecker = new GreekSpellChecker(); new Emailer(defaultChecker).checkEmail(); new Emailer(greekChecker).checkEmail(); } }
就是这样!希望你喜欢Java代码中SOLID设计原理的简单概述。