1 概述
在面向对象的编程中,要实现一个功能,可以有非常多的方式。在多年的经验积累总结下来,人们发现优秀的的代码总是遵循一定的范式。其中23种设计模式(Design Patterns),就是前人对优秀代码的编程范式的总结,是面向对象编程的最佳实践。合理地运用这些设计模式,已经是写出高质量,高效率,可读性强,易维护的代码的充要条件。 而设计模式的六大原则,则是设计模式都会遵守的通用法则。本文将结合简单的例子,介绍这六大原则。
2. 六大原则
2.1 依赖倒置原则(Dependency Inversion Principle)
高层模块不应该依赖于底层模块,抽象不应该依赖于细节。因为相对于实现细节的多样性与易变性,抽象类要稳定得多。 换句话说,我们应该针对接口编程。看一个反例:
public class DIViolation {
public static void main(String[] args) {
JavaDeveloper javaDeveloper = new JavaDeveloper();
PythonDeveloper pythonDeveloper = new PythonDeveloper();
Team team = new Team(javaDeveloper, pythonDeveloper);
team.teamWork();
}
}
class JavaDeveloper {
public void work() {
System.out.format("Java developer is working.");
}
}
class PythonDeveloper {
public void work() {
System.out.format("Python developer is working.");
}
}
class Team {
JavaDeveloper javaDeveloper;
PythonDeveloper pythonDeveloper;
Team(JavaDeveloper javaDeveloper, PythonDeveloper pythonDeveloper) {
this.javaDeveloper = javaDeveloper;
this.pythonDeveloper = pythonDeveloper;
}
public void teamWork() {
javaDeveloper.work();
pythonDeveloper.work();
}
}
上述例子中,类Team
和具体类JavaDeveloper
,PythonDeveloper
强耦合在了一起,扩展性极差。想象一下,某一天,PythonDeveloper
离职了,那我们得修改Team
类,把PythonDeveloper
给删除掉。又有一天,来了一位新同事CPPDeveloper
,我们又得修改Team
类,增加CPPDeveloper
。由此看出,依赖具体类的系统稳定性与扩展性是多不好。如果我们改为依赖接口呢?
public class DIObedience {
public static void main(String[] args) {
Collection<Developer> developers = new HashSet<>();
developers.add(new Javaer());
developers.add(new Pythoner());
Team team = new Team(developers);
team.teamWork();
}
}
interface Developer {
void work();
}
class Javaer implements Developer {
public void work() {
System.out.println("Java developer is working.");
}
}
class Pythoner implements Developer {
public void work() {
System.out.format("Python developer is working.");
}
}
class Team {
Collection<Developer> developers;
Team(Collection<Developer> developers) {
this.developers = developers;
}
public void teamWork() {
developers.forEach(Developer::work);
}
}
上述例子中,Team
依赖于顶层接口Developer
,无论组内成员怎么变动,Team
类本身完全不需要修改,可扩展性很强,稳定性也很高。
2.2 里氏替换原则(Liskov Substitution Principle)
也就是一位姓里的女士提出的原则:-)。对象必须保证在不知道基类的具体实现类的情况下可以被使用,简而言之,子类可以替换掉父类出现。对于Java
语言来说,这个原则是不言而喻的。想象一下,当我们以Collection
对象作为方法参数的时候,无论传ArrayList
还是HashSet
,方法都应该能正常工作。为了遵循这条简单的原则,我们在编程中需要做到以下几点
2.2.1 子类与父类的关系一定是is-A,而不是like-A。
看一个反例:
public class WrongExtend {
public static void main(String[] args) {
Fish fish = new Whale();
fish.breath();
}
}
class Fish {
private String name;
Fish(String name) {
this.name = name;
}
public void breath() {
System.out.format("I'm %s, I breath with cheek.");
}
}
class Whale extends Fish {
Whale() {
super("whale");
}
}
输出:
I'm whale, I breath with cheek.
输出了鲸用腮呼吸。原因是鲸不是鱼,但是却错误地继承了Fish
这个父类,所以导致了行为breath
的错误。所以,当子类并不完全是父类的时候,使用父类的方法,可能会导致错误。
2.2.2 子类应该避免重写父类已定义好的方法
下面是反例:
public class WrongOverride {
public static void main(String[] args) {
Rectangle rectangle = new Square();
rectangle.setWidth(3);
rectangle.setLength(5);
System.out.format("expect rectangle area to be %d, actual is %d", 3 * 5, rectangle.getArea());
}
}
class Rectangle {
private int width;
private int length;
public void setWidth(int width) {
this.width = width;
}
public void setLength(int length) {
this.length = length;
}
public int getArea() {
return width * length;
}
}
class Square extends Rectangle {
// set length same as width
[@Override](https://my.oschina.net/u/1162528)
public void setWidth(int width) {
super.setLength(width);
super.setWidth(width);
}
// set width same as length
[@Override](https://my.oschina.net/u/1162528)
public void setLength(int length) {
super.setLength(length);
super.setWidth(length);
}
}
输出:
expect rectangle area to be 15, actual is 25
因为子类Square
重写了父类的setWidth
与setLength
方法,导致了子类与父类的行为不一致,最终输出了与预期不符的结果。所以当子类重写父类方法时,一定不能破坏父类原有的行为。
2.3 接口隔离原则(Interface Segregation Principle)
应该定义多个隔离的接口,而不是一个全面却庞大的接口。子类不应该包含不允许使用的接口。这条规则要求,接口只应包含单一的功能,子类不应包含不必要的功能。 来看一反例:
public class SIViolation {
public static void main(String[] args) {
Person person1 = new Swimmer();
Person person2 = new Driver();
person1.drive();
person2.swim();
}
}
interface Person {
void eat();
void swim();
void drive();
}
class Swimmer implements Person {
[@Override](https://my.oschina.net/u/1162528)
public void eat() {
System.out.println("Swimmer is eating.");
}
[@Override](https://my.oschina.net/u/1162528)
public void swim() {
System.out.println("Swimmer is swimming.");
}
[@Override](https://my.oschina.net/u/1162528)
public void drive() {
System.out.println("Sorry, swimmer can't drive!");
}
}
class Driver implements Person {
@Override
public void eat() {
System.out.println("Driver is eating.");
}
@Override
public void swim() {
System.out.println("Sorry, driver can't swim!");
}
@Override
public void drive() {
System.out.println("Driver is driving.");
}
}
输出:
Sorry, swimmer can't drive!
Sorry, driver can't swim!
这个例子中,因为我们定义了一个大接口Person
,里面包括了不同的功能,导致子类实现的时候,包含了无法使用的方法。子类不仅徒增了不必要的逻辑,而且导致了最终行为的错误:子类并不能完全替代父类出现(父类Person
调用swim
方法的时候,子类就不能是Driver
而只能是Swimmer
),违反了里氏替换原则。正确的做法应该是,把drive
和swim
两个方法分离到不同的接口中,子类只应该包含自己能做到的接口。下面是正例:
public class ISObedience {
public static void main(String[] args) {
swimmable person1 = new Swimmer();
drivable person2 = new Driver();
person1.swim();
person2.drive();
}
}
interface swimmable {
void swim();
}
interface eatable {
void eat();
}
interface drivable {
void drive();
}
class Swimmer implements swimmable, eatable {
@Override
public void eat() {
System.out.println("Swimmer is eating.");
}
@Override
public void swim() {
System.out.println("Swimmer is swimming.");
}
}
class Driver implements drivable, eatable {
@Override
public void eat() {
System.out.println("Driver is eating.");
}
@Override
public void drive() {
System.out.println("Driver is driving.");
}
}
2.4 单一职责原则(Single Responsibility Principle)
导致类变化的原因应该只有一个。意思就是,一个类只做一件事。类的职责越简单,代码可读性越高,工程的可维护性也越强,同时也能降低类之间的耦合度,从而降低修改代码带来的风险。 单一职责原则与接口隔离原则有些类似。上面的那个例子,也是单一职责原则的很好体现:类/接口的功能应该单一。接口隔离原则更偏向于对抽象与接口的约束,单一职责原则更关注具体实现。
2.5 迪米特法则Demeter Principle)
又叫最少知道原则。一个类对其他类应该有最少的了解,并尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。我们需要做到以下两点:
2.5.1. 类只应该暴露公共的方法,能设成private
的方法/属性,就设成private
。
下面是一个反例:
public class WrongPrivilege {
public static void main(String[] args) {
Cook cook = new Cook();
// accidentally consumed a tomato
cook.consumeTomato();
cook.cookSoup();
}
}
class Cook {
private int tomatoNum = 1;
private int eggNum = 1;
public boolean consumeTomato() {
if (--tomatoNum >= 0) {
System.out.format("Consumed a tomato.");
return true;
}
else {
System.out.format("Error: No tomato left!");
return false;
}
}
public boolean consumeEgg() {
if (--eggNum >= 0) {
System.out.format("Consumed an egg.");
return true;
}
else {
System.out.format("Error: No egg left!");
return false;
}
}
public void cookSoup() {
if (consumeTomato() && consumeEgg()) {
System.out.format("Cook soup successfully!");
}
}
}
输出:
Consumed a tomato.
Error: No tomato left!
上述例子中,由于Cook
类暴露了不该暴露的方法consumeTomato
和consumeEgg
,导致内部的数据一致性遭到破坏,于是cookSoup
方法调用失败。而对于调用者(main
方法)来说,Cook
类过多的public
方法也会增加使用难度与使用的错误率,增加学习成本。
2.5.2. 类只应该与直接依赖产生通讯。
只与直接依赖产生关联,可以使类之间的耦合度降到最低。假如有某个模块出现类问题,那么我们只需要修改与之直接相关的模块即可。以下是一个正例:
public class GoodDependency {
public static void main(String[] args) {
Music music = new Music("See you again");
App app = new App(music);
Computer computer = new Computer(app);
computer.openApp();
}
}
class Computer {
App app;
Computer(App app) {
this.app = app;
}
public void openApp() {
app.open();
}
}
class App {
Music music;
App(Music music) {
this.music = music;
}
public void open() {
System.out.format("App is playing %s.", music.getName());
}
}
class Music {
private String name;
Music(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
输出:
App is playing See you again.
上述例子设计良好,模块只依赖于直接相关的模块。
2.1 开闭原则(Open Closed Principle)
系统应该对扩展开放,对修改关闭。
开闭原则可以说是一条总则,是面向对象编程的最高指导法则。上述的所有例子,都可以看到开闭原则的影子。
以上五条原则,目的都是为了提高系统的可扩展性,并极力降低对类原有结构,功能的修改。如果修改了原来的逻辑,那么所有之前正确的功能模块就需要重新测试。而扩展原来的逻辑,则只需要测试新增的逻辑。
开闭原则要求设计者需要有足够的前瞻性。比如考虑把上面例子中App
的功能play music改一下,变成read book,那么上面的代码需要有很大的改动。而设计良好的写法,将会是类似如下:
public class Flexibility {
public static void main(String[] args) {
AppFunction function = new ReadBook();
App app = new App(function);
Computer computer = new Computer(app);
computer.openApp(function);
}
}
class Computer {
Map<AppFunction, App> apps = new HashMap<>();
Computer(Map<AppFunction, App> apps) {
this.apps = apps;
}
Computer(App app) {
this.apps.put(app.getFunction(), app);
}
public void openApp(AppFunction function) {
apps.get(function).open();
}
}
class AppFunction {
private String name;
AppFunction(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class PlayMusic extends AppFunction {
PlayMusic() {
super("play music");
}
}
class ReadBook extends AppFunction {
ReadBook() {
super("read book");
}
}
class App<T extends AppFunction> {
T function;
App(T function) {
this.function = function;
}
public void open() {
System.out.format("App function is %s.", function.getName());
}
public AppFunction getFunction() {
return function;
}
}
输出:
App function is read book.
乍一看,代码量增加了,但其实系统的可扩展性非常高。无论是需要删除App
还是修改App
,唯一需要改动的就是调用者main
方法。如果把依赖关系配置在类似Spring
的xml
文件中,那么唯一需要改的只是xml
配置!如果是新增APP
功能如玩游戏,则只需要新增一个类PlayGame
继承AppFunction
代表功能就可以,避免修改原来的代码。
3 总结
六大原则是所有面向对象编程者的必修课。好好领悟其中的道理,无论对架构设计,还是日常编程,都大有裨益。
来源:oschina
链接:https://my.oschina.net/u/2411391/blog/3195367