1. 目的

对于某个具体的任务来说,如果要你编码实现它所要求的功能,不同的人会给不同的实现方式。可能写代码的人觉得自己的代码没有问题,但代码的重用性、可扩展性和可维护性等特性可能就很差。

因此,为了使软件可以很好的应对耦合性、内聚性、可维护性、重用性、灵活性和可阅读性等多方面的挑战,我们就需要使得编写的软件遵从某些原则和要求,而这就是设计模式需要完成的事情。

设计模型的目的是使得软件在上述的各种特性上具有更好的表现,具体来说:

  • 重用性:相同的功能,不必重写编写;相似的功能,只需修改很少的部分
  • 可读性:即编程的规范性,所写代码应该容易被其他人阅读和理解
  • 可扩展性:可以很方法的在原有基础上添加或删除某些功能
  • 可靠性:对软件某些部分的更新不会影响整体的性能
  • 高内聚低耦合

2. 分类

常用的设计模型总共有23种,它们大致可以分为创建型模式(Creational Patterns)、结构型模式(Structural Pattern)和行为型模式(Behavioral Pattern)。
设计模式(Design_Pattern).png

3. 原则

在具体学习设计模式之前,我们需要先了解每种设计模式都需要遵循的原则。总的来说,设计模式中常用的七大原则如下所示:

  • 单一职责原则
  • 接口隔离原则
  • 依赖倒转原则
  • 里氏替换原则
  • 开闭原则
  • 迪米特法则
  • 合成复用原则

3.1单一职责原则

单一职责原则(Single Responsibility Principle, SRP)指出:软件模块应该只有一个被修改的理由。简单来说,即一个类应该只负责一项职责。如果每个模块负责的职责有多个,那么修改该模块的原因可能就有很多,从而导致在修改该模块的同时还要考虑他它的修改是否会影响其他模块的正常运行。

其中类的职责可以分为两类:

  • 数据职责:通过成员变量表示
  • 行为职责:通过成员方法表示

单一职责原则的根本目的在于实现高内聚、低耦合。下面我们通过一个例子来感受一下单一职责原则指的是什么,以及为什么需要遵从。假设此时定义一个Person类,如下所示:

  1. public class Person {
  2. private String name;
  3. public Person(String name) {
  4. this.name = name;
  5. }
  6. public void run(String subject){
  7. System.out.println(this.name + " is studying " + subject + " ...");
  8. }
  9. }

类中只有一个run(),方法接收一个字符串,输出信息指示某类人正在学习什么。例如:

  1. public class Demo {
  2. public static void main(String[] args) {
  3. Person p = new Person("Student");
  4. p.run("CS");
  5. }
  6. }

输出为Student is studying CS ...。如果构造函数中传入Teacher、Farmer、Police等任何职业,输出永远表示的是在学习某个课,这显然是不太合理。究其原因是因为run()负责的工作太多了,更好的方式是为不同的职业分别创建不同的类,然后在各个类中分别创建自己的run()

  1. public class Student {
  2. public void run(String subject){
  3. System.out.println("Student is studying " + subject + " ...");
  4. }
  5. }
  1. public class Teacher {
  2. public void run(String subject){
  3. System.out.println("Teacher is teaching " + subject + " ...");
  4. }
  5. }

分别创建了Student类和Teacher类之后,在使用时只需要根据具体的职业实例化相应的类对象,最后使用run()即可。

  1. public class Demo {
  2. public static void main(String[] args) {
  3. new Student().run("CS");
  4. new Teacher().run("CS");
  5. }
  6. }

此时的输出为:

  1. Student is studying CS ...
  2. Teacher is teaching CS ...

这样就在类级别上遵从了单一职责原则。从另一个角度出发,另一个选择是应该为不同的职业创建不同的方法,从而在方法级别上遵从单一职责原则。

  1. public class Person {
  2. private String name;
  3. public Person(String name) {
  4. this.name = name;
  5. }
  6. public void study(String subject){
  7. System.out.println(this.name + " is studying " + subject + " ...");
  8. }
  9. public void teach(String subject){
  10. System.out.println(this.name + " is teaching " + subject + " ...");
  11. }
  12. }

然后在使用时根据不同的职业调用不同的方法:

  1. public class Demo {
  2. public static void main(String[] args) {
  3. new Person("Student").study("CS");
  4. new Person("Teacher").teach("CS");
  5. }
  6. }

此时输出为:

  1. Student is studying CS ...
  2. Teacher is teaching CS ...

将上面提到的类级别和方法级别上遵从单一原则表示到UML类图上,如下所示:
单一职责原则.png

总结:通过遵从单一职责原则,程序可以降低类的复杂度,使得一个类只负责一项职责。从而提高了类的可读性和可扩展性。虽然上面的例子中从类和方法两个级别分别遵从单一职责原则,但实际中只有在类中方法足够少,方法逻辑足够简单才使用方法上的单一职责。否则,尽量都在类级别上遵从单一职责原则。

3.2 接口隔离原则

客户端不应该依赖于它所不需要的接口。

接口隔离原则(Interface Segregation Principle, ISP)指的是一旦一个接口中的方法过多,就需要将其划分为一些更细小的接口,从而使得接口的实现类(客户端)只知道它所想使用的方法即可。简单来说就是,每个接口应该只代表一类角色,它只包含所代表的角色所需的方法,而不应该包含此外其他方法。此外满足接口隔离原则时,接口首先必须满足职责单一原则,在满足高内聚的同时,接口中的方法应该尽量少。

例如,现在有一个ICar接口,接口中包含方法sell()repair()drive()。如果此时创建接口的实现类Seller,那么就需要实现接口中全部的三个方法,而显然ISeller并不关心repair()drive()。同理,如果现在创建实现类IDriver,它并不关心sell()repair()。此时,程序就不满足接口隔离原则。为了使程序满足接口隔离原则,应该将接口进行更细的划分为ISellable、IRepairable和IDrivable,三个接口分别包含sell()repair()drive()

  1. interface ISellable{
  2. void sell();
  3. }
  4. interface IRepairable{
  5. void repair();
  6. }
  7. interface IDrivable{
  8. void drive();
  9. }
  10. class Seller implements ISellable{
  11. @Override
  12. public void sell() {
  13. System.out.println("Selling ...");
  14. }
  15. }
  16. class Mechanic implements IRepairable{
  17. @Override
  18. public void repair() {
  19. System.out.println("repairing ...");
  20. }
  21. }
  22. class Driver implements IDrivable{
  23. @Override
  24. public void drive() {
  25. System.out.println("driving ...");
  26. }
  27. }
  28. public class Demo {
  29. public static void main(String[] args) {
  30. new Seller().sell();
  31. new Mechanic().repair();
  32. new Driver().drive();
  33. }
  34. }

此时程序的输出为:

  1. Selling ...
  2. repairing ...
  3. driving ...

如果将上面 的过程表现在UML类图上,大致可以表示为:
接口隔离原则.png

3.3 依赖倒转原则

高级模块不应该依赖低级模块,两者都应该依赖抽象。 抽象不应该依赖细节,细节应该依赖抽象。

依赖倒转原则(Dependency Inversion Principle, DIP)指的是代码应该依赖于抽象的类,而不要依赖于具体的类;要面向接口或抽象类编程,而不是针对具体的类编程。其实仔细对照我们实际的编程过程也好理解,虽然OOP已经将所有的东西都抽象为类,但是在此基础上还应该进行进一步的抽象。找到某些类中更加共性的东西,构建构建抽象的接口和抽象类,而在具体的实现类中实现细节。

假设此时定义Person类,类中的方法receive表示从何处接收信息,并调用参数代表的对象中的方法输出相应的信息。

  1. class Email{
  2. public void show(){
  3. System.out.println("Email --- hello world...");
  4. }
  5. }
  6. class Person{
  7. public void receive(Email email){
  8. email.show();
  9. }
  10. }
  11. public class Demo {
  12. public static void main(String[] args) {
  13. Person p = new Person();
  14. p.receive(new Email());
  15. }
  16. }

如果此时不只使用Email,还有WeChat、Facebook、Twitter……当前的方法就无法满足要求了。因此,根据依赖倒转原则,应该从不同的媒体中抽象出一个接口,接口中只包含show()。然后,通过接口不同的实现类来表示不同的媒体。最后使用时,Person的receive()中只需要接收接口的实例即可。它会根据传入的不同的接口实现类对象,输出相应的信息。

  1. interface Receiver{
  2. void show();
  3. }
  4. class Email implements Receiver{
  5. @Override
  6. public void show() {
  7. System.out.println("Email --- hello world...");
  8. }
  9. }
  10. class WeChat implements Receiver{
  11. @Override
  12. public void show() {
  13. System.out.println("WeChat --- hello world...");
  14. }
  15. }
  16. class Person{
  17. public void receive(Receiver receiver){
  18. receiver.show();
  19. }
  20. }
  21. public class NewDemo {
  22. public static void main(String[] args) {
  23. new Person().receive(new Email());
  24. new Person().receive(new WeChat());
  25. }
  26. }

将其表现在UML类图上如下所示:
依赖倒转原则.png

3.4 里氏替换原则

里氏替换原则(Liskov Substitution Principle, LSP)指的是所有引用基类的地方必须能透明的使用其子类对象,而且使用子类对象进行替换后,程序不会出现任何的错误和异常,反之不成立。根据里氏替换原则,程序中进行使用基类类型进行对象的定义,在运行时确定子类类型,并用子类对象进行替换。

使用继承时,为了遵从里氏替换原则,程序应注意:

  • 子类必须实现父类中声明的所有方法
  • 子类进行不要重写父类中的方法
  • 尽量把基类设计为抽象类或接口,子类为对应的实现类

通过让程序遵从里氏替换原则,使得程序可以更好的使用继承。假设此时定义一个Father类,类中有一个方法method()实现传入的两个参数相加,并返回相加的结果。同时涉及一个Son类,类中重写method()

  1. class Father{
  2. public int method(int x, int y){
  3. return x + y;
  4. }
  5. }
  6. class Son extends Father{
  7. @Override
  8. public int method(int x, int y) {
  9. return x - y;
  10. }
  11. }
  12. public class Demo {
  13. public static void main(String[] args) {
  14. System.out.println(new Father().method(3, 1));
  15. System.out.println(new Son().method(3, 1));
  16. }
  17. }

此时的程序就不满足里氏替换原则。为了遵从里氏替换原则,应该将基类抽象为一个抽象类,抽象类中包含方法method(),然后根据不同的运算分别创建接口的实现类,并重写抽象类中的方法。

  1. abstract class Method{
  2. public abstract int method(int x, int y);
  3. }
  4. class Add extends Method{
  5. @Override
  6. public int method(int x, int y) {
  7. return x + y;
  8. }
  9. }
  10. class Sub extends Method{
  11. @Override
  12. public int method(int x, int y) {
  13. return x -y;
  14. }
  15. }
  16. public class NewDemo {
  17. public static void main(String[] args) {
  18. Method m = new Add();
  19. System.out.println(m.method(3, 1));
  20. Method m1 = new Sub();
  21. System.out.println(m1.method(3, 1));
  22. }
  23. }

同样的,我们将其表示到UML类图中,如下所示:

里氏替换原则.png

3.5 开闭原则

开闭原则(Open-Closed Principle, OCP)指的是一个软件实体应当对扩展开放对修改关闭。也就是说在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即实现在不修改源代码的情况下改变这个模块的行为。

假设现在有一个GraphicEditor类,类中有一个方法drawShape()来根据不同的参数画不同的图。

  1. class Shape{
  2. int type;
  3. }
  4. class Circle extends Shape {
  5. Circle() {
  6. super.type = 1;
  7. }
  8. }
  9. class Triangle extends Shape {
  10. Triangle() {
  11. super.type = 2;
  12. }
  13. }
  14. class GraphicEditor{
  15. public void drawShape(Shape s){
  16. if (s.type == 1){
  17. drawCircle();
  18. } else if (s.type == 2){
  19. drawTriangle();
  20. }
  21. }
  22. private void drawTriangle() {
  23. System.out.println("drawing triangle...");
  24. }
  25. private void drawCircle() {
  26. System.out.println("drawing circle...");
  27. }
  28. }
  29. public class Demo {
  30. public static void main(String[] args) {
  31. GraphicEditor g = new GraphicEditor();
  32. g.drawShape(new Circle());
  33. g.drawShape(new Triangle());
  34. }
  35. }

如果想要画其他的图形呢?我们只能创建新类,并同时更改GraphicEditor类中drawShape()的逻辑。而这明显破坏了开闭原则,对于要实现的新功能,我们应该使用扩展的方式,而不是修改类的实现代码。

根据开闭原则,我们可以将Shape变为一个抽象类,类中包含抽象方法draw(),Circle和Triangle继承Shape,并实现draw()。GraphicEditor所做不再是根据对象的type值调用不同的方法,而是直接调用传入对象的draw()即可,程序会根据传入的具体对象调用相应的方法。

  1. abstract class Shape{
  2. int type;
  3. public abstract void draw();
  4. }
  5. class Circle extends Shape {
  6. @Override
  7. public void draw() {
  8. System.out.println("drawing circle...");
  9. }
  10. }
  11. class Triangle extends Shape {
  12. @Override
  13. public void draw() {
  14. System.out.println("drawing triangle...");
  15. }
  16. }
  17. class Rectangle extends Shape{
  18. @Override
  19. public void draw() {
  20. System.out.println("drawing Rectangle...");
  21. }
  22. }
  23. class GraphicEditor{
  24. public void drawShape(Shape s){
  25. s.draw();
  26. }
  27. }
  28. public class NewDemo {
  29. public static void main(String[] args) {
  30. GraphicEditor g = new GraphicEditor();
  31. g.drawShape(new Circle());
  32. g.drawShape(new Triangle());
  33. }
  34. }

这样对于使用者来说,它不必修改使用的代码;对于提供方法来说,每需要增加新功能,只需要在此基础上进行扩展,而不必修改类的代码。

将上面的例子表示到UML类图中为:
开闭原则.png

3.6 迪米特法则

迪米特法则(Law of Demeter, LoD)又称最少知道原则,它是指一个对象应该对其他的对象保持最少的了解,对象之间的关系越密切,耦合度越大。迪米特法则另一种说法是:只与直接朋友进行通信直接朋友指的是出现在成员变量、方法参数、方法返回值中的类称为当前类的直接朋友,而出现在局部变量中的类称为陌生人。

因此, 迪米特法则也可以定义为只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

例如明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与媒体公司的业务洽淡。这里的经纪人是明星的朋友,而媒体公司是陌生人,所以适合使用迪米特法则。

  1. public class Star {
  2. String name;
  3. public String getName() {
  4. return name;
  5. }
  6. public void setName(String name) {
  7. this.name = name;
  8. }
  9. }
  1. public class Company {
  2. String name;
  3. public String getName() {
  4. return name;
  5. }
  6. public void setName(String name) {
  7. this.name = name;
  8. }
  9. }
  1. public class Agent {
  2. Star myStar;
  3. Company company;
  4. public Star getMyStar() {
  5. return myStar;
  6. }
  7. public void setMyStar(Star myStar) {
  8. this.myStar = myStar;
  9. }
  10. public Company getCompany() {
  11. return company;
  12. }
  13. public void setCompany(Company company) {
  14. this.company = company;
  15. }
  16. public void meeting(){
  17. System.out.println("meeting...");
  18. }
  19. public void business(){
  20. System.out.println("doing business...");
  21. }
  22. }

将其表现在UML类图中为:
迪米特法则.png

3.7 合成复用原则

合成复用原则(Composite Reuse Principle, CRP)又称为组合/聚合复用原则,它的核心思想是尽量使用聚合和组合的方式,而不是继承。也就是说,一个新的对象里通过关联关系(组合/聚合)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用其已有功能的目的。

假设此时有Car类,类中包含方法sell()repair(),类Seller和Mechanic继承了Car。但对于Mechanic来说,它并不需要sell();对于Seller来说,它不需要repair()。因此,使用继承来使用Car中的方法并不是一个好的选择。

  1. class Car{
  2. public void sell(){
  3. System.out.println("selling...");
  4. }
  5. public void repair(){
  6. System.out.println("repairing...");
  7. }
  8. }
  9. class Seller extends Car{ }
  10. class Mechanic extends Car{}
  11. public class Demo {
  12. public static void main(String[] args) {
  13. new Seller().repair();
  14. new Mechanic().sell();
  15. }
  16. }

因此,根据合成复用原则,可以将Car类对象当作Seller和Mechanic的直接朋友使用,从而用组合/聚合代替继承。

  1. class Car{
  2. public void sell(){
  3. System.out.println("selling...");
  4. }
  5. public void repair(){
  6. System.out.println("repairing...");
  7. }
  8. }
  9. class Seller{
  10. public void sell(Car c){
  11. c.sell();
  12. }
  13. }
  14. class Mechanic{
  15. public void repair(Car c){
  16. c.repair();
  17. }
  18. }
  19. public class NewDemo {
  20. public static void main(String[] args) {
  21. new Seller().sell(new Car());
  22. new Mechanic().repair(new Car());
  23. }
  24. }

例子表现到UML类图中为:
合成复用原则.png

4. 参考

软件设计模式概述 菜鸟教程-设计模式 设计模式的七大原则