图源:
在Java中,接口可能只是特指使用interface
抽象类
在Java中,可以使用abstract
将一个类声明为抽象类,被声明为抽象类的类不能被实例化:
package ch6.abstract1;
abstract class MyClass{
}
public class Main {
public static void main(String[] args) {
// MyClass mc = new MyClass();
// Cannot instantiate the type MyClass
}
}
被注释的代码无法通过编译。
之所以会这么设计,是因为某些类作为基类,只是充当一种“概念”,并不需要真的创建具体实例,比如:
abstract class Tank{
}
class LightTank extends Tank{}
class HeavyTank extends Tank{}
在上面这个继承层次中,Tank
仅仅作为一种基础类型,而不应当真的在代码中初始化,所以这里将其定义为abstract
。
通常抽象类会包含“抽象方法”,这些抽象方法同样会用abstract
进行声明。抽象方法与普通方法的区别在于:只包含返回值和方法签名,而没有方法体。
package ch6.abstract3;
abstract class Tank{
abstract public void move();
abstract public void fire();
}
抽象方法必须被子类重写,除非子类也是个抽象类(一般不会这样做):
package ch6.abstract3;
abstract class Tank {
abstract public void move();
abstract public void fire();
}
class LightTank extends Tank {
public void move() {
System.out.println("Light Tank is moving.");
}
public void fire() {
System.out.println("Light Tank is firing.");
}
}
class HeavyTank extends Tank {
public void move() {
System.out.println("Heavy Tank is moving.");
}
public void fire() {
System.out.println("Heavy Tank is firing.");
}
}
public class Main {
public static void main(String[] args) {
Tank t1 = new HeavyTank();
Tank t2 = new LightTank();
t1.move();
t1.fire();
t2.move();
t2.fire();
// Heavy Tank is moving.
// Heavy Tank is firing.
// Light Tank is moving.
// Light Tank is firing.
}
}
通常你不需要担心你在继承某个抽象类时忘记实现相应的抽象方法,因为IDE会提醒你,甚至帮你创建“骨架代码”。
我们可以利用抽象类和抽象方法的这种特性来应用“模版方法模式”。
假设每辆坦克出厂后都需要经过移动和开火两个步骤来进行试车,那么我们完全可以在抽象类Tank
中添加一个test
方法来完成试车工作,虽然此时需要调用的两个方法move
和fire
都是抽象方法,没有任何实现,但是其抽象方法的特性决定了子类必然会重写这两个方法。换句话说,对于任意的一个继承自Tank
的子类实例,在调用test
时,都会因为多态机制而调用正确的move
和fire
方法完整试车工作。
下面是修改后的代码:
package ch6.abstract3;
abstract class Tank {
abstract public void move();
abstract public void fire();
final public void test() {
this.move();
this.fire();
}
}
...
public class Main {
public static void main(String[] args) {
Tank t1 = new HeavyTank();
Tank t2 = new LightTank();
t1.test();
t2.test();
// Heavy Tank is moving.
// Heavy Tank is firing.
// Light Tank is moving.
// Light Tank is firing.
}
}
这里将test
定义为final
,是因为作为一个模版方法,往往不希望子类进行重写,所以可以考虑声明为final
。
要了解更多的模版方法模式,可以阅读。
接口
就像前边说的,在OOP中接口和抽象类有着类似的定位,但因为具体语言实现的方式不同,使用起来有着一些差异。
在Java中,接口从定义方式而言,要比抽象类简单很多,由interface
关键字定义的接口通常只会包含一组抽象方法:
interface CarTestable{
void move();
void fire();
}
需要注意的是,接口中的方法默认都是抽象方法,所以不需要显式使用abstract
关键字,此外接口的方法都默认是public
访问权限。这是因为接口的特性决定的,因为接口往往是作为一种定义类的“协议”,也就是说实现了某种接口就可以很自然地当做某种接口来调用相应的方法,从这个角度而言接口中定义的方法只能是public
的。
JavaSE8给接口添加了新特性,可以定义静态方法和“默认方法”,这点将在之后进行说明。
在上边的示例中,我将接口命名为CarTestable
而非Tank
是有意为之,在Java中,接口和类的概念是明显不同的,类是一种基本类型,而接口仅仅是一种“能力”和“特性”。所以CartTestable
这个接口代表某种军队中用来测试军车的"能力",任何军用车辆只要能“移动”和"开火",都可以说符合这种能力,可以用来试车,而不仅仅局限于Tank
,所以两者是有很明显的区别的。
顺带一提,因为上面所说的原因,Java中习惯使用XXXable
这样的命名方式来命名接口,比如Runable
。
使用接口同样简单,这里依然沿用之前坦克的例子:
package ch6.interface2;
interface CarTestable {
void move();
void fire();
}
abstract class Tank implements CarTestable {
}
class LightTank extends Tank {
public void move() {
System.out.println("Light Tank is moving.");
}
public void fire() {
System.out.println("Light Tank is firing.");
}
}
class HeavyTank extends Tank {
public void move() {
System.out.println("Heavy Tank is moving.");
}
public void fire() {
System.out.println("Heavy Tank is firing.");
}
}
//装甲车
class ArmouredCar implements CarTestable{
public void move() {
System.out.println("Armoured Car is moving.");
}
public void fire() {
System.out.println("Armoured Car is firing.");
}}
public class Main {
private static void test(CarTestable ct){
ct.move();
ct.fire();
}
public static void main(String[] args) {
CarTestable ct1 = new HeavyTank();
CarTestable ct2 = new LightTank();
CarTestable ct3 = new ArmouredCar();
test(ct1);
test(ct2);
test(ct3);
}
}
上面的例子中没有去除抽象类Tank
,而是让其直接实现CarTestable
接口。可以看出接口和抽象类并非一定是对立的,它们依然可以共存,就像我说的,在Java中,因为代码实现的方式不同,它们在概念上有着明显的差异。
此外,接口明显要比抽象类灵活的多,装甲车明显不是一个坦克,但这并不妨碍它实现CarTestable
接口,并且可以被test(CarTestable ct)
方法接受并进行车辆测试。
上面这个例子中Main
中的test
静态方法显然是为接口CarTestable
专门设计的。如果仅仅会调用一次,这样做并没有什么问题,但如果要调用多次,就很难进行重用。当然我们可以将其放入一个命名为util
包中的某个工具类中,但并不是很合适。理想的情况是可以将其直接与接口CartTestable
进行关联,毕竟没有这个接口也就不会有test
方法。
JavaSE8中添加的接口静态方法可以解决这个问题:
package ch6.interface3;
interface CarTestable {
void move();
void fire();
static void test(CarTestable ct) {
ct.move();
ct.fire();
}
}
...
public class Main {
public static void main(String[] args) {
CarTestable ct1 = new HeavyTank();
CarTestable ct2 = new LightTank();
CarTestable ct3 = new ArmouredCar();
CarTestable.test(ct1);
CarTestable.test(ct2);
CarTestable.test(ct3);
}
}
此外,JavaSE8还给接口添加了一种“默认方法”,使用default
进行声明。“默认方法”与抽象方法不同,可以有方法体,而实现接口的类可以对其进行重写,也可以不重写:
package ch6.interface4;
interface CarTestable {
void move();
default void fire(){
System.out.println("Skip fire test.");
};
static void test(CarTestable ct) {
ct.move();
ct.fire();
}
}
...
class Jeep implements CarTestable{
@Override
public void move() {
System.out.println("Jeep is moving.");
}}
public class Main {
public static void main(String[] args) {
...
CarTestable ct4 = new Jeep();
CarTestable.test(ct4);
// Jeep is moving.
// Skip fire test.
}
}
在上面这个示例中,添加了一个新的类型Jeep
,虽然同样实现了CarTestable
,但一般的用来通勤的吉普车是没有火力的,所以自然无法进行开火测试。所以接口中的fire
方法被修改为“默认方法”,默认情况下会直接跳过开火测试。而Jeep
类只实现了move
方法,没有实现fire
方法,所以最终的效果是进行了移动测试,跳过了开火测试。
这个例子并不一定恰当,仅作为参考。
完全解耦
通过抽象类和接口的使用,可以对系统中的类进行“解耦”,让设计更具扩展性和灵活性。
下面通过示例来进行说明:
package ch6.decouple;
abstract class Sequence {
abstract public Object next();
abstract public boolean hasNext();
public void print() {
while (this.hasNext()) {
System.out.print(this.next() + " ");
}
System.out.println();
}
}
class NumberSequence extends Sequence {
private int[] numbers;
private int cursor;
public NumberSequence(int[] numbers) {
this.numbers = numbers;
}
@Override
public Integer next() {
Integer item = numbers[cursor];
cursor++;
return item;
}
@Override
public boolean hasNext() {
if (cursor >= numbers.length) {
return false;
}
return true;
}
}
class CharSequence extends Sequence {
private char[] chars;
private int cursor;
public CharSequence(char[] chars) {
this.chars = chars;
}
@Override
public Character next() {
Character item = chars[cursor];
cursor++;
return item;
}
@Override
public boolean hasNext() {
if (cursor >= chars.length) {
return false;
}
return true;
}
}
public class Main {
public static void main(String[] args) {
Sequence s1 = new NumberSequence(new int[] { 1, 2, 3 });
Sequence s2 = new CharSequence(new char[]{'a','b','c'});
s1.print();
s2.print();
}
}
这个示例中有一个抽象基类Sequence
代表一种序列类型,而子类NumberSequence
代表数字序列,CharSequence
代表字符序列,序列可以进行遍历,所以基类定义了两个遍历用的抽象方法next
和hasNext
。同时为了能方便地在屏幕上进行打印,提供了一个打印方法print
。
假设我们有一个新的类型NumberGenerator
,这个类可以随机产生一个数字:
class NumberGenerator{
private static Random random = new Random();
public int getNumber(){
return random.nextInt(100);
}
}
我们现在同样想通过类似序列的print
方法一样能简单地获取若干个NumberGenerator
产生的数字并打印,要怎么做呢?
可能有人会试图让NumberGenerator
去继承Sequence
,但这样做是不合适的,因为前者从概念上并不是一个序列,其次前者产生的数字是无限多个,也不适合去实现一个hasNext
这样的方法。更不适合通过类似的方法进行遍历,那样做只会陷入死循环。
换种方式思考,我们这里只是想调用一个“统一”的print
方法,并不关心具体的对象是一个序列还是一个随机数产生器不是吗?所以完全可以将print
方法抽象成一个Printable
接口,让相应的类型实现这个接口即可:
package ch6.decouple3;
import java.util.Random;
interface Printable {
void print();
}
abstract class Sequence implements Printable {
abstract public Object next();
abstract public boolean hasNext();
public void print() {
while (this.hasNext()) {
System.out.print(this.next() + " ");
}
System.out.println();
}
}
...
class NumberGenerator implements Printable {
private static Random random = new Random();
private int printTimes = random.nextInt(10);
public int getNumber() {
return random.nextInt(100);
}
@Override
public void print() {
for (int i = 0; i < printTimes; i++) {
System.out.print(getNumber() + " ");
}
System.out.println();
}
public void setPrintTimes(int times) {
if (times > 0) {
printTimes = times;
}
}
}
public class Main {
public static void main(String[] args) {
Printable p1 = new NumberSequence(new int[] { 1, 2, 3 });
Printable p2 = new CharSequence(new char[] { 'a', 'b', 'c' });
p1.print();
p2.print();
NumberGenerator ng = new NumberGenerator();
ng.setPrintTimes(5);
ng.print();
// 1 2 3
// a b c
// 58 90 17 3 65
}
}
这样做依然有一个不太合适的地方,即为了给NumberGenerator
添加打印功能,我们不得不将其进行了一定程度的修改,为了控制打印次数,还添加上了printTimes
这个属性以及相应的修改器。一般情况下这样做是没什么太大问题的,但有时候可能出于某种原因,你不希望对既有类型做出这样的修改,但同时又想要让既有类型能实现某个接口,这种情况下可以使用适配器模式。
适配器模式简单地讲,就是通过定义一个适配器类,将某个已有类型“适配”为目标类型,这个过程就像是插线板上的适配器插头那样,可以将原本不能直接使用的220V高压交流电变成能直接使用的低压直流电。
更多的适配器模式介绍可以阅读。
使用适配器模式修改后的代码:
...
class NumberGenerator {
private static Random random = new Random();
public int getNumber() {
return random.nextInt(100);
}
}
class NGPrintAdapter implements Printable {
private NumberGenerator ng;
private int printTimes;
public NGPrintAdapter(NumberGenerator ng, int printTimes) {
this.ng = ng;
if (printTimes < 0) {
throw new Error();
}
this.printTimes = printTimes;
}
@Override
public void print() {
for (int i = 0; i < printTimes; i++) {
System.out.print(ng.getNumber() + " ");
}
System.out.println();
}
}
public class Main {
public static void main(String[] args) {
Printable p1 = new NumberSequence(new int[] { 1, 2, 3 });
Printable p2 = new CharSequence(new char[] { 'a', 'b', 'c' });
p1.print();
p2.print();
NumberGenerator ng = new NumberGenerator();
Printable p3 = new NGPrintAdapter(ng, 6);
p3.print();
// 1 2 3
// a b c
// 40 26 82 31 78 6
}
}
可以看到,这样的代码比之前的实现要“干净”很多,并且在NGPrintAdapter
中设置打印次数也更为合理。这样做也更符合设计模式的单一职责原则。
多重继承
“多继承”是一个相当古老的问题,伴随着编程语言的发展史。
具体来说,“多继承”是指一个类可以同时继承多个不同的基类。显然在Java中并不支持这样的做法。
但是在OOP中,多继承显然是必须的,就像之前例子中展示的,一辆轻型坦克在是坦克的基础上,也具备车辆测试的能力,一个数字序列在是一个序列的基础上,也具备打印的能力。
对此不同的编程语言有不同的解决方式,在Python中,是直接对多继承提供支持,并通过“菱形策略”来解决随之而来的“方法冲突”问题。在Go中,因为没有类,只有struct
,并且不同的struct
全部是通过组合的方式来进行复用,所以压根不存在多继承的问题。而PHP的OOP部分是借鉴自Java,与Java几乎一模一样。
Java对此给出的解决方案是一个类只能继承一个基类,但可以实现一个或多个接口。
虽然绝大多数情况下,Java的这种做法可以避免出现多继承的“方法冲突”问题,但是某些极端情况下依然可能会产生类似的问题:
package ch6.multi_ext;
interface Printable1 {
void print();
}
interface Printable2 {
int print();
}
class MyClass implements Printable1, Printable2 {
// Duplicate method print() in type MyClass
@Override
public int print() {
return 0;
}
@Override
public void print() {
}
}
public class Main {
}
上边的代码无法通过编译,会提示“方法重复定义”。这里的原因在于,在中提到过,方法重载是通过方法签名来进行区分的,这里边并不包含返回值类型。道理在于在调用方法的时候,比如a.test()
,并不需要一定接收返回值,所以是没法在调用时通过返回值类型来区分方法的,自然返回值类型就不会作为重载的依据。而重写方法时则要求方法签名与返回值都必须完全相同,至少也要是“协变返回值”。这就导致了上边出现的问题,即两个接口有着方法签名相同,但返回值类型不同的方法,而某个类同时实现这两个接口的时候就会发现,无法同时实现这两个接口的方法,因为只有返回值类型不同是不算做方法重载的,这种情况只会出现“方法重复定义”这样的错误。
但不需要过于担心这样的问题,因为很少会有接口定义类似的方法,且还要同时实现这些接口的情况出现。
扩展接口
在Go中,可以很容易地组合不同的接口。在Java中,同样可以通过类似的方式来扩展接口:
package ch6.ext_if;
import java.util.Arrays;
import java.util.Random;
interface Writer {
int write(char[] content);
}
interface ReaderWriter extends Writer {
int read(char[] content);
}
class CharSequence implements ReaderWriter {
private char[] chars;
public CharSequence(char[] chars) {
this.chars = chars;
}
@Override
public int write(char[] content) {
if (content.length == 0) {
return 0;
}
int counter = 0;
for (int i = 0; i < chars.length; i++) {
if (i >= content.length) {
break;
}
chars[i] = content[i];
counter++;
}
return counter;
}
@Override
public int read(char[] content) {
int counter = 0;
for (int i = 0; i < content.length; i++) {
if (i >= chars.length) {
break;
}
content[i] = chars[i];
counter++;
}
return counter;
}
@Override
public String toString() {
return Arrays.toString(chars);
}
}
public class Main {
private static Random random = new Random();
private static char[] getRandomChars() {
char[] chars = new char[random.nextInt(10) + 1];
for (int i = 0; i < chars.length; i++) {
chars[i] = (char) (random.nextInt(32) + 97);
}
return chars;
}
public static void main(String[] args) {
CharSequence cs = new CharSequence(new char[10]);
char[] chars1 = getRandomChars();
cs.write(chars1);
System.out.println(chars1);
System.out.println(cs);
char[] chars2 = new char[5];
cs.read(chars2);
System.out.println(chars2);
// pyvgtqalyy
// [p, y, v, g, t, q, a, l, y, y]
// pyvgt
}
}
这里采用了Go的风格命名接口,实际上应当命名为
Writeable
这样的名称。
一个接口还可以扩展自多个接口,比如上面的ReaderWriter
接口可以拆分成两个接口:
interface Writer {
int write(char[] content);
}
interface Reader {
int read(char[] content);
}
interface ReaderWriter extends Reader, Writer {
}
...
这样更具灵活性,可以按需要让类实现Reader
接口或Writer
接口,甚至是ReaderWriter
接口。
在同时扩展多个几口时,同样可能出现前面所说的“方法冲突”的问题。
适配接口
在前边【完全解耦】小节中,有说明如何使用适配器模式来更灵活地实现接口,这里再补充一个例子:
package ch6.adapter;
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.Random;
import java.util.Scanner;
class NumberGenerator {
private static Random random = new Random();
public int getNumber() {
return random.nextInt(100);
}
}
class NGReadableAdapter implements Readable {
private int times;
private NumberGenerator ng;
public NGReadableAdapter(NumberGenerator ng, int readTimes) {
this.ng = ng;
if (readTimes < 0) {
throw new Error();
}
this.times = readTimes;
}
@Override
public int read(CharBuffer cb) throws IOException {
if (times > 0) {
String strNum = Integer.toString(ng.getNumber());
cb.append(strNum + " ");
times--;
return strNum.length() + 1;
}
return -1;
}
}
public class Main {
public static void main(String[] args) {
NumberGenerator ng = new NumberGenerator();
Scanner scanner = new Scanner(new NGReadableAdapter(ng, 10));
while (scanner.hasNext()) {
System.out.println(scanner.next());
}
}
}
这里同样使用NumberGenerator
这个类,不过是使用标准库的Scanner
类来对其进行遍历。Scanner
类的构造函数支持多种类型,比如Stream
或File
等,这里使用一个简单的Readable
接口。换言之,所有实现了Readable
接口的类型都可以被Scanner
进行遍历。
Readable
需要实现一个read
方法,其接收一个CharBuffer
类型的参数,CharBuffer
可以看做一个字符缓冲,使用append
方法可以简单地向里边填充字符序列。并需要返回填充的字符个数,如果没有东西可以返回,就返回-1
。需要注意的是,Scanner
类遍历时是按照字符分隔符来遍历的,也就是说会以空白符或者换行来切分字符串,所以为了能让Scanner
正确遍历,需要在填充完一个数字后填充一个空格,作为token
。
最后的遍历代码很简单,使用Scanner
的hasNext
和next
方法即可,Java中很多用于遍历的方法都采用类似的设计。
接口中的字段
《Java编程思想》将这里翻译为“接口中的域”,我认为是不合适的,即使没有看原版,也能猜测原文应当使用的是
field
这个单词,通常会用field
或attribute
来称呼类属性,所以这里应当翻译为“字段”。
虽然不太常见,但接口中的确是可以定义属性(字段)的,接口中的属性默认是public static final
的,所以在JavaSE5引入enum
之前,通常会利用接口来定义枚举值:
package ch6.field;
interface WeekDay {
int MONDAY = 1,
TUESDAY = 2,
WEDNESDAY = 3,
THURSDAY = 4,
FRIADAY = 5,
SATURDAY = 6,
SUNDAY = 7;
}
public class Main {
public static void main(String[] args) {
System.out.println(WeekDay.MONDAY);
}
}
与类不同的是,接口因为不能被实例化,也无法被继承,所以只能拥有静态属性,不能定义非静态属性。此外,接口的静态属性作为默认的final
常量,必须被在定义的同时初始化,也就是说不能定义“空final”形式的属性。这也不难理解,因为接口是没有构造函数的,“空final”形式的属性对接口没有意义。
但是接口的字段并非只能使用常量在编译期初始化,同样可以用某些表达式实现运行时的初始化:
package ch6.field2;
import java.util.Random;
interface WeekDay {
int MONDAY = 1,
TUESDAY = 2,
WEDNESDAY = 3,
THURSDAY = 4,
FRIADAY = 5,
SATURDAY = 6,
SUNDAY = 7;
Random RANDOM = new Random();
int RANDOM_WEEK_DAY = RANDOM.nextInt(7) + 1;
}
public class Main {
public static void main(String[] args) {
System.out.println(WeekDay.MONDAY);
System.out.println(WeekDay.RANDOM_WEEK_DAY);
}
}
嵌套接口
在中提到过,类只有两种访问权限:public
和包访问权限。接口与之类似,同样只能有public
和包访问权限。
其实除了常见的类定义外,还可以在类中定义类,这种方式可以看做是类嵌套,在Java中有个专有名词——“内部类”。与之类似的是,接口也可以进行嵌套,或许我们可以叫它“内部接口”?
好像并没有“内部接口”这样的官方称呼,或许这和接口嵌套并不常见有关。
用一个接口来嵌套另一个接口,仅能构建某种接口的从属关系,就像是给接口又套了一层包一样,和内部类相比似乎没有太大用处:
package ch6.inner;
import java.util.Random;
interface IOInterface {
interface Reader {
int read(char[] content);
}
interface Writer {
int write(char[] content);
}
interface ReaderWriter extends Reader, Writer {
}
}
...
当然这里可以给IOInterface
添加一些额外方法,但似乎没有一定需要这么做的必要。
使用这样嵌套在内部的接口需要使用相应的外部接口名称:
package ch6.inner;
import java.util.Arrays;
import ch6.inner.IOInterface.ReaderWriter;
public class CharSequence implements ReaderWriter {
private char[] chars;
...
}
除了可以在接口中定义接口,还可以在类中定义接口:
...
class IOInterface {
public interface Reader {
int read(char[] content);
}
public interface Writer {
int write(char[] content);
}
public interface ReaderWriter extends Reader, Writer {
}
}
...
和上边接口嵌套接口的例子没有太大差别。需要注意的是类中定义的接口默认是包访问权限,所以需要指定为public
,否则就是包访问权限,这和在接口中定义明显不同(默认是public
的)。
此外,在接口中定义的接口,其访问权限只能是public
的,事实上接口中的所有元素(字段、方法、嵌套接口)都只能是public
的,无论你有没有明确指定。
显然类定义中并没有那么多限制,所以可能出现一些比较奇怪的现象,比如一个private
的内部接口:
package ch6.inner3;
class MyClass {
private interface Printable {
void print();
}
public static void passPrintable(Printable p) {
p.print();
}
public static Printable getPrintable() {
return new Printable() {
@Override
public void print() {
System.out.println("This is a printable test.");
}
};
}
}
public class Main {
public static void main(String[] args) {
// MyClass.Printable p = MyClass.getPrintable();
// The type MyClass.Printable is not visible
// Object o = MyClass.getPrintable();
// MyClass.passPrintable(o);
// The method passPrintable(MyClass.Printable) in the type MyClass is not applicable for the arguments (Object)
MyClass.passPrintable(MyClass.getPrintable());
// This is a printable test.
}
}
Printable
是定义在MyClass
内部的一个接口,它的访问权限是private
的,也就是说只有MyClass
内可以访问。静态方法getPrintable
会返回一个Printable
类型的实例。在测试用的main
函数中,会发现无法通过类似MyClass.Printable p = MyClass.getPrintable();
的语句获取到getPrintable
的返回值,原因是MyClass.Printable
是私有的,无法在main
函数中访问,自然就无法通过编译。虽然可以用Object
句柄来替代MyClass.Printable
承接返回值,毕竟所有类都是Object
的子类。但是如果尝试将获取到的实例o
传递给passPrintable
方法,就会发现并不可行,因为目标方法只能接收一个Printable
接口类型。
这样我们就会陷入类似鸡生蛋蛋生鸡的困境中,但其实可以将getPrintable
的返回值直接传递给passPrintable
方法,比如MyClass.passPrintable(MyClass.getPrintable())
,这样做就可以正常执行。或许这样看上去有点怪异,也想不出有这么做的必要性,但是这的确产生了一种“类A产生的值只能由类A自己处理,外部代码最多只能进行中途传递,而无法正常持有”的奇特效果。
《Thinking in Java》对此的看法是只要语言中存在一种特性,总会有用武之地,我对此持保留态度。
接口与工厂
在OOP设计的系统中,往往需要进行一些类构件工作,比如生产一辆坦克:
package ch6.factory;
class Tank{
public void buildSites(){
System.out.println("Tank sites is build.");
}
public void buildBarbette(){
System.out.println("Tank barbette is build.");
}
public void buildWeaponSystem(){
System.out.println("Tank weapon system is build.");
}
public void ready(){
System.out.println("Tank build work is all over.");
}
}
public class Main {
public static void main(String[] args) {
Tank t = new Tank();
t.buildSites();
t.buildBarbette();
t.buildWeaponSystem();
t.ready();
}
}
Tank
对象创建后,必须按顺序调用一系列方法完成相关的创建工作,只有所有工作执行完毕后,Tank
对象才能交付给客户端代码进行使用。
这种创建对象的方式在OOP设计中相当常见,如果这些“准备”新对象的代码需要重复使用,那么对其进行封装是必然的选择:
package ch6.factory2;
public class Factory {
public static Tank buildTank(){
Tank t = new Tank();
t.buildSites();
t.buildBarbette();
t.buildWeaponSystem();
t.ready();
return t;
}
}
现在我们可以用更简洁的方式创建Tank
实例:
package ch6.factory2;
public class Main {
public static void main(String[] args) {
Tank t = Factory.buildTank();
}
}
这种解决方案在设计模式中被称作“简单工厂”。
在解决实际问题中,往往Tank
是一簇产品,而非简单的单一产品,所以我们需要利用接口或抽象类来创建一个产品簇,相应的,工厂类同样也需要一簇工厂来进行“生产”:
package ch6.factory3;
public class Main {
public static void main(String[] args) {
Factory factory = new HTFactory();
Tank tank1 = factory.buildTank();
factory = new LTFactory();
Tank tank2 = factory.buildTank();
// Heavy Tank sites is build.
// Heavy Tank barbette is build.
// Heavy Tank weapon system is build.
// Heavy Tank build work is all over.
// Light Tank sites is build.
// Light Tank barbette is build.
// Light Tank weapon system is build.
// Light Tank build work is all over.
}
}
篇幅关系,这里只展示测试代码,完整代码见。
需要说明的是,Tank
和Factory
作为底层抽象,使用接口或抽象类实现都是可行的,一般来说,Tank
作为产品来说,使用抽象类更合适,Factory
因为仅包含一个工厂方法,可以定义为接口,也可以定义为抽象类。
更多的工厂模式可以阅读。
谢谢阅读。
文章评论