1- 策略模式
策略模式定义了一组算法,封装每个算法,并使它们可以互换。策略让算法可以独立于使用它们的客户端而变化。
除了正式的定义,当你需要在代码的不同地方切换算法或策略时,策略模式是一个广泛使用的工具。
简单的例子:
假设你正在构建一个游戏,其中有一个实现饮食类(Diet class)的猫类。但是,让我们假设你的猫体重增加了很多,现在它需要进行减肥以保持健康。
在这种情况下,你可以实现一个策略模式,例如:
interface EatBehavior {
public void eat();
}
实现 eat 行为的饮食类型
public class NormalDiet implements EatBehavior {
@Override
public void eat() {
// normal food
}
}public class LosingWeightDiet implements EatBehavior {
@Override
public void eat() {
// healthy food
}
}
猫类,它HAS-A_ eat 行为_
public abstract class Cat { EatBehavior eatBehavior; public Cat(){}
public void eat() {
eatBehavior.eat();
}
}
客户端代码如下所示:
cat.eat(); // Cat eats as usual.cat.setEatBehavior(new LosingWeightDiet());cat.eat(); // Cat eats with a healthier diet.
策略模式还可以帮助你为不同的子类型分配不同的行为。
假设你有一个无毛猫(一种没有毛的猫),还有一只虎斑猫(或任何其他有毛的猫)。
你可以轻松地更新你的类如下:
interface EatBehavior {
public void eat();
}
interface DisplayBehavior {
public void display();
}
实现 display 行为的毛类型
public class HasFur implements DisplayBehavior {
@Override
public void display() {
// display with fur
}
}public class NoFur implements DisplayBehavior {
@Override
public void display() {
// display without fur
}
}
猫类,它IS-A_ Cat 类型,HAS-A eat 和 display 行为_
public class Sphynx extends Cat { EatBehavior eatBehavior;
DisplayBehavior displayBehavior; public Sphynx() {
eatBehavior = normalDiet();
displayBehavior = noFur();
} public void eat() {
eatBehavior.eat();
} public void display() {
displayBehavior.display();
}
}
另一个猫类,它IS-A_ Cat 类型,HAS-A eat 行为和一个_不同的_ display 行为_
public class Tabby extends Cat {
EatBehavior eatBehavior;
DisplayBehavior displayBehavior; public Tabby() {
eatBehavior = normalDiet();
displayBehavior = hasFur();
} public void eat() {
eatBehavior.eat();
} public void display() {
displayBehavior.display();
}
}
客户端代码如下所示:
Cat spyhnx = new Spyhnx();
sphynx.display(); // displays cat without furCat tabby = new Tabby();
tabby.display(); // displays cat with fur
如上所示,它还可以帮助你分离类的关注点。
如果有一天你想要从虎斑猫中删除毛发,你只需要更改 Tabby 类中的 displayBehavior 设置,客户端不需要被通知此更改。
每个类只关心自己的事情。(单一职责原则)
真实的例子:
假设你正在编写一款显示天气预报的软件,但你想要将用户信息存储在一个数据库中,将天气信息存储在另一个数据库中。
实现数据库策略的不同数据库类型
public class PostgreStrategy implements DatabaseStrategy {
@Override
public void save() {
// save to PostgreSQL
}
}public class CouchbaseStrategy implements DatabaseStrategy {
@Override
public void save() {
// save to Couchbase
}
}
客户端代码:
DatabaseContext db = new DatabaseContext();db.setDatabaseStrategy(new PostgreStrategy());
db.save(userInformation);db.setDatabaseStrategy(new CouchbaseStrategy());
db.save(weatherInformation);
同样,如果有一天你想要更改 PostgreStrategy 或 CouchbaseStrategy 的工作方式,你不需要触及客户端代码。
简而言之,策略模式帮助我们在编写代码时动态地更改组件的工作方式并分离我们的关注点。
阅读单独的故事:策略模式-快速指南 (3分钟阅读)
2- 装饰器模式
装饰器模式可以动态地将附加职责附加到对象上。装饰器提供了一种灵活的替代方案,可以扩展功能,而不必使用子类化。
除了正式的定义,装饰器模式还包装一个装饰器类的对象,允许你在客户端代码中动态地更改对象的行为。简单示例:
假设你拥有一家“按照你的口味制作披萨”的本地披萨店。
它的软件有一个披萨超类,包括披萨子类,如有蘑菇的披萨,有玉米的披萨,有意大利腊肠的披萨,有蘑菇和玉米的披萨,有蘑菇和意大利腊肠的披萨,有玉米和意大利腊肠的披萨,有蘑菇、玉米和意大利腊肠的披萨。
这不是一个很好的实践,对吧?
如果我们将来想添加新的配料怎么办?如果我们想添加新的面饼类型,比如薄饼披萨?如果我们想更新每种配料的价格?如果我们想要再加入两倍的玉米?
在我们的情况下,这种实践将导致维护噩梦。
我们的目标必须是允许我们的类添加新的行为,而不必修改现有的代码。(开闭原则)
编写的代码应该是可重用的。
为了实现这一点,我们可以实现装饰器模式,例如:
public interface Pizza {
String getSummary();
int getPrice();
}
实现Pizza接口的披萨类
public class StandardCrustPizza implements Pizza private static final String PRICE = 25;
@Override
public String getSummary() {
return "Standard Pizza (" + PRICE + ")";
} @Override
public int getPrice() {
return PRICE;
}
}public class ThinCrustPizza implements Pizza { private static final String PRICE = 30;
@Override
public String getSummary() {
return "Thin Crust Pizza (" + PRICE + ")";
} @Override
public int getPrice() {
return PRICE;
}
}
实现Pizza接口的披萨装饰器抽象类
abstract class PizzaDecorator implements Pizza { @Override
public abstract String getSummary();
@Override
public abstract int getPrice();
}
扩展PizzaDecorator抽象类的披萨配料
public class Corn extends PizzaDecorator {
private Pizza pizza;
private static final String PRICE = 4;
Corn(Pizza pizza) {
this.pizza = pizza;
} @Override
public String getSummary() {
return pizza.getSummary() +
", with corn (" + PRICE + ")";
} @Override
public int getPrice() {
return pizza.getPrice() + PRICE;
}
}public class Mushrooms extends PizzaDecorator { private Pizza pizza;
private static final String PRICE = 6; Mushrooms(Pizza pizza) {
this.pizza = pizza;
} @Override
public String getSummary() {
return pizza.getSummary() +
", with mushrooms (" + PRICE + ")";
} @Override
public int getPrice() {
return pizza.getPrice() + PRICE;
}
}public class Salami extends PizzaDecorator {
private Pizza pizza;
private static final String PRICE = 5; Salami(Pizza pizza) {
this.pizza = pizza;
} @Override
public String getSummary() {
return pizza.getSummary() +
", with salami (" + PRICE + ")";
} @Override
public int getPrice() {
return pizza.getPrice() + PRICE;
}
}
客户端代码将是这样的:
Pizza pizza = new StandardPizza(); // standard pizza, base: 25
pizza = new Salami(pizza); // salami added, total: 30
pizza = new Corn(pizza); // corn added, total: 34
pizza = new Corn(pizza); // corn x2 added, total: 38Output:
Standard Pizza (25), with salami (5), with corn (4), with corn (4)
Price: 38Pizza pizza = new ThinCrustPizza(); // thin crust pizza, base: 30
pizza = new Mushroom(pizza); // mushroom added, total: 36
pizza = new Salami(pizza); // salami added, total: 41Output:
Thin Crust Pizza (30), with mushroom (6), with salami (5)
Price: 41
看,这一切都非常可重用!如果有一天你想添加新的面饼/配料或更新当前的价格,你不需要触及客户端代码。
每个类只关心自己的事情。(单一职责原则)
实际例子:
装饰器模式在java.io包中被广泛使用,例如:
InputStream in = new FileInputStream(file);
in = new BufferedInputStream(in);
in = new DataInputStream(in);
FileInputStream是这里的面饼,而BufferedInputStream和DataInputStream是配料。
你甚至可以通过简单地扩展java.io包装饰器的超类(FilterInputStream)并将其包装在基类FileInputStream周围来编写自己的装饰器程序。
public class UpperCaseInputStream extends FilterInputStream {
public UpperCaseInputStream(InputStream in) {
super(in);
} @Override
public int read() throws IOException {
return Character.toUpperCase(super.read());
}
}
客户端代码将是这样的:
InputStream in = new FileInputStream(file);
in = new UpperCaseInputStream(in);// other decorators
简而言之,装饰器模式帮助我们在编写代码时动态地将附加功能附加到我们的基类。
作为单个故事阅读:装饰器模式——快速指南(阅读时间4分钟)
3- 观察者模式
“观察者模式定义了对象之间的一对多依赖关系,因此当一个对象改变状态时,所有依赖它的对象将自动被通知和更新。”
除了正式定义外,观察者模式帮助我们听取(订阅)主题并在发生更改时及时更新。
主题对观察者进行_订阅_,并不知道(也不关心)它们是如何实现的,反之亦然。
简单示例:
假设你正在构建一个社交媒体应用,并将其实现到多个平台上。
一个平台枚举(为简单起见)
public enum Platform {
WEB(1),
MOBILE(2);
}
一个视图接口,具有一个显示方法
public interface View {
void display();
}
一个主题接口,具有添加/删除观察者和通知观察者方法
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
一个观察者接口,具有一个更新方法
public interface Observer {
void update(Double x, Double y);
}
假设你希望在我们的应用程序中显示货币信息。根据传入的数据,你将更新屏幕上的数字。
一个实现了Subject接口方法的货币类
public class CurrencyData implements Subject {
private final List<Observer> observers = new ArrayList<>();
private Double value;
private Double interestRate; @Override
public void registerObserver(Observer observer) {
observers.add(observer);
} @Override
public void removeObserver(Observer observer) {
observers.remove(observer);
} @Override
public void notifyObservers() {
observers.forEach(observer ->
observer.update(value, interestRate));
} public void setValue(Double value) {
this.value = value;
notifyObservers();
} public void setInterestRate(Double interestRate) {
this.interestRate = interestRate;
notifyObservers();
}// getters}
注意,每当发生更改时,都会调用notifyObservers()。
随着你的应用程序变得更大,你可能需要添加新的功能,例如天气信息。
一个实现了Subject接口方法的天气类
public class WeatherData implements Subject {
private final List<Observer> observers = new ArrayList<>();
private Double temperature;
private Double humidity; @Override
public void registerObserver(Observer observer) {
observers.add(observer);
} @Override
public void removeObserver(Observer observer) {
observers.remove(observer);
} @Override
public void notifyObservers() {
observers.forEach(observer ->
observer.update(temperature, humidity));
} public void setTemperature(Double temperature) {
this.temperature = temperature;
notifyObservers();
} public void setHumidity(Double humidity) {
this.humidity = humidity;
notifyObservers();
}// getters}
一个实现了Observer和View接口方法的货币显示类
public class CurrencyDisplay implements Observer, View {
private Platform platform;
private Double value;
private Double interestRate; public CurrencyDisplay(Platform platform, Subject subject) {
this.platform = platform;
subject.registerObserver(this);
} public void unsubscribe(Subject subject) {
subject.removeObserver(this);
} @Override
public void update(Double temperature, Double humidity) {
this.value = temperature;
this.interestRate = humidity;
display();
} @Override
public void display() {
// display
}
}
一个实现了Observer和View接口方法的天气显示类
public class WeatherDisplay implements Observer, View {
private Platform platform;
private Double temperature;
private Double humidity; public WeatherDisplay(Platform platform, Subject subject) {
this.platform = platform;
subject.registerObserver(this);
} public void unsubscribe(Subject subject) {
subject.removeObserver(this);
} @Override
public void update(Double temperature, Double humidity) {
this.temperature = temperature;
this.humidity = humidity;
display();
} @Override
public void display() {
// display
}
}
让我们看看它的运行情况:
CurrencyData currency = new CurrencyData();
WeatherData weather = new WeatherData();CurrencyDisplay webCurrency = new CurrencyDisplay(WEB, currency);
CurrencyDisplay mobileCurrency
= new CurrencyDisplay(MOBILE, currency);
// currency information is available for both platformsWeatherDisplay mobileWeather = new WeatherDisplay(MOBILE, weather);
// weather information is available for mobile,
// not available for webdouble temperature = 21.05;
weather.setTemperature(temperature);
// for weather -> only mobile is notifieddouble interestRate = 14.18;
currency.setInterestRate(interestRate);
// for currency -> both web and mobile got notifiedmobileCurrency.unsubscribe(currency);
// for currency -> mobile is not listening anymoreinterestRate = 15.20;
currency.setInterestRate(interestRate);
// for currency -> only web is notified
在观察者模式中,主题是一个开放的书。观察者可以自由地_订阅_和_取消订阅_根据它的需求。
如果你想取消订阅,这很容易!由于这种设计是松耦合的,你只需调用unsubscribe(subject),就可以完成所有操作!
松耦合设计允许我们构建灵活的面向对象系统,可以处理更改。它们最小化了对象之间的相互依赖。
图片来自 Unsplash,摄影师 Clay Banks
真实案例:
观察者模式 在 javax.swing 包中被广泛使用。
让我们来想象一个服务,它将基于数据的更改更新显示和数据库信息:
UserData data = new UserData();Display webDisplay = new Display(WEB, data);
Display mobileDisplay = new Display(MOBILE, data);Database postgresql = new Postgresql(data);
Database couchbase = new Couchbase(data);// web display, mobile display, postgresql and couchbase
// all subscribed to user data.
这里的按钮是 主题,当单击按钮时,数据会被更改。
button.addActionListener((ActionListener)
ae -> data.setEmail(email));
当数据发生更改时,所有观察者都会收到通知并更新。
想要阅读完整的故事: 观察者模式-快速指南 (阅读时间4分钟)
4- 工厂模式
“工厂模式有3种类型:简单工厂、工厂方法和抽象工厂。”
图片来自 Unsplash,摄影师 Natalie Kuhl
简单工厂
假设你有一家 本地披萨店,并为每个订单实例化 一个披萨对象。
Pizza pizza = new Pizza();
相比之下,你可以简单地这样做:
public class Pizza {
private Pizza(){} // hide the constructor, so no one
initializes pizza with "new" public static Pizza create() {
return new Pizza();
}
}
或者这样做:
public class PizzaFactory {
public static Pizza create() {
return new Pizza();
}
}public class Pizza {
// assuming your models are in the same package, you can
make this package-private to make sure constructor won't be
called from client code
protected Pizza(){}
}
客户端代码如下:
Pizza pizza = Pizza.create();orPizza pizza = PizzaFactory.create();
如果你想更改此方法的工作方式(或者可能添加一些业务逻辑),你可以更新 create 而不是将逻辑代码放入构造函数中。
不要担心,我听到你的声音,你说我们可以通过构造函数实现相同的功能。简单工厂基本上是具有自定义名称的构造函数。
想象一下:
Person person = new Person(170);toPerson person = Person.fromHeight(170);orPerson person = PersonFactory.fromHeight(170);
你可以使用它或不使用它。我个人喜欢它。
有些人甚至不认为简单工厂是设计模式。然而,工厂方法和抽象工厂则有些不同。
接下来让我们看看 工厂方法。
工厂方法
所以…回到订单管理系统。
假设你的业务现在更大了,你开始销售汉堡。
工厂方法通过覆盖方法来帮助你在披萨和汉堡之间进行选择,但将不变部分保留在公共方法中。
让我们首先创建一个实现餐点接口的 Meal。
public interface Meal {
// implementations
}public class Pizza implements Meal {
// implementations
}public class Burger implements Meal {
// implementations
}
让我们创建一个更通用的 MealFactory 接口,PizzaFactory 和 BurgerFactory 都实现了它。
interface MealFactory {
//factory method
Meal create(); // compulsory inheritance
}public class PizzaFactory implements MealFactory {
// implementations @Override
Meal create() {
return new Pizza();
}
}public class BurgerFactory implements MealFactory {
// implementations
@Override
Meal create() {
return new Burger();
}
}
两种披萨类型根据自己的需求重写 create。
现在客户端代码将是这样的:
MealFactory pizzaFactory = new PizzaFactory();
Meal pizza = pizzaFactory.create();MealFactory burgerFactory = new BurgerFactory();
Meal burger = burgerFactory.create();
就是这样!
如果两种餐点都有不考虑类型的公共方法,则可以将 MealFactory 转换为抽象类而不是接口。
abstract class MealFactory {
//factory method
protected abstact Meal create(); // compulsory inheritance public void createAndSendOrder() {
Meal meal = create();
// do stuff
}
}public class PizzaFactory extends MealFactory {
// implementations @Override
protected Meal create() {
return new Pizza();
}
}public class BurgerFactory extends MealFactory {
// implementations
@Override
protected Meal create() {
return new Burger();
}
}
客户端代码将是这样的:
MealFactory pizzaFactory = new PizzaFactory();
pizzaFactory.createAndSendOrder();MealFactory burgerFactory = new BurgerFactory();
burgerFactory.createAndSendOrder();
这很棒,因为我完全将创建逻辑从客户端中封装了起来。
抽象工厂
现在让我们添加披萨和汉堡的素食选项。
所以会是这样的:
interface MealFactory {
Pizza createPizza(); // no inheritance needed
Burger createBurger(); // no inheritance needed
}public class VeganMealFactory implements MealFactory { @Override
public Pizza createPizza() {
return new VeganPizza();
} @Override
public Burger createBurger() {
return new VeganBurger();
}
}public class NonVeganMealFactory implements MealFactory { @Override
public Pizza createPizza() {
return new NonVeganPizza();
} @Override
public Burger createBurger() {
return new NonVeganBurger();
}
}
现在它是一个工厂的工厂。
在工厂方法中,只有一个方法负责创建披萨,在这里我们为不同的披萨有不同的方法,因此整个类都负责。
在工厂方法中,披萨和汉堡需要在类型 Meal 中,而在这里并非必须如此。
让我们看看客户端代码:
MealFactory veganMealFactory = new VeganMealFactory();
MealFactory nonVeganMealFactory = new NonVeganMealFactory();Pizza veganPizza = veganMealFactory.createPizza();
Burger veganBurger = veganMealFactory.createBurger();Pizza nonVeganPizza = nonVeganMealFactory.createPizza();
Burger nonVeganBurger = nonVeganMealFactory.createBurger();
既然 VeganMealFactory 和 NonVeganMealFactory 都是 PizzaFactory 类型,我们不能只使用一个简单的决策者来决定它们之间的区别吗?
让我们试试:
public class MealFactoryDecider { private MealFactoryDecider(){} public static MealFactory decide(MealType mealType) {
switch (mealType) {
case VEGAN:
return new VeganMealFactory();
case NONVEGAN:
return new NonVeganMealFactory();
default: throw new RuntimeException("Invalid type.");
}
}
}
最终客户端代码将是这样的:
MealFactory veganMealFactory =
MealFactoryDecider.decide(VEGAN);MealFactory nonVeganMealFactory = MealFactoryDecider.decide(NONVEGAN);Pizza veganPizza = veganMealFactory.createPizza();
Burger veganBurger = veganMealFactory.createBurger();Pizza nonVeganPizza = nonVeganMealFactory.createPizza();
Burger nonVeganBurger = nonVeganMealFactory.createBurger();
如果觉得多种模式结合使用会更好,那么就记得结合使用吧。
总结
简单工厂 依赖于可读性:虽然它可能不被认为是一种设计模式,但它是一种将客户端与具体类解耦的简单方法。
工厂方法 依赖于继承:对象创建被委派给实现特定工厂方法的子类。
抽象工厂 依赖于组合:对象创建在工厂接口中实现的多个方法中。
所有工厂模式都通过减少应用程序对具体类的依赖来促进松耦合。
想要阅读完整的故事: 工厂模式-快速指南 (阅读时间4分钟)
5- 单例模式
“单例模式确保一个类只有一个实例,并提供对它的全局访问点。”
图片来自 Unsplash,摄影师 davisuko**简单示例:**假设你需要一位木匠来制作沙发、椅子和橱柜。最好的方法是召唤一位木匠,与他一起完成所有工作。如果每个物品都召唤不同的木匠,那就不明智了。
**现实生活中的例子:**这与简单示例非常相似。假设你需要一个数据库客户端,它将处理所有CRUD操作。你不想拥有多个数据库客户端,因为这会导致意外行为,例如内存问题和数据损坏。
实现
public class Singleton {private static Singleton instance;
private Singleton(){} // hiding the constructor
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// other methods
}
客户端代码如下:
Singleton singleton = Singleton.getInstance();
在上面的实现中,逻辑是这样的:如果对象不存在,则创建并返回对象;如果对象存在,则返回已经创建的对象。
解决多线程问题
上面的代码块存在的问题是,在多线程应用程序中,如果在**getInstance()**第一次被使用时,线程同时访问同一个if块,则对象可能会被初始化多次。
让我们继续考虑可能的解决方案:
1. 急切初始化
public class Singleton {private static Singleton instance = new Singleton();
private Singleton(){} // hiding the constructor
public static Singleton getInstance() {
return instance;
}
// other methods
}
使用这种方法,我们将责任传递给JVM,并在类加载时初始化我们的单例实例,因此多线程不是问题。
如果你确定你的应用程序中至少会使用一次getInstance(),那么你可以使用这个方法。
2. 锁定初始化
public class Singleton {private static Singleton instance;
private Singleton(){} // hiding the constructor
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// other methods
}
我们可以使用synchronized关键字锁定getInstance(),并强制其他线程在当前线程退出之前等待进入这个代码块。
虽然上面的例子是正确的(并且在大多数例子中都使用),但这是一个不好的做法。我们不需要每次调用**getInstance()**时都锁定该方法,**我们需要在第一次调用时锁定它。**由于锁定是性能昂贵的操作,我们最好不要锁定整个方法。
public class Singleton {private static volatile Singleton instance;
private Singleton(){} // hiding the constructor
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
// other methods
}
需要考虑的事情
虽然单例模式很棒并且被广泛使用,但有时被认为是一种反模式,因为它可以帮助我们掩盖不良设计。
1. 违反松耦合当我们像**Singleton.getInstance()**这样传递单例类时,我们可能没有意识到我们的应用程序类彼此之间了解得太多,耦合度过高。例如,将数据库客户端传递到我们的应用程序中是一种不好的做法。
2. 违反单一责任原则单例模式既处理确保一个类只有一个实例,又提供了访问它的接入点,这使得它在肩负两个重大责任方面。
3. 单例模式 vs 静态类我们可以使用上面的if检查使单例模式变成惰性初始化,静态方法默认情况下是惰性初始化的。单例允许方法重写,静态方法不允许重写,但可以通过方法隐藏实现类似的行为。
**何时使用静态类:**如果你计划编写帮助方法,如数学、数组操作等,只需使用一个充满静态帮助方法的类即可。
**何时使用单例模式:**其他所有情况。单例类只是遵循正常OOP原则的普通类。
4. 单例模式 vs 依赖注入如果你已经使用处理注入的框架,那么只需注入类而不是应用单例模式可能是一种更好的做法。
我更喜欢先检查它是否是帮助方法,如果是,则创建一个静态类。如果不是,请检查我们的DI可用性。如果是,请使用DI。如果不是,请使用单例模式。
作为一个单一的故事阅读:单例模式-快速指南 (阅读时间4分钟)
6- 建造者模式
“建造者模式通过允许使用相同的构建代码创建该对象的不同表示来帮助我们构造复杂对象。”
建造者模式只是缩减构造函数和许多行的设置器的简单替代方法。
public class Pizza { private int slices;
private boolean cheese;
private boolean mushrooms;public Pizza(int slices) { … }
public Pizza(int slices, boolean cheese) { … }
public Pizza(int slices, boolean cheese, boolean mushrooms) { … }// implement methods}
这是一个缩减构造函数的样子。我们可以进一步扩展它:
public Pizza(int slices, boolean cheese, boolean mushrooms, boolean pepperoni, boolean onions, boolean studentDiscount…) { … }
假设切片是必需的参数,其余参数不是,为了满足所有对象组合,创建不同的构造函数是不明智的。
最简单的方法是创建一个构造函数,它需要切片参数,并根据我们的需要设置其余字段。
public class Pizza {private int slices;
private boolean cheese;
private boolean mushrooms;public Pizza(int slices) { … }}
客户端代码将如下所示:
Pizza pizza = new Pizza(2);pizza.setCheese(true);
pizza.setMushrooms(true);
pizza.setPepperoni(true);
…
这种替代方法看起来也不好。最简单的方法是创建一个生成器类,例如:
public class Pizza {
private int slices;
private boolean cheese;
private boolean mushrooms;
private boolean pepperoni;
private boolean onions;
private boolean studentDiscount; public Pizza(Builder builder) {
this.slices = builder.slices;
this.cheese = builder.cheese;
this.mushrooms = builder.mushrooms;
this.pepperoni = builder.pepperoni;
this.onions = builder.onions;
this.studentDiscount = builder.studentDiscount;
}// getters & setterspublic static final class Builder {
private int slices;
private boolean cheese;
private boolean mushrooms;
private boolean pepperoni;
private boolean onions;
private boolean studentDiscount; private Builder() {} public static Builder initialize(int slices) {
return new Builder().withSlices(slices);
} public Builder withSlices(int slices) {
this.slices = slices;
return this;
} public Builder withCheese() {
this.cheese = true;
return this;
} public Builder withMushrooms() {
this.mushrooms = true;
return this;
} public Builder withPepperoni() {
this.pepperoni = true;
return this;
} public Builder withOnions() {
this.onions = true;
return this;
} public Builder withStudentDiscount() {
this.studentDiscount = true;
return this;
} public Pizza build() {
return new Pizza(this);
}
}
}
由于切片是必需的字段,Builder的私有构造函数是隐藏的,并且具有一个简单的工厂方法,只允许使用切片进行初始化。
直到调用**build()**方法,Builder返回一个Builder类型,**build()**方法将Pizza.Builder转换为实际的Pizza对象。
客户端代码将如下所示:
Pizza pizza = Pizza.Builder
.initialize(2)
.withCheese()
.withMushrooms()
.withStudentDiscount()
.build();
作为一个单一的故事阅读:建造者模式-快速指南 (阅读时间2分钟)
7- 命令模式
“命令模式将请求封装为一个对象,使我们能够将其与不同的请求一起使用并支持撤消操作。”
Photo by Daniel Bradley on Unsplash
命令模式就像在餐厅点餐一样。
厨师不会直接听取客户的订单,而是使用一个助手(比如服务员)。
艺术家:Dmitry Zhart
假设你有一台空调,需要打开/关闭。
通常情况下,你只需要编写一个名为AirConditioner的类,其中包含**turnOn()和turnOff()**方法,但是这一次,我们将在类中实现操作而不是方法。
为此,你可以编写一个名为RemoteControl的辅助类。
现在是这样的:
艺术家:Dmitry Zhart
我们是客户端,遥控器是服务员,空调是厨师。
让我们看看代码:
public class AirConditioner {
private boolean active; void turnOn() {
active = true;
} void turnOff() {
active = false;
}
}
你需要为打开/关闭操作(命令)编写单独的类,例如:
public interface Command {
void execute();
void undo();
}public class AirConditionerOff implements Command {
private final AirConditioner airConditioner; public AirConditionerOff(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
} @Override
public void execute() {
airConditioner.turnOff();
} @Override
public void undo() {
airConditioner.turnOn();
}
}public class AirConditionerOn implements Command {
private final AirConditioner airConditioner; public AirConditionerOn(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
} @Override
public void execute() {
airConditioner.turnOn();
} @Override
public void undo() {
airConditioner.turnOff();
}
}
你需要一个助手(调用者)在我们和空调之间进行通信:
public class RemoteControl {private RemoteControl(){} // hiding the constructor public static void submit(Command command) {
command.execute();
} public static void undo(Command command) {
command.undo();
}
}
客户端代码将如下所示:
AirConditioner airConditioner = new AirConditioner();Command turnOn = new AirConditionerOn(airConditioner);
Command turnOff = new AirConditionerOff(airConditioner);RemoteControl.submit(turnOn); // turned air conditioner on
RemoteControl.undo(turnOn); // turned air conditioner offRemoteControl.submit(turnOn); // turned air conditioner on
RemoteControl.submit(turnOff); // turned air conditioner off
多个命令和堆栈
如果你想创建一个具有多个命令而不仅仅是打开/关闭的应用程序呢?
public class AirConditioner {
private int power; void off() {
power = Power.OFF; // OFF = 0
} void low() {
power = Power.LOW; // LOW = 1
} void medium() {
power = Power.MEDIUM; // MEDIUM = 2
} void high() {
power = Power.HIGH; // HIGH = 3
}
}public interface Command {
void execute();
// undo is gone from here for now, we'll see it below later}public class AirConditionerOff implements Command {
private final AirConditioner airConditioner; public AirConditionerOff(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
} @Override
public void execute() {
airConditioner.off();
}
}public class AirConditionerLowPower implements Command {
private final AirConditioner airConditioner; public AirConditionerLowPower(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
} @Override
public void execute() {
airConditioner.low();
}
}public class AirConditionerMediumPower implements Command {
private final AirConditioner airConditioner; public AirConditionerMediumPower(AirConditioner airConditioner){
this.airConditioner = airConditioner;
} @Override
public void execute() {
airConditioner.medium();
}
}public class AirConditionerHighPower implements Command {
private final AirConditioner airConditioner; public AirConditionerHighPower(AirConditioner airConditioner){
this.airConditioner = airConditioner;
} @Override
public void execute() {
airConditioner.high();
}
}
但是这次撤销操作要复杂一些。因为现在有多个命令,所以需要知道客户端采取的确切步骤。
为了实现这一点,你可以使用堆栈数据结构。
public class RemoteControl { private RemoteControl() {} private static final Stack<Command> undoStack = new Stack<>();
private static final Stack<Command> redoStack = new Stack<>(); public static void submit(Command command) {
command.execute(); undoStack.add(command);
redoStack.clear();
} public static void undo() {
if (!undoStack.isEmpty()) {
undoStack.peek().execute();
redoStack.add(undoStack.pop());
}
} public static void redo() {
if (!redoStack.isEmpty()) {
redoStack.peek().execute();
undoStack.add(redoStack.pop());
}
}
}
现在我们应该能够使用多个命令并支持撤销/重做操作。 :)
让我们看看客户端代码:
AirConditioner airConditioner = new AirConditioner();Command highPower = new AirConditionerHighPower(airConditioner);
Command mediumPower = new AirConditionerMediumPower(airConditioner);
Command lowPower = new AirConditionerLowPower(airConditioner);
Command off = new AirConditionerOff(airConditioner);// feel free to apply any creational design pattern aboveRemoteControl.submit(highPower); // air conditioner set HIGH
RemoteControl.submit(lowPower); // air conditioner set LOW
RemoteControl.submit(mediumPower); // air conditioner set MEDIUMRemoteControl.undo(); // air conditioner set LOW
RemoteControl.undo(); // air conditioner set HIGHRemoteControl.redo(); // air conditioner set LOWRemoteControl.submit(off); // air conditioner set OFF
作为一个单一的故事阅读:命令模式-快速指南(4分钟阅读)
8- 适配器模式
“适配器模式将一个类的接口转换成客户端所期望的另一种接口。适配器使那些由于接口不兼容而不能在一起工作的类可以一起工作。”
照片来自Markus Winkler,在Unsplash上
**简单的例子:**假设你从欧洲旅行到美国,由于使用的插座不同,你需要一个适配器/包装器/转换器,才能使用你的设备。
**现实生活中的例子:**假设你正在编写一个应用程序,并且正在使用Kafka客户端从主题中消费消息。
该客户端有名为consume和produce的方法,用于读取和写入Kafka主题,并且你从版本1.8的代理服务器上听取主题。让我们称之为:Client A.
现在,你需要从另一个代理服务器上听取另一个主题。该代理服务器版本为2.4,但你的Kafka客户端尚不支持此版本。你找到另一个Kafka客户端来帮助你。让我们称之为:Client B.
但是有一个问题:你编写了所有代码都是针对_Client A_编写的,但是_Client B_使用不同的约定。
例如,它使用名称read而不是consume,并使用名称write而不是produce。
你不想将所有代码迁移到_Client B_,因为你听说_Client A_很快就会开始支持更新的版本,并且_Client B_整体上使用的内存比_Client A_多。
由于我们的代码应该是开放扩展的,但关闭修改的,因此最好的做法是不要触及现有代码,只需将_Client A_的代码包装在_Client B_周围,以便与现有逻辑一起工作。
可能有很多其他类似的情况需要使用适配器模式,将类型转换为彼此非常常见。
实现
public class ClientBAdapter extends ClientA {
private final ClientB clientB; public ClientBAdapter(ClientB clientB) {
this.clientB = clientB;
} @Override
public void consume() {
clientB.read();
} @Override
public void produce() {
clientB.write();
}
}
使用这种方法,ClientB可以轻松地像ClientA一样工作。
ClientB clientB = new ClientB();clientB.write();
clientB.read();ClientA clientBAdapted = new ClientBAdapter(clientB);
// feel free to use a static helper method instead of a constructorclientBAdapted.consume();
clientBAdapted.produce();
我们没有更改任何现有代码,只编写了一个辅助类。简单而快速的解决方案。 :)
Facade和Adapter都包装多个类,但Facade的目的是简化,而Adapter的目的是将接口转换为不同的接口。
作为一个单一的故事阅读:适配器模式-快速指南(2分钟阅读)
9- 外观模式
“外观模式提供了一个统一的界面,更易于使用子系统中的接口集。”
```
Photo by Yusuf Evli on Unsplash
**简单示例:**假设你要建造一座房子,需要进行许多步骤。你需要建造墙和屋顶,购买家具,涂漆等等...你可能只是想将墙壁作为独立的功能进行涂漆,但现在我们需要一个全能型的建房包,因为我们现在需要一个完整的房子。
**实际示例:**假设你正在编写一个应用程序,需要进行 HTTP 请求。你需要根据需要使用不同的配置变体进行调整。
最好在一个方法中完成所有配置,并从客户端代码中调用该方法以简化使用。
实现
public class HttpClientFacade {
private final HttpClient httpClient; public HttpClientFacade(HttpClient httpClient) {
this.httpClient = httpClient;
} public JsonObject getRequest(String url, String agentName) {
httpClient.setConnectTimeout(5000);
httpClient.expectCompression(true);
httpClient.setHeader("agentname", agentName);
//...
//...
return httpClient.get(url);
}
}
当你需要在 GET 请求期间添加/删除属性时,你只需要将其实现到这个方法中,这将是一个快速且简单的更改。代码的其他部分不需要了解 getRequest 的细节。
客户端代码如下所示:
HttpClient httpClient = new HttpClient();
HttpClientFacade httpClientFacade =
new HttpClientFacade(httpClient);httpClientFacade.getRequest("emretanriverdi.medium.com", "itsme");
外观模式和适配器模式都包装了多个类,但外观模式的目的是简化,而适配器模式的目的是将接口转换为不同的接口。
阅读完整文章:外观模式-快速指南(阅读时间2分钟)
10- 状态模式
“状态模式允许对象在其内部状态改变时改变其行为。”
美术作品由 Dmitry Zhart 创作
**简单示例:**假设你有一部手机,它有三个来电配置文件:静音模式、震动模式和响铃模式。来电始终相同,但你接收来电的方式总是不同。
**实际示例:**假设你有一个网站,它有首页、关于和联系页面。它的暗模式和浅色模式的内部逻辑是相同的。最好我们可以通过简单的代码块改变可见性,使其保持抽象。
实现
public interface PageViewState {
void view();
}public class ViewStateContext {
private PageViewState currentState; public ViewStateContext() {
currentState = new LightModeState(); // default
} public void setState(PageViewState state) {
currentState = state;
} public void view() {
currentState.view(this);
}
}public class LightModeState implements PageState {
@Override
public void view(ViewStateContext ctx) {
// implementation
}
}public class DarkModeState implements PageState {
@Override
public void view(ViewStateContext ctx) {
// implementation
}
}
客户端代码如下所示:
ViewStateContext stateContext = new ViewStateContext();stateContext.view(); // shows pages in light mode
stateContext.view();stateContext.setState(new DarkModeState());stateContext.view(); // shows pages in dark mode
stateContext.view();
大功告成!
阅读完整文章:状态模式-快速指南(阅读时间2分钟)
11- 责任链模式
“责任链模式让你将请求沿着处理程序链传递。在收到请求后,每个处理程序都决定是处理请求还是将其传递给链中的下一个处理程序。”
照片由 Keith Wong 提供于 Unsplash
**简单示例:**假设你想在计算机上玩视频游戏,游戏尝试根据你的 GPU 选择最佳设置。
它检查你的 GPU 是否可以处理 Ultra,如果不能,则检查你的 GPU 是否可以处理 High,如果不能,则检查你的 GPU 是否可以处理 Medium。如果你很幸运,它会在此停止。
如果你的 GPU 仍然不够,它会检查是否可以处理 Low,然后是 Ultra Low(如果游戏有此选项)。
责任链模式实际上就是这么简单。
实际示例:
到目前为止,你一定意识到这是一堆 if 和 else if 语句。
public void initialize(VisualEntity visualEntity,
FrameRequest frameRequest) {
initializeCommonFields(visualEntity, frameRequest); if (frameRequest.isCircle()) {
// initialize circle
} else if (frameRequest.isSquare()) {
// initialize square
} else if (frameRequest.isRectangle()) {
// initialize rectangle
} else throw new UnsupportedOperationException("message");
}
假设你有一个类似于此代码块的代码块,你让客户端选择他们照片的框架。
我相信我们可以一致认为所有这些 if 块 看起来都不太美观,如果有一天你想将 Triangle 添加到可用框架中,或者可能是情侣之间的 Heart 符号,代码块将变得更大。
因此,让我们尝试使用责任链进行不同的设计!
实现
public abstract class InitializerSelector {
private InitializerSelector successor; abstract void initialize(VisualEntity visualEntity,
FrameRequest frameRequest); public InitializerSelector getSuccessor() {
return successor;
} public void setSuccessor(InitializerSelector successor) {
this.successor = successor;
}
}public class CircleInitializer extends InitializerSelector { public CircleInitializer(SquareInitializer squareInitializer) {
setSuccessor(squareInitializer);
} @Override
public void initialize(VisualEntity visualEntity,
FrameRequest frameRequest) {
if (frameRequest.isCircle()) {
// initialize circle
} else {
getSuccessor().initialize(visualEntity, frameRequest);
}
}
}public class SquareInitializer extends InitializerSelector { public SquareInitializer(RectangleInitializer
rectangleInitializer) {
setSuccessor(rectangleInitializer);
} @Override
public void initialize(VisualEntity visualEntity,
FrameRequest frameRequest) {
if (frameRequest.isSquare()) {
// initialize square
} else {
getSuccessor().initialize(visualEntity, frameRequest);
}
}
}public class RectangleInitializer extends InitializerSelector {
@Override
public void initialize(VisualEntity visualEntity,
FrameRequest frameRequest) {
if (frameRequest.isRectangle()) {
// initialize rectangle
} else {
throw new UnsupportedOperationException("message");
}
}
}
如果有一天你需要添加 TriangleInitializer,则可以将其添加为 RectangleInitializer 的后继者,并将 Exception 移动到 TriangleInitializer。
客户端代码如下所示:
public void initialize(VisualEntity visualEntity,
FrameRequest frameRequest) {
initializeCommonFields(visualEntity, frameRequest);
circleInitializer.initialize(visualEntity, frameRequest);
}
你可以通过将初始调用从 circleInitializer 更改为更抽象的内容来完成客户端代码。你可以创建一个新类,通过调用 CircleInitializer 来启动流程。
public void initialize(VisualEntity visualEntity,
FrameRequest frameRequest) {
initializeCommonFields(visualEntity, frameRequest);
frameInitializer.initialize(visualEntity, frameRequest);
}
有许多其他方法可以实现责任链模式,Spring 有自己的内置方式来更轻松地实现它,根据你的情况(和框架选择)可以使用不同的方法。
阅读完整文章:责任链模式-快速指南(阅读时间3分钟)
12- 模板方法模式
“模板方法模式在一个方法中定义了算法的骨架,将某些步骤推迟到子类中。模板方法允许子类重新定义算法的某些步骤,而不改变算法的结构。”
```
例子:
假设你有一个应用程序可以帮助你将你的应用程序部署到 Staging 或 Production 环境中,像这样:
public class StagingDeploy {
public void deploy() {
runUnitTests();
pushSemanticVersioningTag();
pushTaggedImage();
saveImageToRegistryAsBackup();
} void runUnitTests() {
// implementation for unit tests
} void pushSemanticVersioningTag() {
// implementation for semantic versioning
} void pushTaggedImageToStaging() {
// implementation for pushing tagged image to staging
}
void saveImageToRegistryAsBackup() {
// implementation for saving image as backup
}
}public class ProductionDeploy {
public void deploy() {
runSmokeTests();
pushReleaseTag();
pushTaggedImage();
saveImageToRegistryAsBackup();
} void runSmokeTests() {
// implementation for smoke tests
} void pushReleaseTag() {
// implementation for release versioning
} void pushTaggedImageToProduction() {
// implementation for pushing tagged image to production
} void saveImageToRegistryAsBackup() {
// implementation for saving image as backup
}
}
在 Staging 和 Production 部署之间存在明显的相似之处,因此你可以为它们创建一个模板来使用。
实现
public abstract class Deploy {
public void final deploy() {
runTests();
pushTag();
pushTaggedImage();
saveImage();
} abstract void runTests(); abstract void pushTag(); abstract void pushTaggedImage(); abstract void saveImage();
}
现在你可以像这样扩展它:
public class StagingDeploy extends Deploy { @Override
void runTests() {
// implementation for unit tests
} @Override
void pushTag() {
// implementation for semantic versioning
} @Override
void pushTaggedImage() {
// implementation for pushing tagged image to staging
} @Override
void saveImage() {
// implementation for saving image as backup
}
}public class ProductionDeploy extends Deploy { @Override
void runTests() {
// implementation for smoke tests
} @Override
void pushTag() {
// implementation for release versioning
} @Override
void pushTaggedImage() {
// implementation for pushing tagged image to production
} @Override
void saveImage() {
// implementation for saving image as backup
}
}
客户端代码将会是这样的:
StagingDeploy stagingDeploy = new StagingDeploy();
stagingDeploy.deploy();ProductionDeploy productionDeploy = new ProductionDeploy();
productionDeploy.deploy();
对于这种情况,你可能已经意识到 saveImage() 方法对于两个类来说是相同的。因此,你可以像这样更改代码:
public abstract class Deploy {
public void final deploy() {
runTests();
pushTag();
pushTaggedImage();
saveImage();
} abstract void runTests(); abstract void pushTag(); abstract void pushTaggedImage(); void saveImage() {
// implementation for saving image as backup
}}public class StagingDeploy extends Deploy { @Override
void runTests() {
// implementation for unit tests
} @Override
void pushTag() {
// implementation for semantic versioning
} @Override
void pushTaggedImage() {
// implementation for pushing tagged image to staging
}
}public class ProductionDeploy extends Deploy { @Override
void runTests() {
// implementation for smoke tests
} @Override
void pushTag() {
// implementation for release versioning
} @Override
void pushTaggedImage() {
// implementation for pushing tagged image to production
}
}
钩子
还有另一个叫做钩子的概念。钩子是抽象类中可以被重写的方法,但不一定要重写。
假设你想向我们的模板添加“性能测试”步骤,但你不希望它在 Staging 和 Production 环境中运行。
现在假设你有一个新的环境,关键的应用程序在这里工作,你希望确保部署在这里的应用程序运行速度快于 2ms。
public abstract class Deploy {
public void final deploy() {
runPerformanceTests();
runTests();
pushTag();
pushTaggedImage();
saveImage();
} void runPerformanceTests() {
// empty
} abstract void runTests(); abstract void pushTag(); abstract void pushTaggedImage(); void saveImage() {
// implementation for saving image as backup
}
}public class StagingDeploy extends Deploy { @Override
void runTests() {
// implementation for unit tests
} @Override
void pushTag() {
// implementation for semantic versioning
} @Override
void pushTaggedImage() {
// implementation for pushing tagged image to staging
}
}public class ProductionDeploy extends Deploy { @Override
void runTests() {
// implementation for smoke tests
} @Override
void pushTag() {
// implementation for release versioning
} @Override
void pushTaggedImage() {
// implementation for pushing tagged image to production
}
}public class NewProductionDeploy extends Deploy { @Override
void runPerformanceTests() {
// implementation for performance tests
} @Override
void runTests() {
// implementation for both unit & smoke tests
} @Override
void pushTag() {
// implementation for release versioning
} @Override
void pushTaggedImage() {
// implementation for pushing tagged image to new production
}
}
钩子: 你可以重写方法以添加行为,但你不一定要这样做。**抽象方法:**你必须重写该方法。
需要考虑的事情
虽然模板方法模式非常好并且被广泛使用(有时甚至不自知),但在使用之前我们应该三思:
1. 始终优先使用组合而非继承。 在使用模板方法模式之前,检查策略模式是否更合适。模板方法模式在类级别上工作,因此它有点更静态。策略模式在对象级别上工作,允许你在运行时切换行为。
2. 要知道可能会违反里氏替换原则。 LSP基本上是说,超类的对象应该可以用其子类的对象替换而不会破坏应用程序。虽然听起来很容易,但随着应用程序在未来变得更大,我们可能会无意中修改模板以使其不适用于所有子类。
3. 要知道可能会违反开闭原则。 OCD基本上是说,类应该对扩展开放,但对修改关闭。随着类变得越来越大,为每个类维护模板变得困难,而不是类自适应模板,情况开始逆转,模板开始自适应类。
阅读完整文章:模板方法模式 - 简明指南 (阅读时间 4 分钟)
感谢你的阅读!
参考资料:
- 《Head First 设计模式》Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra 著。O'Reilly Media, Inc.出版。
- 《深入理解设计模式》Alexander Shvets 著。2019 年出版。
译自:https://betterprogramming.pub/design-patterns-a-complete-guide-b2699315961f
评论(0)