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