设计模式前篇之面向对象设计模式7大原则

一.面向对象设计原则

在面向对象编程时,以面向对象的设计原则为指导,开发可扩展性和维护性高的程序,多个原则需要适当取舍,达到业务与技术的一个平衡.

二.具体原则

1.开闭原则

(1).定义:对修改关闭,对扩展开放

  • 作用: 用抽象构建框架,用实现扩展细节
  • 优点:提高软件系统的可复用性和可维护性(可扩展性)
  • 实例:

以商城为例:

  1. /**
  2. * 商品接口
  3. * @author shixinke
  4. */
  5. public interface Product {
  6. /**
  7. * 获取商品ID
  8. * @return
  9. */
  10. Long getProductId();
  11. /**
  12. * 获取商品名称
  13. * @return
  14. */
  15. String getName();
  16. /**
  17. * 价格
  18. * @return
  19. */
  20. Double getPrice();
  21. }
  22. /**
  23. * 书籍类商品
  24. * @author shixinke
  25. */
  26. public class Book implements Product {
  27. /**
  28. * 商品ID
  29. */
  30. private Long productId;
  31. /**
  32. * 商品名称
  33. */
  34. private String name;
  35. /**
  36. * 商品价格
  37. */
  38. private Double price;
  39. /**
  40. * ISBN号
  41. */
  42. private String isbn;
  43. public Long getProductId() {
  44. return productId;
  45. }
  46. public String getName() {
  47. return name;
  48. }
  49. public Double getPrice() {
  50. return price;
  51. }
  52. }

有这样一个需求:在双11或618等有活动,需要对商品进行打折出售,那么如何去获取这个价格呢?

第一种方式:修改Book类中的getPrice方法

  1. /**
  2. * 书籍类商品
  3. * @author shixinke
  4. */
  5. public class Book implements Product {
  6. /**
  7. * 商品ID
  8. */
  9. private Long productId;
  10. /**
  11. * 商品名称
  12. */
  13. private String name;
  14. /**
  15. * 商品价格
  16. */
  17. private Double price;
  18. /**
  19. * ISBN号
  20. */
  21. private String isbn;
  22. public Book(Long productId, String name, Double price, String isbn) {
  23. this.productId = productId;
  24. this.name = name;
  25. this.price = price;
  26. this.isbn = isbn;
  27. }
  28. public Long getProductId() {
  29. return productId;
  30. }
  31. public String getName() {
  32. return name;
  33. }
  34. /**
  35. * 比如打9折(修改获取价格的方法)
  36. * @return
  37. */
  38. public Double getPrice() {
  39. return 0.9 * price;
  40. }
  41. }

第二种方式:新建一个子类继承Book类,重写getPrice方法

  1. /**
  2. * 活动书籍
  3. * @author shixinke
  4. */
  5. public class ActivityBook extends Book {
  6. public ActivityBook(Long productId, String name, Double price, String isbn) {
  7. super(productId, name, price, isbn);
  8. }
  9. /**
  10. * 获取原价
  11. * @return
  12. */
  13. public Double getOriginPrice() {
  14. return super.getPrice();
  15. }
  16. /**
  17. * 获取活动价
  18. * @return
  19. */
  20. public Double getPrice() {
  21. return 0.9 * super.getPrice();
  22. }
  23. }
  24. /**
  25. * 商品测试类
  26. * @author shixinke
  27. */
  28. public class ProductTest {
  29. public static void main(String[] args) {
  30. /**
  31. * 平时
  32. */
  33. Product book = new Book(1001L, "java设计模式", 125D, "97845124578");
  34. /**
  35. * 活动期间
  36. */
  37. Product activityBook = new ActivityBook(1001L, "java设计模式", 125D, "97845124578");
  38. ActivityBook bookObj = (ActivityBook) activityBook;
  39. System.out.println("书籍名称:"+bookObj.getName() + " 书籍原价:" + bookObj.getOriginPrice() + " 书籍活动价:" + bookObj.getPrice());
  40. }
  41. }

第二种方式类图如下:

开闭原则

总结:

  • 第一种方式是通过直接对原类进行修改,这样做是违背了”面向接口编程,而不是面向实现编程”的原则,如果又有一个新的活动,是另外一种折扣,则又需要修改原类,而且,几个活动无法并存
  • 第二种方式通过扩展原类,重写父类的相关方法,没有修改原类,又实现了相应的功能,如果又有一个活动,直接再写一个扩展类,继承原类即可
2.依赖倒置原则
  • 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 表现:
    • 抽象不应该依赖细节;细节应该依赖抽象
    • 针对接口编程,不应该针对实现编程
  • 优点:减少类之间的耦合,提高代码的可维护性和可读性,可降低因修改带来的风险
  • 实例:

第一种:

  1. /**
  2. * 缓存数据源
  3. * @author shixinke
  4. */
  5. public class CacheDataSource {
  6. public String get(Long userId) {
  7. return "从缓存中获取数据";
  8. }
  9. }
  10. /**
  11. * 用户数据访问服务
  12. * @author shixinke
  13. */
  14. public class UserService {
  15. /**
  16. * 缓存数据源
  17. */
  18. private CacheDataSource cacheDataSource;
  19. public UserService(CacheDataSource cacheDataSource) {
  20. this.cacheDataSource = cacheDataSource;
  21. }
  22. /**
  23. * 获取数据
  24. * @param userId
  25. * @return
  26. */
  27. public String getData(Long userId) {
  28. return cacheDataSource.get(userId);
  29. }
  30. }

上面例子的缺点:

  • 针对实现编程,灵活性差,如果将数据源改成数据库,则需要修改UserService类的代码

第二种:

(1)将数据源抽象为一个接口

  1. /**
  2. * 数据源接口
  3. * @author shixinke
  4. *
  5. */
  6. public interface DataSource {
  7. /**
  8. * 获取数据
  9. * @param userId
  10. * @return
  11. */
  12. String get(Long userId);
  13. }

(2)不同的数据源实现这个接口

  1. /**
  2. * 缓存数据源
  3. * @author shixinke
  4. */
  5. public class CacheDataSource implements DataSource {
  6. public String get(Long userId) {
  7. return "从缓存中获取数据";
  8. }
  9. }
  1. /**
  2. * 数据库数据源
  3. * @author shixinke
  4. */
  5. public class DatabaseDataSource implements DataSource {
  6. public String get(Long userId) {
  7. return "从数据库中获取数据";
  8. }
  9. }

(3)userService中定义使用接口作为参数

  1. /**
  2. * 用户数据访问服务
  3. * @author shixinke
  4. */
  5. public class UserService {
  6. /**
  7. * 数据源
  8. */
  9. private DataSource dataSource;
  10. public UserService(DataSource dataSource) {
  11. this.dataSource = dataSource;
  12. }
  13. /**
  14. * 获取数据
  15. * @param userId
  16. * @return
  17. */
  18. public String getData(Long userId) {
  19. return dataSource.get(userId);
  20. }
  21. }

(4)客户端控制使用哪个数据源

  1. /**
  2. * 用户数据测试类
  3. * @author shixinke
  4. */
  5. public class UserServiceTest {
  6. public static void main(String[] args) {
  7. DataSource cache = new CacheDataSource();
  8. UserService userService = new UserService(cache);
  9. System.out.println(userService.getData(1L));
  10. /**
  11. * 如果从其他数据源获取,只需要使用以下形式,而不用修改UserService类
  12. */
  13. DataSource db = new DatabaseDataSource();
  14. userService = new UserService(db);
  15. System.out.println(userService.getData(1L));
  16. }
  17. }

以上类的UML关系:

依赖倒置原则

3.单一职责原则

(1)定义:不要存在多于一个导致类变更的原因

(2)表现:一个类/接口/方法只负责一项职责

(3)优点:降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险

(4)实例:

订单支付

第一种:

  1. /**
  2. * 订单支付
  3. * @author shixinke
  4. */
  5. public class Payment {
  6. /**
  7. * 支付订单
  8. * @param paymentType
  9. * @param price
  10. * @return
  11. */
  12. public boolean pay(String paymentType, Double price) {
  13. if ("alipay".equalsIgnoreCase(paymentType)) {
  14. System.out.println("使用支付宝支付,支付金额为:"+price);
  15. /**
  16. * 支付宝支付逻辑
  17. */
  18. return true;
  19. } else if ("wepay".equalsIgnoreCase(paymentType)) {
  20. System.out.println("使用微信支付,支付金额为:"+price);
  21. /**
  22. * 微信支付逻辑
  23. */
  24. return true;
  25. }
  26. System.out.println("暂不支持其他支付方式");
  27. return false;
  28. }
  29. }

缺点:如果添加一种新的支付方式,得修改支付类

第二种:

(1)定义一个支付接口

  1. /**
  2. * 订单支付
  3. * @author shixinke
  4. */
  5. public interface Payment {
  6. /**
  7. * 订单支付
  8. * @param price
  9. * @return
  10. */
  11. boolean pay(Double price);
  12. }

(2)根据不同的支付方式定义对应的支付类

  1. /**
  2. * 支付宝支付
  3. * @author shixinke
  4. */
  5. public class Alipay implements Payment {
  6. public boolean pay(Double price) {
  7. System.out.println("使用支付宝支付,支付金额为:"+price);
  8. return true;
  9. }
  10. }
  1. /**
  2. * 微信支付
  3. * @author shixinke
  4. */
  5. public class Wepay implements Payment {
  6. public boolean pay(Double price) {
  7. System.out.println("使用微信支付,支付金额为:" + price);
  8. return true;
  9. }
  10. }

(3)客户端确定使用哪种支付方式

  1. /**
  2. * 支付测试
  3. * @author shixinke
  4. */
  5. public class PaymentTest {
  6. public static void main(String[] args) {
  7. Payment payment = new Alipay();
  8. payment.pay(38.9);
  9. Payment wepay = new Wepay();
  10. wepay.pay(38.9);
  11. }
  12. }

以上类的UML关系:

单一职责原则

4.接口隔离原则

(1)定义:客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上

(2)表现:不要在一个接口里面放很多的方法

(3)优点:最小化接口,降低耦合,提高复用性

(4)实例:

第一种:不遵循接口隔离原则

定义一个大的接口:

  1. /**
  2. * 手机接口
  3. * @author shixinke
  4. */
  5. public interface Mobile {
  6. /**
  7. * 可以打电话
  8. */
  9. void call();
  10. /**
  11. * 可以上网
  12. */
  13. void internet();
  14. }

只包含接口部分功能的实现类

  1. /**
  2. * 诺基亚手机(砸核桃的那种)
  3. * @author shixinke
  4. */
  5. public class NokiaMobile implements Mobile {
  6. public void call() {
  7. System.out.println("用诺基亚手机打电话");
  8. }
  9. public void internet() {
  10. System.out.println("用诺基亚无法上网");
  11. }
  12. }

包含接口所有功能的实现类

  1. /**
  2. * 华为手机
  3. * @author shixinke
  4. */
  5. public class HuweiMobile implements Mobile {
  6. public void call() {
  7. System.out.println("用华为手机打电话");
  8. }
  9. public void internet() {
  10. System.out.println("用华为手机上5G网网络");
  11. }
  12. }

第二种:遵循接口隔离原则的

将接口最小化定义,每个接口只实现一个功能

  • 打电话的接口
  1. /**
  2. * 可以打电话的手机
  3. * @author shixinke
  4. */
  5. public interface CallableMobile {
  6. /**
  7. * 打电话
  8. */
  9. void call();
  10. }
  • 上网的接口
  1. /**
  2. * 可以上网的手机
  3. * @author shixinke
  4. */
  5. public interface InternetMobile {
  6. /**
  7. * 上网
  8. */
  9. void internet();
  10. }

实现类选择对应的功能

  • 诺基亚手机只实现打电话的功能
  1. /**
  2. * 诺基亚手机(它可以打电话,因此实现打电话的功能)
  3. * @author shixinke
  4. */
  5. public class NokiaMobile implements CallableMobile {
  6. public void call() {
  7. System.out.println("用诺基亚手机打电话");
  8. }
  9. }
  • 华为手机实现打电话和上网两个功能
  1. /**
  2. * 华为手机(即能打电话,也能上网)
  3. * @author shixinke
  4. */
  5. public class HuaweiMobile implements CallableMobile, InternetMobile {
  6. public void call() {
  7. System.out.println("用华为手机打电话");
  8. }
  9. public void internet() {
  10. System.out.println("用华为手机上5G网网络");
  11. }
  12. }

以上几个类对应的UML关系图:

接口隔离原则

5.迪米特原则(最少知道原则)

(1)定义:一个对象应该对其他对象保持最少的了解(也叫最少知道原则)

(2)表现:

  • 尽量降低类与类之间的耦合(尽量少使用public的方法和变量)
  • 只与成员变量、方法的入参及出参进行交流

(3)优点:降低类之间的耦合

(4)实例:

老板想知道当天的订单总数,而老板向运营人员获取这个数量,而此时运营系统没有查询订单数的功能(这个运营系统功能真不全),运营人员需要向开发求助,开发人员通过查询数据库来获取,将结果交给运营人员,运营人员再将结果报告给老板

不遵循迪米特法则

  • 订单
  1. /**
  2. * 订单
  3. * @author shixinke
  4. */
  5. public class Order {
  6. }
  • 开发者
  1. /**
  2. * 开发者
  3. * @author shixinke
  4. */
  5. public class Developer {
  6. public int getOrderCount() {
  7. /**
  8. * 查库
  9. */
  10. List<Order> orders = new ArrayList<Order>(20);
  11. for (int i = 0; i < 20; i++) {
  12. orders.add(new Order());
  13. }
  14. return orders.size();
  15. }
  16. }
  • 运营人员
  1. /**
  2. * 运营工作人员
  3. * @author shixinke
  4. */
  5. public class Operator {
  6. /**
  7. * 因为后台没有这个功能,所以运营需要开发查库才能获取数量
  8. */
  9. public void getOrderCount(Developer developer) {
  10. System.out.println("订单数:" + developer.getOrderCount());
  11. }
  12. }
  • 老板
  1. /**
  2. * 老板
  3. * @author shixinke
  4. */
  5. public class Boss {
  6. /**
  7. * 问运营今天有多少订单数
  8. * @param operator
  9. */
  10. public void getOrderCount(Operator operator) {
  11. Developer developer = new Developer();
  12. operator.getOrderCount(developer);
  13. }
  14. }

缺点:

  • boss类,需要为运营找一个开发,让开发来查询,而boss类只关心订单结果,不关心结果是怎么来的,因此,它不需要知道Developer类

遵循迪米特法则

  • 订单
  1. /**
  2. * 订单
  3. * @author shixinke
  4. */
  5. public class Order {
  6. }
  • 开发者
  1. /**
  2. * 开发者
  3. * @author shixinke
  4. */
  5. public class Developer {
  6. public int getOrderCount() {
  7. /**
  8. * 查库
  9. */
  10. List<Order> orders = new ArrayList<Order>(20);
  11. for (int i = 0; i < 20; i++) {
  12. orders.add(new Order());
  13. }
  14. return orders.size();
  15. }
  16. }
  • 运营人员
  1. /**
  2. * 运营工作人员
  3. * @author shixinke
  4. */
  5. public class Operator {
  6. /**
  7. * 因为后台没有这个功能,所以运营需要开发查库才能获取数量
  8. */
  9. public void getOrderCount() {
  10. Developer developer = new Developer();
  11. System.out.println("订单数:" + developer.getOrderCount());
  12. }
  13. }
  • 老板
  1. /**
  2. * 老板
  3. * @author shixinke
  4. */
  5. public class Boss {
  6. /**
  7. * 问运营今天有多少订单数
  8. * @param operator
  9. */
  10. public void getOrderCount(Operator operator) {
  11. operator.getOrderCount();
  12. }
  13. }

注:老板只需要向运营人员要结果就行,其他细节他不关心,也不应该让他知道

以上类的UML关系图如下:

最少知道原则

6.里氏替换原则

(1)定义:任何基类可以出现的地方,子类一定也可以出现

(2)表现:

  • 子类可以扩展父类的功能,但不能改变父类原有的功能

(3)实例:

  • 基类:
  1. /**
  2. * 动物类
  3. * @author shixinke
  4. */
  5. public class Animal {
  6. public void eat() {
  7. System.out.println("吃东西");
  8. }
  9. }
  • 子类
  1. /**
  2. * 猴子
  3. * @author shixinke
  4. */
  5. public class Monkey extends Animal{
  6. public void climb() {
  7. System.out.println("猴子爬树");
  8. }
  9. }
  • 使用
  1. /**
  2. * 测试类
  3. * @author shixinke
  4. */
  5. public class Test {
  6. public static void main(String[] args) {
  7. Animal animal = new Animal();
  8. animal.eat();
  9. /**
  10. * 猴子
  11. */
  12. Animal mon = new Monkey();
  13. mon.eat();
  14. }
  15. }

在使用Animal类对象的时候,都可以使用Monkey这个子类的对象来进行替换

UML图如下:

里氏替换原则

7.组合复用原则

(1)定义:一个对象应该对其他对象保持最少的了解(也叫最少知道原则)

(2)表现:

  • 尽量多用组合,少用继承

(3)实例:

以从数据源获取数据为例(数据源可能是Redis或者MySQL)

  • 定义一个数据源接口:
  1. /**
  2. * 数据源
  3. * @author shixinke
  4. */
  5. public interface DataSource {
  6. /**
  7. * 获取数据
  8. * @param id
  9. * @return
  10. */
  11. String get(String id);
  12. }
  • 定义Redis数据源
  1. public class RedisDataSource implements DataSource {
  2. public String get(String id) {
  3. return "从Redis中获取数据";
  4. }
  5. }
  • 定义MySQL数据源
  1. public class MysqlDataSource implements DataSource {
  2. public String get(String id) {
  3. return "从MySQL中获取数据";
  4. }
  5. }
  • 定义用户数据操作类
  1. /**
  2. * 用户数据操作类
  3. * @author shixinke
  4. */
  5. public class UserDao {
  6. private DataSource dataSource;
  7. public UserDao(DataSource dataSource) {
  8. this.dataSource = dataSource;
  9. }
  10. public String getUserInfo(String id) {
  11. return dataSource.get(id);
  12. }
  13. }
  • 测试
  1. /**
  2. * 数据源测试
  3. * @author shixinke
  4. */
  5. public class DataSourceTest {
  6. public static void main(String[] args) {
  7. DataSource ds = new RedisDataSource();
  8. UserDao userDao = new UserDao(ds);
  9. System.out.println(userDao.getUserInfo("11"));
  10. }
  11. }

注:用户数据操作类UserDao不继承MysqlDataSource也不继承RedisDataSource,而是将DataSource的实例当成UserDao的一个实例变量,通过组合来关联两个类

以上类之间的关系:

组合复用原则