功能性Java中的现代策略模式
本文介绍了如何在函数式Java中使用策略模式,并加入枚举和函数式句法糖。
有一种思考设计模式的方式让我印象深刻。就像Venkat在2019年Devoxx演讲的开头所说的,它们很像奶奶的食谱。我们都喜欢我们的祖母为我们做饭。但要尽量问清楚菜谱--面粉或糖的使用量从来都不精确。而当你自己准备食物时,出来的效果也完全不同。
在我们Evojam目前最大的Java项目中,我们对策略模式的食谱进行了调整。我们加入了我们的个人风格,加入了一些枚举和功能语法糖。像往常一样,解释它的最简单方法是用一个例子。让我们直接开始吧。
设置场景
当你拍摄照片时,曝光取决于三个值。如果你随意挑选它们,结果很可能比你预期的更亮或更模糊。幸运的是,即使是古老的模拟相机也允许你使用全手动以外的模式。下面这个类代表了你在拍照前需要的所有控制。
@lombok.Value
class CameraControls {
Mode mode;
FilmSpeed filmSpeed;
Aperture aperture;
Shutter shutter;
}
在模拟摄影中,速度(也叫ISO)取决于你使用的胶卷。然后你的相机要么读取它,要么需要你来设置它。你在插入一个新的胶卷后做一次。
这就给你留下了光圈和快门。在每种模式下,我们对它们的处理方式不同。
enum Mode {
MANUAL, APERTURE_PRIORITY
}
当在手动模式下,光圈和快门速度都是用户要求的。如果照片出来太暗或太亮,这不是相机的错。光圈优先则不同。顾名思义,它使用用户设置的光圈来选择正确的快门速度,考虑到特定的胶片速度。这是一个平衡的行为,最好的说明就是通常所说的 "曝光三角"。
相机在光圈优先模式下使用测光表来选择正确的快门速度。在一个晴朗的日子里,三角形的理想区域与在你的客厅里是不同的。这就是为什么你需要为每个新场景调整控制以获得正确的曝光。
好吧,如果你不了解摄影,这一切听起来可能有点令人难以接受。不过,这并不复杂。每个控件都有一组有限的值。
enum FilmSpeed {
_100, _200, _400, _800
}
enum Aperture {
F2, F4, F8, F16
}
enum Shutter {
_60, _250, _500, _1000
}
当你按下快门时,胶卷的速度已经固定。这就使我们只有两个值可以传递给shoot()方法。
Java
void shoot(Aperture aperture, Shutter shutter) {...}
保持简单
正如我之前所说,模式会影响光圈和快门值的处理。在手册中,相机认为来自用户控制的值是理所当然的。这正是下面第一个如果块里面发生的事情。
光圈优先意味着你要使用控制中传递的光圈值。对于快门速度,你参考了测光表。你向它提供胶片速度和所需的光圈。作为回报,你会得到一个基于测光的快门速度建议。你可以在第二个if块中遵循这个逻辑。
Java
void shutterRelease(CameraControls controls, LightMeter meter) {
if (controls.getMode() == Mode.MANUAL) {
shoot(controls.getAperture(), controls.getShutter());
} else if (controls.getMode() == Mode.APERTURE_PRIORITY) {
Shutter shutter = meter.pickShutter(
controls.getFilmSpeed(),
controls.getAperture()
);
shoot(controls.getAperture(), shutter);
}
}
一步之遥
俗话说,软件中唯一不变的东西就是变化。没有,一个Java方法里面的if语句可能会长期保持不变。更不用说一个方法里面有两个if语句了。
在我们的案例中,一个可能的变化要求可能是处理一个新的模式。快门优先是光圈优先的反面。让我们把它添加到原来的枚举中。
Java
enum Mode {
MANUAL, APERTURE_PRIORITY, SHUTTER_PRIORITY
}
只添加第三个值是非常幼稚的,但代码可以编译。在我们最初的设计中,没有新的模式出现。当摄影师选择快门优先并按下快门时,什么也不会发生。为了处理新模式,你需要添加另一个if块。
不过,一定有更好的方法来处理这个问题。有的--它的名字叫 策略模式。
首先要做的是定义挑选快门和光圈值的接口。你可以使用一个通用类型T来表示快门或光圈。
Java
interface Picker<T> {
T pick(CameraControls settings, LightMeter meter);
}
模式在挑选光圈和快门的方式上有所不同。用Picker策略对模式进行参数化似乎很自然。在枚举中引入私有的最终字段使它们成为强制性的和不可改变的。这正是你对每个现有模式以及未来的任何新模式的需要。
Java
@lombok.Getter
@lombok.RequiredArgsConstructor
enum Mode {
MANUAL(),
APERTURE_PRIORITY(),
SHUTTER_PRIORITY();
private final Picker<Aperture> aperturePicker;
private final Picker<Shutter> shutterPicker;
}
为了使上面的代码能够编译,你还需要给每个构造函数传递两个参数。Picker是一个功能接口,因为它只有一个方法。你可以用简单的Java lambdas来实现它。
Java
@lombok.Getter
@lombok.RequiredArgsConstructor
enum Mode {
MANUAL(
(controls, meter) -> controls.getAperture(),
(controls, meter) -> controls.getShutter()
),
...
}
最后的修饰
仔细想想,我们最终可能会重复自己。手动和光圈优先都是从用户控制中获取光圈。手动和快门优先都是从用户控制中获取快门速度。牢记DRY原则,让我们把这些lambdas提取为静态方法。
Java
interface Picker<T> {
T pick(CameraControls settings, LightMeter meter);
static Aperture apertureFixed(CameraControls controls,
LightMeter meter) {
return controls.getAperture();
}
static Shutter pickShutter(CameraControls controls,
LightMeter meter) {
return meter.pickShutter(
controls.getFilmSpeed(),
controls.getAperture()
);
}
...
}
你必须承认,最终的结果是一段漂亮的干净的代码。
Java
@lombok.Getter
@lombok.RequiredArgsConstructor
enum Mode {
MANUAL(Picker::apertureFixed, Picker::shutterFixed),
APERTURE_PRIORITY(Picker::apertureFixed, Picker::pickShutter),
SHUTTER_PRIORITY(Picker::pickAperture, Picker::shutterFixed);
private final Picker<Aperture> aperturePicker;
private final Picker<Shutter> shutterPicker;
}
首先,它是不言自明的,这要归功于对方法和字段名称的精心选择。其次,它不会让你未来的自己在不处理两个选取器的情况下引入一个新模式。最后,它适合在代码的其他部分优雅地使用,完全没有if语句。
Java
@lombok.Value
class CameraControls {
Mode mode;
Speed speed;
Aperture aperture;
Shutter shutter;
Aperture pickAperture(LightMeter meter) {
return mode.getAperturePicker().pick(this, meter);
}
Shutter pickShutter(LightMeter meter) {
return mode.getShutterPicker().pick(this, meter);
}
}
void shutterRelease(CameraControls controls, LightMeter meter) {
Aperture aperture = controls.pickAperture(meter);
Shutter shutter = controls.pickShutter(meter);
shoot(aperture, shutter);
}
请确保你将这段代码与早先有两个if语句的shutterRelease()进行比较。哪一个更容易阅读?记住,在那个时候,我们甚至没有处理第三种模式。对于另一种模式,我们还需要另一个if块。
保持SOLID
策略模式使我们的实现在不止一个方面是 SOLID的。改变模式枚举的唯一原因是为了处理一个新的选择。在不修改shutterRelease()的情况下,用新的模式扩展这个枚举是很容易的。
继续,自己去寻找所有五个SOLID原则的证明。请把它们放在下面的评论区。一旦你这样做了,你应该开始注意到你当前项目中的完美用例。