策略模式适用于什么样子的场景呢?
当我们的代码中出现了一连串的if…else…或者是switch…case…语句时,我们的代码体就会很长很臃肿,阅读性大大下降,此时可采用策略模式进行重构。
原理
策略模式利用的是面向对象语言的三个特性,尤其是继承和多态。首先让多个类继承同一个抽象的父类或者是实现一个接口,在抽象类或者接口中声明我们要实现的抽象方法,在子类或者实现类中写出不同的方法实现。根据多态性,创建一个父类类型的引用,让其指向子类的对象,让根据对象的不同,执行各个同名但不同体的方法。
示例讲解
以一个在线租赁商店demo为例:
影片类,具有标题,价格码(对应的是影片的类型):
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title;
private int priceCode;
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
}
租赁单类,包含一部电影和租赁的日期,同时还包含一个计算租赁金的方法:
public class Rental {
private Movie movie;
private int dayRented;
public Rental(Movie movie, int dayRented) {
this.movie = movie;
this.dayRented = dayRented;
}
public Movie getMovie() {
return movie;
}
public int getDayRented() {
return dayRented;
}
public double getAmount(){
double thisAmount = 0d;
switch (movie.getPriceCode()) {
case Movie.REGULAR:
thisAmount += 2;
if (this.getDayRented() > 2) {
thisAmount += (this.getDayRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += this.getDayRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (this.getDayRented() > 3) {
thisAmount += (this.getDayRented() - 3) * 1.5;
}
break;
}
return thisAmount;
}
}
顾客类,包含姓名、租赁的影片、总的租金以及积分点,和一个打印所有租赁信息的方法printStatement:
public class Customer {
private String name;
double totalAmount;
int frequentRenterPoints;
private List<Rental> rentals = new ArrayList<>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental arg) {
rentals.add(arg);
}
public String getName() {
return name;
}
public void printHeader(){
String header = "Rental Record for " + getName() + "\n";
System.out.print(header);
}
public void printBody(){
String body = "";
for (Rental each : this.rentals) {
frequentRenterPoints++;
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDayRented() > 1) {
frequentRenterPoints++;
}
body += "\t" + each.getMovie().getTitle() + "\t" + each.getAmount() + "\n";
totalAmount += each.getAmount();
}
System.out.print(body);
}
public void printTail(){
String tail = "";
tail += "Amount owed is " + totalAmount + "\n";
tail += "You earned " + frequentRenterPoints + " frequent renter points";
System.out.print(tail);
}
public void printStatement(){
printHeader();
printBody();
printTail();
}
}
主类,用于运行和测试:
public class Main {
public static void main(String[] args) {
Movie movie = new Movie("泰坦尼克号",Movie.REGULAR);
Rental rental = new Rental(movie,3);
Customer customer = new Customer("Lily");
customer.addRental(rental);
customer.printStatement();
}
}
运行结果:
Rental Record for Lily
泰坦尼克号 3.5
Amount owed is 3.5
You earned 1 frequent renter points
代码其实不难理解(因为我觉得方法命名还不错…),根据用户租赁的影片打印出用户消费的票据,每部影片的租金根据电影的标签和租赁时长计算,还可以累积积分点。
一眼看过去其实还是有几个比较明显的bad code smell,我们暂且先选择性忽视它们…
Rental类的getAmount()方法属于典型的长方法,又臭又长,大部分的原因是因为这个switch语句:
``
switch (movie.getPriceCode()) {
case Movie.REGULAR:
thisAmount += 2;
if (this.getDayRented() > 2) {
thisAmount += (this.getDayRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += this.getDayRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (this.getDayRented() > 3) {
thisAmount += (this.getDayRented() - 3) * 1.5;
}
break;
}
```
接下来我们尝试消除这段长代码!!!
根据策略模式的思想,我们可以让三个不同的类去继承Rental类,在Rental类中添加一个抽象方法,让各个子类去给出不同的实现,这样就可以取代不同的case。
abstract public class Rental {
private Movie movie;
private int dayRented;
public Rental(Movie movie, int dayRented) {
this.movie = movie;
this.dayRented = dayRented;
}
public Movie getMovie() {
return movie;
}
public int getDayRented() {
return dayRented;
}
public double getAmount(){
double thisAmount = 0d;
return countThisAmount(thisAmount);
}
abstract public double countThisAmount(double thisAmount);
}
之前的判断逻辑,全都被放到子类的继承的抽象方法去实现:
public class NewReleaseRental extends Rental {
public NewReleaseRental(Movie movie, int dayRented) {
super(movie, dayRented);
}
@Override
public double countThisAmount(double thisAmount) {
thisAmount += this.getDayRented() * 3;
return thisAmount;
}
}
public class RegularRental extends Rental {
public RegularRental(Movie movie, int dayRented) {
super(movie, dayRented);
}
@Override
public double countThisAmount(double thisAmount) {
thisAmount += 2;
if (this.getDayRented() > 2) {
thisAmount += (this.getDayRented() - 2) * 1.5;
}
return thisAmount;
}
}
public class ChildrenRental extends Rental{
public ChildrenRental(Movie movie, int dayRented) {
super(movie, dayRented);
}
@Override
public double countThisAmount(double thisAmount) {
thisAmount += 1.5;
if (this.getDayRented() > 3) {
thisAmount += (this.getDayRented() - 3) * 1.5;
}
return thisAmount;
}
}
而原来的长方法,变得非常简洁,大大增强了可读性。
public double getAmount(){
double thisAmount = 0d;
return countThisAmount(thisAmount);
}
适用场景
策略模式正如其名字一样,提供不同的策略,用户只需要决定用哪个算法,算法内部被系统封装。因此,策略模式多用在算法决策系统中,对不同的输入做不同处理:
-
以不同的格式保存文件;
-
以不同的算法压缩文件;
-
以不同的算法截获图象;
总结
策略模式在重构代码中帮助很大,往往能消除长方法,大大提高代码的可读性。同时,经过策略模式重构后的代码更易于扩展,一旦有新的条件出现,直接构建一个新的策略就可以,不会影响其他代码。
关于重构代码的思考
策略模式、模板模式、提取方法等都可以去重构代码,好处在于,更易于后期的维护和继续开发,利于代码重用,代码更具有可读性。但是重构也会带来一些问题,频繁的函数调用会更耗费内存,过度地重构浪费开发时间,说不定还会减少可读性。何解呢?业务需求必定是其一(emmmmmm),还是见仁见智吧,每个人都有自己的理解和行为习惯,只要你觉得ok,你的partners也ok,就ok(o)/~