精心整理了设计模式的七大原则,包括代码解释方便理解,但是难免不了存在纰漏,感谢大家的指正与理解!觉的写的不错的小伙伴儿,一键三连支持一下,后期会有持续更新!!抱拳了罒ω罒
设计模式的七大原则
面向对象的设计模式有七大基本原则:
- 开闭原则(Open Closed Principle,OCP)
- 单一职责原则(Single Responsibility Principle, SRP)
- 里氏代换原则(Liskov Substitution Principle,LSP)
- 依赖倒转原则(Dependency Inversion Principle,DIP)
- 接口隔离原则(Interface Segregation Principle,ISP)
- 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
- 最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)
设计模式原则名称 | 简单定义 |
---|---|
开闭原则 | 对扩展开放,对修改关闭 |
单一职责原则 | 一个类只负责一个功能领域中的相应职责 |
里氏代换原则 | 所有引用基类的地方必须能透明地使用其子类的对象 |
依赖倒转原则 | 依赖于抽象,不能依赖于具体实现 |
接口隔离原则 | 类之间的依赖关系应该建立在最小的接口上 |
合成/聚合复用原则 | 尽量使用合成/聚合,而不是通过继承达到复用的目的 |
迪米特法则 | 一个软件实体应当尽可能少的与其他实体发生相互作用 |
1. 开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原代码的情况下进行扩展。
下面通过一个例子遵循开闭原则进行设计,场景是这样:某系统的后台需要监测业务数据展示图表,如柱状图、折线图等,在未来需要增加饼图展示操作。在开始设计的时候,代码可能是这样的:
public class BarChart {
public void draw(){
System.out.println("Draw bar chart...");
}
}
public class LineChart {
public void draw(){
System.out.println("Draw line chart...");
}
}
public class App {
public void drawChart(String type){
if (type.equalsIgnoreCase("line")){
new LineChart().draw();
}else if (type.equalsIgnoreCase("bar")){
new BarChart().draw();
}
}
}
这样做在初期是能满足业务需要的,开发效率也十分高,但是当后面需要新增一个饼状图的时候,既要添加一个饼状图的类,原来的客户端App类的drawChart方法也要新增一个if分支,这样做就是修改了原有客户端类库的方法,是十分不合理的。基于此,需要引入一个抽象Chart类AbstractChart,App类在画图的时候总是把相关的操作委托到具体的AbstractChart的派生类实例,这样的话App类的代码就不用修改:
public abstract class AbstractChart {
public abstract void draw();
}
public class BarChart extends AbstractChart{
@Override
public void draw() {
System.out.println("Draw bar chart...");
}
}
public class LineChart extends AbstractChart {
@Override
public void draw() {
System.out.println("Draw line chart...");
}
}
public class App {
public void drawChart(AbstractChart chart){
chart.draw();
}
}
如果新加一种图,只需要新增一个AbstractChart的子类即可。客户端类App不需要改变原来的逻辑。修改后的设计符合开闭原则,因为整个系统在扩展时原有的代码没有做任何修改。
2. 单一职责原则
定义:对类或者模块来说,一个类或者模块应该只负责一项职责。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。
下面通过一个很简单的实例说明一下单一职责原则:在一个项目系统代码编写的时候,由于历史原因和人为的不规范,导致项目没有分层,一个Service类的伪代码是这样的:
public class Service {
public UserDTO findUser(String name){
String sql = "SELECT * FROM t_user WHERE name = ?";
Connection connection = getConnection();
PreparedStatement preparedStatement =
connection.prepareStatement(sql);
preparedStatement.setObject(1, name);
User user = //处理结果
UserDTO dto = new UserDTO();
//entity值拷贝到dto
return dto;
}
}
这里出现一个问题,Service做了太多东西,包括数据库连接的管理,Sql的执行等,这些业务层不应该接触到的逻辑,更可怕的是,例如到时候如果数据库换成了Oracle,这个方法将会大改。因此,拆分出新的DataBaseUtils类用于专门管理数据库资源,Dao类用于专门执行查询和查询结果封装,改造后Service类的伪代码如下:
public class Service {
private Dao dao;
public UserDTO findUser(String name){
User user = dao.findUserByName(name);
UserDTO dto = new UserDTO();
//entity值拷贝到dto
return dto;
}
}
public class Dao{
public User findUserByName(String name){
Connection connection = DataBaseUtils.getConnnection();
String sql = "SELECT * FROM t_user WHERE name = ?";
PreparedStatement preparedStatement =
connection.prepareStatement(sql);
preparedStatement.setObject(1, name);
User user = //处理结果
return user;
}
}
如果有查询封装的变动只需要修改Dao类,数据库相关变动只需要修改DataBaseUtils类,每个类的职责分明。
3. 里式替换原则
定义:所有引用基类的地方必须能透明地使用其子类的对象,通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能,即尽量不要重写父类的方法。
举个简单的例子,基类A是计算两个数相减,类B完成两个数相加,然后和9求和
public class LisKov {
public static void main(String[] args) {
A a = new A();
System.out.println("11 - 3 = " + a.func1(11,3));
B b = new B();
//由于B重写的func1方法,所以计算11和3的差会变成求和,出现计算错误
System.out.println("11 - 3 = " + b.func1(11,3));
}
}
class A{
public int func1(int num1,int num2){
return num1 - num2;
}
}
class B extends A{
@Override
public int func1(int a, int b){
return a + b;
}
}
我们发现原来运行正常的相减功能发生了错误。原因就是类 B 无意中重写了父类的func1方法,造成原有功能出现错误。通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替。
public class LisKov {
public static void main(String[] args) {
A a = new A();
System.out.println("11 - 3 = " + a.func1(11,3));
B b = new B();
// 使用组合调用A的func1的方法
System.out.println("11 -3 = " + b.func3(11,3));
}
}
// 创建一个更加基础的基类
class Base{
}
class A extends Base{
public int func1(int num1,int num2){
return num1 - num2;
}
}
// 使用组合关系来代替继承
class B extends Base {
private A a = new A();
public int func1(int a, int b){
return a + b;
}
// 我们仍然想使用 A 的方法
public int func3(int a,int b){
return this.a.func1(a,b);
}
}
4. 依赖倒转原则
定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。从Java角度看待依赖倒转原则的本质就是:面向接口(抽象)编程。
遵循依赖倒转原则的一个例子,场景是接受邮件:
public class DependencyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email{
public String getInfo(){
return "电子邮件信息:Hello,world!";
}
}
class Person{
public void receive(Email email){
System.out.println(email.getInfo());
}
}
如果我们获取的对象是微信,短信等,则要新增类,同时Person也要增加相应的接收。因此引入一个抽象的接口 IReceiver,表示接收者,这样Person类与接口发生依赖,因为Email还有微信等都属于接收的范围,它们各自实现 IReceiver 接口就ok,这样我们就符合依赖倒转原则:
public class DependencyInversion {
public static void main(String[] args) {
// 客户端无需改变
Person person = new Person();
person.receive(new Email());
person.receive(new WeChat());
}
}
interface IReceiver{
String getInfo();
}
class Email implements IReceiver{
@Override
public String getInfo(){
return "电子邮件信息:Hello,world!";
}
}
class WeChat implements IReceiver{
@Override
public String getInfo() {
return "微信消息:hello,ok!";
}
}
class Person{
public void receive(IReceiver receiver){
// 这里我们是对接口的依赖
System.out.println(receiver.getInfo());
}
}
5. 接口隔离原则
定义:是客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。简单来说就是建立单一的接口,不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少。
举例来说明接口隔离原则:
interface I {
public void method1();
public void method2();
public void method3();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
}
class B implements I{
public void method1() {
System.out.println("类B实现接口I的方法1");
}
public void method2() {
System.out.println("类B实现接口I的方法2");
}
//对于类B来说,method3不是必需的,但是由于接口中有这个方法,
//所以在实现过程中即使这个方法的方法体为空,也要将这个没有作用的方法进行实现。
public void method3() { }
}
public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
}
}
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分:
interface I1 {
public void method1();
public void method2();
}
interface I2 {
public void method3();
}
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I1 i){
i.method2();
}
}
class B implements I1{
public void method1() {
System.out.println("类B实现接口I1的方法1");
}
public void method2() {
System.out.println("类B实现接口I1的方法2");
}
}
public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
}
}
6. 合成/聚合复用原则
定义:尽量使用合成/聚合,而不是通过继承达到复用的目的。
类A有2个方法,类B刚好需要调用这两个方法,我们第一可能会想到直接继承,这样“多快好省”,但随着业务进展,功能越来越复杂,A类需要增加其他方法,比如Method3 ,与B类毫无关联,将会大大增加耦合性,合用复用原则的核心就是使用关联,我们可以通过依赖、聚合、合成等关联方法,降低耦合,提高可维护性和降低维护成本。
public class A{
public void Method1(){
Console.WriteLine("我是方法一");
}
public void Method2(){
Console.WriteLine("我是方法二");
}
}
//依赖
public class B{
public void Method1(A a){
Console.WriteLine("调用A方法");
}
}
//聚合
public class C{
private A a;
public void SetA(A al){
a = al;
}
}
//合成
public class D{
public A a = new A();
}
7. 迪米特法则
定义:迪米特法则,有时候也叫做最少知识原则,一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类不要以局部变量的形式出现在类的内部。
举个很简单的例子,体育老师要知道班里面女生的人数,他委托体育课代表点清女生的人数:
public class Girl {
}
public class GroupLeader {
private final List<Girl> girls;
public GroupLeader(List<Girl> girls) {
this.girls = girls;
}
public void countGirls() {
System.out.println("The sum of girls is " + girls.size());
}
}
public class Teacher {
public void command(GroupLeader leader){
leader.countGirls();
}
}
public class App {
public static void main(String[] args) throws Exception {
Girl girl1 = new Girl();
Girl girl2 = new Girl();
List<Girl> list = new ArrayList<>();
list.add(girl1);list.add(girl2);
Teacher teacher = new Teacher();
GroupLeader groupLeader = new GroupLeader(list);
teacher.command(groupLeader);
}
}
这个例子中,体育课代表就是中间类,体育课代表对于体育老师来说就是"直接的朋友",如果去掉体育课代表这个中间类,体育老师必须亲自清点女生的人数(实际上就数人数这个功能,体育老师是不必要获取所有女生的对象列表),这样做会违反迪米特法则。
参考文章:
https://www.cnblogs.com/throwable/p/9315318.html
https://www.cnblogs.com/zhaye/p/11176906.html