我们知道软件开发的需求总是不完整的,错误的,容易让人产生误解的,而且需求一直在变化,它主要表现在以下几个方面:用户对需求的看法,可能在于开发人员讨论以及看到软件新的可能性后发生变化;随着对问题的熟悉,开发人员对问题领域的看法也会变化;不是需求在变,而是人们对需求的理解再变化。
我们应该采用何种方法去应对需求变化呢?首先,在方法论层面我们应该采用敏捷开发;其次,在代码层面,使用OOD(Object-Oriented Design,面向对象设计),它的根本原则:面向接口编程;多用组合,而不是继承;发现变化,封装变化。但如何让设计满足这个原则呢?我们的先辈们总结出了5条设计原则,俗称SOLID原则,这就是本期我们要介绍的详细内容。
单一职责原则(SRP,The Single Responsibility Principle)
Robert C. Martin 大师对于单一职责原则有这样一个定义:一个类应该仅有一个引起它变化的原因,而这个引起变化的原因就是职责。那么职责是什么东西呢?先来说说我们人类的职责。作为项目经理,我的职责是:项目计划、需求管理、项目成本控制、项目时间控制等,我需要处理很多事情,同时,这些事紧密相关的。对应到面向对象设计领域,一个类的职责应该包含多个相关的功能。比如说用户控制类中一般包含了新增用户、删除用户、修改用户等相关功能。
开闭原则(OCP,The Open close Principle)
开闭原则就是说对扩展开发,对修改封闭。看到这里,我们不禁会问:新增功能还可以不用修改代码呀? 这是如何做到的呀?实际上开闭原则指的是提供者增加新的功能,而使用者不需要修改代码,并且增加的新功能不能是一个全新的功能,而是原有功能的替代实现。为了帮助大家理解,我举一个具体的例子。目前购物车有添加商品、计算价格两个功能,而商品有香蕉、苹果两种。Java代码如下:
代码语言:javascript复制public interface Item{
public String getName();
public float getPrice();
}
public class Apple implements Item {
public String getName() {
return "Apple";
}
public float getPrice() {
return 5.0f;
}
}
class Orange implements Item{
public String getName() {
return "Orange";
}
public float getunitPrice() {
return 6.0f;
}
}
public class ShopCar {
List<Item> items = new ArrayList<Item>();
public void addItem(Item item) {
items.add(item);
}
public float calculateTotalPrice() {
float total = 0.0f;
for (Item i : items) {
total = i.getPrice();
}
return total;
}
}
我们注意到当新增一个香蕉类,ShopCar类不用修改,这是因为香蕉类是原有功能的替代实现,而不是一个全新的功能,如果购物车中新增商品价格变化这一全新功能,则需要修改ShopCar类。
Liskov替换原则(LSP,The Liskov Substitutuin Principle)
LSP替换原则指的是:子类型能够完全替换父类。它最经典的例子就是长方形和正方形。我们知道正方形是一种特殊的长方形,但是在面向对象设计领域,正方形并不能作为长方形的子类。原因在于设置正方形的长或宽时,同时设置了它的宽或长,那么,正方形的面积等于最后一次设定的长或宽的平方,而不是长乘以宽。具体Java代码如下:
代码语言:javascript复制// 长方形
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width){
this.width = width;
}
public void setHeight(int height){
this.height = height;
}
public int area(){
return width*height;
}
}
// 正方形
public class Squrare extends Rectangle{
public void setWidth(int width){
this.width = width;
this.height = height;
}
public void setHeight(int height){
this.width = width;
this.height = height;
}
}
// 测试类
public class Test{
public static void main(String[] args){
Rectangle rectangle = new Rectangle();
rectangle.setWidth(4);
rectangle.setHeight(5);
// 正确
assert( rectangle.getArea() == 20);
rectangle = new Square();
rectangle.setWidth(4);
rectangle.setHeight(5);
// 错误,此时正方形面积是25
assert( rectangle.getArea() == 20);
}
}
判断一个子类是否满足LSP替换原则,我们可以像上面这个例子那样,在测试类中用子类来替换父类。如果测试能够通过,则说明符合LSP原则,否则就说明不符合LSP原则。
接口隔离原则(ISP,The Interface Segregation Interface)
虽然我们要求软件开发人员都要遵循SRP,但是很多情况下类是没有满足SRP原则的,这个时候就需要用到接口隔离原则。ISP原则指的是客户端不应该被强迫去依赖它们并不需要的接口。也就是说,调用方调用的不需要是一个大而全的接口,而是一个小而精的接口。
依赖倒置原则(DIP,The Dependency Inversion Principle)
DIP包含两层意思:1. 高层模块不应该直接依赖低层模块,两者都应该依赖抽象层;2. 抽象不能依赖细节,细节必须依赖抽象。这里模块是个广义概念,可以是系统、子系统、子模块,甚至是类。而依赖包含多层意思:高层模块调用底层模块的方法,底层模块继承或实现抽象层。