设计模式之七大原则
设计模式基本原则
单一职责原则 (Single Responsibility Principle, SRP)
定义:
一个类只负责一个功能领域中相应职责 (对一个类而言,应该只有一个引起它变化的原因)
作用: 实现高内聚,低耦合
案例:
客户信息图形统计模块
违背单一职责原则 如果修改数据库连接方式或者修改图标显示方式都需要修改这个类; 不能重用数据库连接的代码
重构
代码实现:
class CustomerDataChart {
private CustomerDao customerDao;
public void createChart(){
System.out.println("创建图表");
}
public void displayChart(){
System.out.println("显示图表");
}
}
class CustomerDao {
private DBUtil dbUtil;
public List<Customer> findCustomers() {
System.out.println("获取全部的客户列表");
}
}
class DBUtil {
public Connection getConnection() {
System.out.println("获取数据库连接");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
开闭原则 (Open-Closed Principle, OCP)
定义:
一个软件实体应当对扩展开放, 对修改关闭 (尽量不修改原来的代码, 而是添加新代码实现需求)
作用: 使系统拥有适应性和灵活性,同时具备较好的稳定性和延续性
案例
代码
......
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
......
2
3
4
5
6
7
8
9
10
问题: 当需要新增一种图表显示时,必须修改源代码,增加判断语句,违背开闭原则
重构
代码:
class ChartDisplay {
private AbstractChart chart;
public void setChart(AbstractChart chart) {
this.chart = chart;
}
public void display(){
chart.display();
}
}
abstract class AbstractChart {
public abstract void display();
}
class PieChart extends AbstractChart {
public void display() {
System.out.println("圆饼图形显示");
}
}
class BarChart extends AbstractChart {
public void display() {
System.out.println("条形图形显示");
}
}
// 新增显示类, 不需要修改原有代码
class CurveChart extends AbstractChart {
public void display() {
System.out.println("曲线图形显示");
}
}
public class OCPTest {
public static void main(String[] args) {
ChartDisplay c = new ChartDisplay();
// 修改客户端代码, 或者使用 xml 或者 properties 配置文件,修改配置文件实现类字符串实现
AbstractChart chart = new CurveChart();
c.setChart(chart);
c.display();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
里氏替换 (Liskov Substitution Principle, LSP)
定义:
所有引用基类的地方必须能透明的使用其子类的对象 在软件中将一个基类对象替换成它的子类对象, 程序将不会产生任何错误和异常, 反过来则不成立, 如果一个软件实体使用的是一个子类对象的话, 那么它不一定能够使用基类对象.
作用: 里氏代换原则是实现开闭原则的重要方式之一, 由于使用基类对象的地方都可以使用子类对象, 因此在程序中尽量使用基类类型来对对象进行定义, 而在运行时再确定其子类类型, 用子类对象来替换父类对象.
在使用里氏代换原则时需要注意如下几个问题:
- 子类的所有方法必须在父类中声明, 或子类必须实现父类中声明的所有方法. 根据里氏代换原则, 为了保证系统的扩展性, 在程序中通常使用父类来进行定义, 如果一个方法只存在子类中, 在父类中不提供相应的声明, 则无法在以父类定义的对象中使用该方法.
- 我们在运用里氏代换原则时, 尽量把父类设计为抽象类或者接口, 让子类继承父类或实现父接口, 并实现在父类中声明的方法, 运行时, 子类实例替换父类实例, 我们可以很方便地扩展系统的功能, 同时无须修改原有子类的代码, 增加新的功能可以通过增加一个新的子类来实现. 里氏代换原则是开闭原则的具体实现手段之一.
- Java 语言中, 在编译阶段, Java 编译器会检查一个程序是否符合里氏代换原则, 这是一个与实现无关的、纯语法意义上的检查, 但 Java 编译器的检查是有局限的.
案例
重构
代码
class EmailSender {
public void send(Customer customer){
System.out.print(customer.getName() + "发送邮件");
}
}
abstract class Customer {
protected String name;
protected String email;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setEmail(String email){
this.email = email;
}
public String getEmail(){
return email;
}
}
class CommonCustomer extends Customer{
}
class VIPCustomer extends Customer{
}
public class LSPTest {
public static void main(String[] args) {
Customer customer = new CommonCustomer();
customer.setName("普通用户");
new EmailSender().send(customer);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
依赖倒转 (Dependency Inversion Principle, DIP)
定义:
抽象不应该依赖于细节,细节应当依赖于抽象(针对接口编程,而不是针对实现编程) 依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中, 尽量引用层次高的抽象层类, 即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明, 以及数据类型的转换等, 而不要用具体类来做这些事情.
作用: 在引入抽象层后, 系统将具有很好的灵活性, 在程序中尽量使用抽象层进行编程, 而将具体类写在配置文件中, 这样一来, 如果系统行为发生变化, 只需要对抽象层进行扩展, 并修改配置文件, 而无须修改原有系统的源代码, 在不修改的情况下来扩展系统的功能, 满足开闭原则的要求
案例
代码
class CustomerDao {
public void addCustomers(TXTDataConvertor convertor){
convertor.readFile();
System.out.println("存入数据库");
}
}
class TXTDataConvertor {
public void readFile(){
System.out.println("从文本转换数据");
}
}
class ExcelDataConvertor {
public void readFile(){
System.out.println("从excle转换数据");
}
}
class DIPTest {
public static void main(String[] args) {
CustomerDao customerDao = new CustomerDao();
customerDao.addCustomers(new TXTDataConvertor());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
当需要从 excel 文件转换数据时,必须修改 CustomerDao 实现代码, 违背开闭原则.
重构
代码
class CustomerDao {
// 改成抽象类
public void addCustomers(DataConvertor convertor){
convertor.readFile();
System.out.println("存入数据库");
}
}
abstract class DataConvertor {
public abstract void readFile();
}
class TXTDataConvertor extends DataConvertor {
public void readFile(){
System.out.println("从文本转换数据");
}
}
class ExcelDataConvertor extends DataConvertor {
public void readFile(){
System.out.println("从excle转换数据");
}
}
class DIPTest {
public static void main(String[] args) {
CustomerDao customerDao = new CustomerDao();
// 这里可以从配置文件中读取类名,然后使用反射创建对象
customerDao.addCustomers(new TXTDataConvertor());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
里氏代换原则是基础, 依赖倒转原则是手段, 开闭原则是目标
接口隔离 (Interface Segregation Principle, ISP)
定义:
使用多个专门的接口, 而不使用单一的中接口 ( 客户端不应该依赖那些不需要的接口)
作用: 避免实现不需要的功能, 造成类过大
案例
实现 CustomerDataDisplay , 必须全部实现里面的抽象方法,但是显示类有时候并不需要某些方法, 这是因为 CustomerDataDisplay 声明了太多抽象方法
重构
显示类本身有3个方法, 如果需要其他功能, 可以实现对应功能的接口即可
在使用接口隔离原则时, 我们需要注意控制接口的粒度, 接口不能太小, 如果太小会导致系统中接口泛滥, 不利于维护;接口也不能太大, 太大的接口将违背接口隔离原则, 灵活性较差, 使用起来很不方便. 一般而言, 接口中仅包含为某一类用户定制的方法即可, 不应该强迫客户依赖于那些它们不用的方法.
合成复用原则 (Composite Reuse Principle, CRP)
定义:
尽量使用对象组合, 而不是进程来达到复用的目的 在面向对象设计中, 可以通过两种方法在不同的环境中复用已有的设计和实现, 即通过组合 / 聚合关系或通过继承, 但首先应该考虑使用组合 / 聚合, 组合 / 聚合可以使系统更加灵活, 降低类与类之间的耦合度, 一个类的变化对其他类造成的影响相对较少;其次才考虑继承, 在使用继承时, 需要严格遵循里氏代换原则, 有效使用继承会有助于对问题的理解, 降低复杂度, 而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度, 因此需要慎重使用继承复用.
作用: 降低耦合度
is-a 关系 (继承): 一个类是另一个的 一种 has-a 关系 (组合/聚合): 某个角色具有某一项责任
案例
将获取数据库连接的方法提取到 DBUtil 中, CustomerDao 继承 DBUtil 当需要更换数据库时, 必须修改 DBUtil 代码,违反了开闭原则,或者修改 CustomerDao, 继承另一个获取数据库连接实现类,同样潍坊了开闭原则
重构
使用关联关系
代码
class DBUtil {
public Connection getConnection(){
System.out.println("得到数据库连接");
return null;
}
}
class OracleDBUtil extends DBUtil {
public Connection getConnection(){
System.out.println("得到Oracle数据库连接");
return null;
}
}
class CustomerDao {
private DBUtil dbUtil;
public CustomerDao(DBUtil dbUtil){
this.dbUtil = dbUtil;
}
public void addCustomerDao(){
dbUtil.getConnection();
System.out.println("添加操作");
}
}
public class CRPTest {
public static void main(String[] args) {
DBUtil dbUtil = new OracleDBUtil();
// 可以从配置文件中获取数据库实现类,然后使用反射创建对象
CustomerDao customerDao = new CustomerDao(dbUtil);
customerDao.addCustomerDao();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
这样重构后,当再次更换数据库时,只需要添加一个获取数据库的实现类,然后继承 DBUtil 即可.不需要更改任何代码
迪米特法则 (Law of Demeter, LoD)
定义:
一个软件实体应当尽可能少地与其他实体发生相互作用. 如果一个系统符合迪米特法则, 那么当其中某一个模块发生修改时, 就会尽量少地影响其他模块, 扩展会相对容易, 这是对软件实体之间通信的限制, 迪米特法则要求限制软件实体之间通信的宽度和深度. 迪米特法则可降低系统的耦合度, 使类与类之间保持松散的耦合关系.
作用: 使系统更容易扩展, 降低耦合度
迪米特法则还有几种定义形式, 包括: 不要和 “陌生人” 说话、只与你的直接朋友通信等, 在迪米特法则中, 对于一个对象, 其朋友包括以下几类:
- 当前对象本身 (this);
- 以参数形式传入到当前对象方法中的对象;
- 当前对象的成员对象;
- 如果当前对象的成员对象是一个集合, 那么集合中的元素也都是朋友;
- 当前对象所创建的对象.
引用
面向对象设计原则之单一职责原则 面向对象设计原则之开闭原则 面向对象设计原则之里氏代换原则 面向对象设计原则之依赖倒转原则 面向对象设计原则之接口隔离原则 面向对象设计原则之合成复用原则 面向对象设计原则之迪米特法则