设计模式--面向对象设计原则、UML

来源:互联网 发布:yy网络怎么创建直播间 编辑:程序博客网 时间:2024/05/16 08:01

基础

1、一些常用的UML标记

  1. 用 实线加> 来表示某个类中包含另一个类的实例的意思
  2. 用 实线加△ 来表示某个类继承了另一个类的意思
  3. 用 虚线加△ 来表示某个类实现了某个接口的意思

1.1 实体类的表示

上图就是UML图中实体类的表示方法,类图分成三层:第一层是类名,如果类是抽象的,就用斜体表示;第二层是字段和属性;第三层是操作方法。如果方法或者属性是public的就在前面加’+’,private的加’-‘,procted的加’#’。

1.2 接口的表示

接口的表示和类基本相同,只是它的类名上面加了<<interface>>,而且没有字段和属性一层。

1.3 继承以及实现接口

继承基类或者实现接口的表示方式已经在上图中裂了出来,无需额外的说明。

1.4 聚合

上面的图表示的是两个类之间的聚合关系,它通常用来表示一个类中包含许多个另一个类的实例,通常是指一个类中包含另一个类的数组或者容器。比如,在上图中表示的就是在雁群中可以有很多个大雁,这里的雁群和大雁之间就属于聚合关系。(可以理解成一个类中聚集了很多个另一个类的实例)

1.5 组合

上面表示的就是组合关系。所谓的组合,就是在一个类中包含另一个类的实例,而另一个类是该类的一部分。它和聚合的区别是,聚合中的一个类不是另一个类的一部分(是在另一个类的容器或者数组中)。

在组合中可以在下方标注数字来标识组合的数量关系,比如一个鸟有两个翅膀的话,就在线下面分别用数字1和2来表示。

1.6 依赖

依赖关系与聚合和组合的不同之处在于,依赖关系是指在方法中要依赖其他的类作为参数进行输入。

2、面向对象设计六大原则

2.1 单一责任原则

就一个类而言,应该只有一个引起它发生变化的原因。

比如,我们经常在界面程序中增加各种逻辑,这样的代码在修改起来非常困难,复用不可能,也缺乏灵活性。如果一个类承担的责任过多,就等于把很多责任耦合在一起,一个责任的变化可能会影响或者削弱这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化的时候,设计会遭到意想不到的破坏。

2.2 开放-封闭原则

软件实体(类、模块、函数等)应该对拓展开放,对修改关闭。淡然,绝对的封闭是不可能的,但是我们在设计的时候,应该猜出最有可能发生变化的部分,然后构造抽象来隔离变化。所以,当有了新需求的时候,我们的代码应该是通过添加新的代码来进行的,而不是修改现有的代码。

典型的例子是策略模式,以及策略+工厂,将两者结合,我们就只需要修改工厂和增加新的策略,来为程序添加新的功能。

2.3 依赖倒转原则

依赖倒转有两层意思:

  1. 高层模块不应该依赖底层模块,两个都应该依赖抽象;
  2. 抽象不应该依赖细节,而细节应该依赖抽象。

所谓的“高层模块不应该依赖底层模块”,就是说,我们在写代码的时候,不应该在一个类中通过库函数的形式调用另一类的方法。常见的方式是,将一些操作封装成一个Helper方法,然后在Service层中调用这些方法。

所谓的依赖于抽象应该是指面向接口编程或者是抽象类,也就是只声明接口类型,然后将接口的具体是实现赋值给它。这样做的好处是,假如某个接口的实现由问题,那么我们只需要修改这个接口的实现,但是接口前后的逻辑是不需要发生变化的。

2.4 里氏替代原则

子类型必须能够替换它们的父类型

如果软件实体使用的是父类,那么一定适用于其子类。也就是把父类都替换成子类,程序的行为不会发生变化。比如,我们在程序中的所有位置都使用的是接口,如果我们要将接口替换成该接口的子类,那么程序的行为不会发生变化。

里氏代换原则的4层含义

1.子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法

里氏代换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏代换原则就是表达了这一层含义。

2.子类中可以增加自己特有的方法

3.当子类的方法实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松

4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格

2.5 迪米特法则

迪米特法则:也称为最少知识原则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public方法,我就调用这么多,其他的一概不关心。

下面的例子来自csdn博客

// 类A和类B是好朋友,能找类B来帮忙public class A{   private String name;   public A(String name) {       this.name = name;   }   public B getB(String name) {       return new B(name);   }   public void work() {       B b = getB("李四");       C c = b.getC("王五");       c.work();   }}// 类B和类C是好朋友,能知道类C能办成此事public class B {   private String name;   public B(String name){       this.name = name;   }   public C getC(String name) {       return new C(name);   }}// 类C能够办成此事public classC {   public String name;   public C(String name) {       this.name = name;   }   public void work() {       System.out.println(name + "把这件事做好了");   }}// 客户端public classClient {   public static void main(String[] args) {       A a = new A("张三");       a.work();   }}

上面的程序最终的输出结果是:

王五把这件事情做好了

但是因为在A中调用了C来解决这个问题,而C和A本身是没有联系的。这种场景在实际开发中是非常常见的一种情况。对象A需要调用对象B的方法,对象B有需要调用对象C的方法……就是常见的getXXX().getXXX().getXXX()。所以,如果在一个类中通过另一个类的getter方法获取了其他的实例时,就应该注意,可能导致代码之间的耦合过深了。

下面是使用迪米特法则之后的结果:

// 类A和类B是好朋友,能找类B来帮忙public class A {   public String name;   public A(String name) {       this.name = name;   }   public B getB(String name) {       return new B(name);   }   public void work() {       B b = getB("李四");       b.work();   }}// 类B和类C是好朋友,能知道类C能办成此事public class B {   private String name;   public B(String name) {       this.name = name;   }   public C getC(String name) {       return new C(name);   }   public void work() {       C c = getC("王五");       c.work();   }}// 类C能够办成此事public classC {   public String name;   public C(String name) {       this.name = name;   }   public void work() {       System.out.println(name + "把这件事做好了");   }}// 客户端public classClient {   public static void main(String[] args) {       A a = new A("张三");       a.work();   }}

上修改的代码中,我们在B和A中分别只获取它们的关联的类来处理。虽然最终的输出结果是一样的,但是这种方式不会像上面那样耦合过深,便于对代码进行修改。

关于应用迪米特法则的注意事项:

  1. 在类的划分上,应该创建有弱耦合的类;
  2. 在类的结构设计上,每一个类都应当尽量降低成员的访问权限;
  3. 在类的设计上,只要有可能,一个类应当设计成不变类;
  4. 在对其他类的引用上,一个对象对其它对象的引用应当降到最低;
  5. 尽量降低类的访问权限;
  6. 谨慎使用序列化功能;
  7. 不要暴露类成员,而应该提供相应的访问器(属性)。

2.6 合成/聚合原则

合成/聚合原则,尽量使用合成/聚合,尽量不要使用类继承。

在面向对象设计中,如果直接继承基类,会破坏封装,因为继承将基类的实现细节暴露给子类。如果基类的实现发生改变,则子类的实现也不得不发生改变。从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性。于是就提出了合成/聚合复用原则,也就是在实际开发设计中,尽量使用合成/聚合,不要使用类继承

继承复用与合成/聚合复用

1.继承复用

继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显地捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展父类的实现。继承是类型的复用。

继承复用的优点:

  1. 新的实现较为容易,因为超类的大部分功能可通过继承关系自动进入子类;
  2. 修改或扩展继承而来的实现较为容易。

继承复用的缺点:

  1. 继承复用破坏封装,因为继承将细节暴露给子类;
  2. 如果父类的实现发生改变,那么子子类的实现也不得不发生改变;
  3. 从父类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。

2.合成/聚合复用

由于合成/聚合可以将已有对象纳入到新对象中,使之成为新对象的一部分,所以新对象可以调用已有对象的功能

合成/聚合复用的优点:

  1. 该复用支持封装;
  2. 该复用所需的依赖较少;
  3. 每个新的任务可将焦点集中在一个任务上。

合成/聚合复用的缺点:

  1. 通过这种复用建造的系统会有较多的对象需要管理;
  2. 为了能将多个不同的对象作为组合块来使用,必须仔细地对接口进行定义。

3、针对接口编程

所谓针对接口编程就是,假如有一个Animal接口,而Dog和Cat实现了它,那么当我们想要创建一个Dog的实例的时候,我们这样定义:

Animal dog = new Dog();

而不是

Dog dog = new Dog();

这样的好处是,如果你想要修改dog的实现,将其替换成一个cat,那么你可以使用setter方法注入。也就是说,当我们使用接口的时候,我们可以有更多的选择的余地。因为只要是Animal的实现,我们都可以将其赋值给dog.

4、内聚

内聚用来都连一个类或模块紧密地达到单一目的或责任。当一个模块或一个类被设计成只支持单一相关的功能时,我们说它具有高内聚;反之,被设计成一组不想干的功能时,我们称它具有低内聚。


更多内容

1、该项目整理了设计模式、Java语法、JVM、SQL、数据结构与算法等相关内容:https://github.com/Shouheng88/Java-Programming。

2、由于时间仓促,不免于存在错误,欢迎批评指正。

原创粉丝点击