设计模式的目的和七大原则
设计模式是为了让程序(软件)具有更好的:
- 代码可重用性
- 可读性(程序规范性)
- 可扩展性(很方便的增加新功能)
- 可靠性(增加新功能后,对原功能不会产生不利影响)
- 使程序呈现高内聚、低耦合的特性
七大原则:
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里氏替换原则
- 开闭原则
- 迪米特法原则
- 合成复用原则
单一职责原则
对类来说,即一个类应该只负责一项职责,如类A负责两个不同的原则:职责1和职责2,当职责1修改导致类A改变时,可能会造成职责2执行错误,因此需要将类A的粒度分解为A1和A2。
接口隔离原则
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
如下面的代码示例,例1中,B和D类都实现了接口interface1的所有接口方法,但是类A通过接口依赖B,只需要使用operation1()、operation2()、operation3(),类C通过接口依赖D,只需要使用1()、operation4()、operation5(),而例1中的B和D实现了接口的所有方法,即客户端依赖了它不需要的方法
。例2对接口进行改进,将接口进行拆分,其他类按需实现接口即可。
例1:
interface Interface1 {
void operation1();
void operation2();
void operation3();
void operation4();
void operation5();
}
class B implements Interface1 {
public void operation1() {
System.out.println("B-operation1");
}
public void operation2() {
System.out.println("B-operation2");
}
public void operation3() {
System.out.println("B-operation3");
}
public void operation4() {
System.out.println("B-operation4");
}
public void operation5() {
System.out.println("B-operation5");
}
}
class D implements Interface1 {
public void operation1() {
System.out.println("D-operation1");
}
public void operation2() {
System.out.println("D-operation2");
}
public void operation3() {
System.out.println("D-operation3");
}
public void operation4() {
System.out.println("D-operation4");
}
public void operation5() {
System.out.println("D-operation5");
}
}
class A { //A 类通过接口Interface1 依赖(使用) B类,但是只会用到1,2,3方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend2(Interface1 i) {
i.operation2();
}
public void depend3(Interface1 i) {
i.operation3();
}
}
class C { //C 类通过接口Interface1 依赖(使用) D类,但是只会用到1,4,5方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend4(Interface1 i) {
i.operation4();
}
public void depend5(Interface1 i) {
i.operation5();
}
}
例2:
public class Segregation1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 使用一把
A a = new A();
a.depend1(new B()); // A类通过接口去依赖B类
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D()); // C类通过接口去依赖(使用)D类
c.depend4(new D());
c.depend5(new D());
}
}
// 接口1
interface Interface1 {
void operation1();
}
// 接口2
interface Interface2 {
void operation2();
void operation3();
}
// 接口3
interface Interface3 {
void operation4();
void operation5();
}
class B implements Interface1, Interface2 {
public void operation1() {
System.out.println("B-operation1");
}
public void operation2() {
System.out.println("B-operation2");
}
public void operation3() {
System.out.println("B-operation3");
}
}
class D implements Interface1, Interface3 {
public void operation1() {
System.out.println("D-operation1");
}
public void operation4() {
System.out.println("D-operation4");
}
public void operation5() {
System.out.println("D-operation5");
}
}
class A { // A 类通过接口Interface1,Interface2 依赖(使用) B类,但是只会用到1,2,3方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend2(Interface2 i) {
i.operation2();
}
public void depend3(Interface2 i) {
i.operation3();
}
}
class C { // C 类通过接口Interface1,Interface3 依赖(使用) D类,但是只会用到1,4,5方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend4(Interface3 i) {
i.operation4();
}
public void depend5(Interface3 i) {
i.operation5();
}
}
依赖倒转原则
例4的这种方式直接把Email类类型作为receive方法的参数类型,如果发送消息由Email改为WeChat,那么就要对Person 类进行修改。而例5对例4进行了改进,receive方法参数不再写死为Email,而是通过定义一个接口IReceiver ,其他类实现该方法,receive方法参数改为IReceiver 类型,由客户端传入一个该接口的具体实现类,这就是依赖倒转。
依赖倒转原则是指:
- 高层模块不应该依赖底层模块,二周都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转的中心思想是面向接口编程
例3:
public class DependecyInversion {
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());
}
}
例:4:
public class DependecyInversion {
public static void main(String[] args) {
//客户端无需改变
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
}
}
//定义接口
interface IReceiver {
public String getInfo();
}
class Email implements IReceiver {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
//增加微信
class WeiXin implements IReceiver {
public String getInfo() {
return "微信信息: hello,ok";
}
}
//方式2
class Person {
//这里是对接口的依赖
public void receive(IReceiver receiver ) {
System.out.println(receiver.getInfo());
}
}
李氏替换原则
继承带来的缺点:侵入性、不够灵活、高耦合
- 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性;
- 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用。
因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则:
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
下面以一个简单的例子展示继承的缺陷以及如何根据里氏替换原则改善代码。
例5:
public class Liskov {
public static void main(String[] args) {
B b = new B();
//本意是调用父类A的func1方法进行减法运算,但子类重写了该方法
System.out.println("11-3=" + b.func1(11, 3));
System.out.println("11+3+9=" + b.func2(11, 3));
}
}
class A {
public int func1(int num1, int num2) {
return num1 - num2;
}
}
class B extends A {
//这里,重写了A类的方法, 但是程序员并不知道这里覆盖了父类的func1方法
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
}
例6:
public class Liskov {
public static void main(String[] args) {
B b = new B();
//B类不再继承A类,调用的就是b对象的func1方法,和程序员本意相同
System.out.println("11+3=" + b.func1(11, 3));
System.out.println("11+3+9=" + b.func2(11, 3));
//使用组合仍然可以使用到A类相关方法
System.out.println("11-3=" + b.func3(11, 3));
}
}
//创建一个基类
class Base {
//把基础的方法和成员写到Base类
}
class A extends Base {
public int func1(int num1, int num2) {
return num1 - num2;
}
}
// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends Base {
//如果B需要使用A类的方法,使用组合关系
private A a = new A();
//这里,重写了A类的方法, 可能是无意识
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
//使用A的方法
public int func3(int a, int b) {
return this.a.func1(a, b);
}
}
开闭原则(OCP,全称OpenClosedPrinciple)
一个软件实体如类、模块和函数应该对扩展开放(对提供方而言),对修改关闭(对使用方而言)。用抽象构建框架,用实现扩展细节。
当软件需要变换时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
编程中遵循的其他原则,以及使用设计模式的目的就是遵循开闭原则。
下面举一个例子,例7中,根据m_type不同的值,绘制不同的图形。目前实现了绘制矩形和圆形的方法,如果新增需求绘制三角形,那就需要修改GraphicEditor 类的drawShape方法中的if else判断逻辑,GraphicEditor 类作为使用方,修改该类以及违反了OCP。
例8是对例7的优化,将Shape定义为一个抽象类,提供一个抽象方法draw(),其他类(绘制具体图形的类)继承抽象类Shape,并实现draw()方法,GraphicEditor类中的drawShape()方法只需要根据传入参数的不同,调用不同的实现方法。如果新增需求绘制三角形,只需要再提供一个Triangle类继承Shape抽象类,将Triangle类对象传给GraphicEditor的drawShape()方法即可使用到绘制三角形的draw()方法。这就遵循了OCP原则,对扩展开放(提供方根据新需求提供不同的类,如Triangle),对修改关闭(有新的需求,不需要对使用方GraphicEditor类进行修改)。
例7:
public class Ocp {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
}
}
//用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
}
//绘制矩形
public void drawRectangle(Shape r) {
System.out.println(" 绘制矩形 ");
}
//绘制圆形
public void drawCircle(Shape r) {
System.out.println(" 绘制圆形 ");
}
}
//Shape类,基类
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
例8:
public class Ocp {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
}
}
//用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,调用draw方法
public void drawShape(Shape s) {
s.draw();
}
}
//Shape类,基类
abstract class Shape {
int m_type;
public abstract void draw();//抽象方法
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
@Override
public void draw() {
System.out.println(" 绘制矩形 ");
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
@Override
public void draw() {
System.out.println(" 绘制圆形 ");
}
}
迪米特法则
- 一个对象应该对其他对象保持最少的了解
- 类和类关系越密切,耦合度越大
- 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供public方法,不对外泄漏任何信息。
- 迪米特法则还有个更简单的定义:只与直接的朋友通信
- 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
A依赖B类,A和B可能是继承关系、实现关系、聚合关系、组合关系。A类如果用到B类,调用B类提供的public方法即可,而不关系方法的具体实现。
例9:
import java.util.ArrayList;
import java.util.List;
//客户端
public class Demeter1 {
public static void main(String[] args) {
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
}
//学校管理类
//CollegeEmployee不是直接朋友,而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
//学院员工信息
void printAllEmployee(CollegeManager sub) {
//获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
例10:
import java.util.ArrayList;
import java.util.List;
//客户端
public class Demeter1 {
public static void main(String[] args) {
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
public void printAllEmployee() {
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
//学校管理类
class SchoolManager {
//学院员工信息
void printAllEmployee(CollegeManager sub) {
//获取到学院员工
sub.printAllEmployee();
}
}
合成复用原则
合成复用原则指尽量使用合成/聚合的方式,而不是使用继承
【注】总结自尚硅谷免费公开课程,侵权立即删除