设计模式--简析

Posted 程序猿架构之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式--简析相关的知识,希望对你有一定的参考价值。

无论您是初入职场的码畜,还是久经沙场的码神,设计模式都值得一学。对于那些久经沙场的码神,学习设计模式有助于了解在软件开发过程中所面临的问题的最佳解决方案;对于那些初入职场的码畜,学习设计模式有助于通过一种简单快捷的方式来学习软件设计。

where

70年代后期,一位名叫克里斯托弗·亚历山大(Christopher Alexand er)的建筑师提出了模式概念,激发了对面向对象(OO)社区人们的兴趣,许多创新者开始为软件设计。在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 的书,该书首次提到了软件开发中设计模式的概念。四位作者合称 GOF(四人帮,全拼 Gang of Four)。他们所提出的设计模式主要是基于以下的面向对象设计原则。   
  • 对接口编程而不是对实现编程。    

  • 优先使用对象组合而不是继承。

what

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

设计模式4要素:    
  1. 模式名:用于为定义设计问题和解决方案的模式提供一个有意义的名称。   

  2. 使用场景:描述了问题及其背景。可以描述特定的设计问题,例如如何使用对象表示算法。可以描述类或对象结构的不合理设计。   

  3. 解决方案:描述组成设计的元素、它们的关系、职责和协作。解决方案不是完整的代码,但是它是一个可以用代码实现的模板。相反,模式提供了对设计问题的抽象描述,以及元素的总体安排如何解决该问题。

  4. 结果和影响:通常涉及空间和时间的权衡。还可以处理语言和实现问题。可重用通常是面向对象设计中的一个因素,使用模式可能对系统的灵活性、可扩展性或可移植性产生影响。明确地列出这些影响有助于您理解和评估。

why     
设计模式带来哪些好处
  1. 灵活性:使用设计模式,提高代码灵活性

  2. 可重用性:松散耦合且内聚的对象和类可提高代码的重用性

  3. 可移植性:使得与其他团队成员共享您的代码和思想变得很容易

  4. 健壮性:设计模式已有许多成功的问题解决案例。

简单的说,设计模式提高的代码的灵活性、可重用性、可移植性和健壮性,为开发者提供更好的解决方案。
how

这么多设计模式,我们应该如何选择呢?为了实现针对特定设计问题的正确设计模式,必须对它们有非常深刻的理解。

首先,您需要确定所面临的设计问题的种类。设计问题可以分为创造性,结构性,行为性以及J2EE 设计模式(非GoF)。根据此类别,您可以过滤模式并选择适当的模式。

例如:     
  1.  一个类有太多的实例:它代表的只有一件事,对象的属性的值是相同的,他们只是用作只读:您可以选择这个设计的单例模式问题,确保整个应用程序只有一个实例。它还有助于减少内存大小。    

  2. 类依赖太多:一个类中的更改会影响所有其他从属类:您可以使用桥接器、中介器或命令来解决此设计问题。    

  3. 在代码的两个不同部分中有两个不同的不兼容接口,您需要将一个接口转换为另一个接口,客户端代码使用该接口使整个代码工作,适配器模式适合解决这个问题。   

一个设计模式可以用于解决多个设计问题,一个设计问题可以由多个设计模式解决。可能会有大量的设计问题和解决方案,选择完全适合的模式取决于您对设计模式的了解和理解,还取决于您已经拥有的代码。

分类

创建型模式:创建设计模式用于设计对象的实例化过程。封装了关于系统使用哪些具体类的信息,隐藏了如何创建和组合这些类的实例。整个系统对对象的了解都是抽象类定义的接口。因此,这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。

结构型模式:结构模式关注的是如何组合类和对象以形成更大的结构。继承的概念被用来组合接口和定义组合对象获得新功能的方式。

行为型模式:关心的是对象之间的算法和职责分配,特别关注对象之间的通信。

J2EE模式:特别关注表示层。这些模式是由 Sun Java Center 鉴定的。

下面是设计模式分类图以及关系图

设计模式--简析

设计原则
1. 单一职责原则SRP(Single Responsibility Principle)

每个类都必须恰好只做一件事。换句话说,我们修改类的原因不应多于一个。单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。

我们系统的模型类通常始终遵循SRP原则。可以这么说,我们需要修改系统中用户的状态,我们只需修改User类

public class User { private int id;     private String name;        private List<Address> addresses;        //constructors, getters, setters}
单一职责的优点:   
    1) 降低类的复杂度,一个类只负责一项职责。   
    2) 提高类的可读性,可维护性    
    3) 降低变更引起的风险。
    4) 通常情况下,我们应该遵守单一职责原则。只有逻辑足够简单,才可以在代码级别违反单一职责原则; 只有类中方法数量足够少,可以在方法级别保持单一职责 
2. 开闭原则OCP(Open-Closed Principle) 

一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。抽象化是开闭原则的关键。

假设我们的系统中有一个EventPlanner类,它在我们的生产服务器上长期运行良好 

 public class EventPlanner {  private List<String> participants;      private String organizer;          public void planEvent() {              System.out.println("Planning a simple traditional event");                       ...       }          ... 

但是现在,我们计划使用一个ThemeEventPlanner来代替,它将使用一个随机的主题来规划事件,使它们更加有趣。与其直接跳到现有代码中并添加逻辑来选择事件主题并使用它,不如扩展我们的稳定类 

public class ThemeEventPlanner extends EventPlanner     private String theme;         ... 

对于大型系统,要确定类的所有用途并不是一件很简单的事情。因此,通过扩展功能,我们减少了处理系统未知问题的机会。

3. 里氏替换原则LSP(Liskov Substitution Principle)

所有引用父类的地方必须能透明地使用其子类的对象。

LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。

里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

为了能够实现这一点,我们的子类的对象必须与超类对象的行为完全相同。这个原则帮助我们避免类型之间不正确的关系,因为它们可能会导致意想不到的bug或副作用。

public class Bird    public void fly() {           System.out.println("Bird is now flying");     }}public class Ostrich extends Bird {    @Override        public void fly() {            throw new IllegalStateException("Ostrich can't fly");   }}

虽然鸵鸟是鸟,但它不会飞,所以这明显违反了里氏替代原则(LSP)。此外,涉及类型检查逻辑的代码清楚地表明已经建立了不正确的关系。

遵循LSP有两种重构代码的方法:

1. 消除对象之间不正确的关系

2. 使用Tell, don't ask原则来消除类型检查和强制类型转换

假设我们有一些涉及类型检查的代码:
for(User user : listOfUsers) {  if(user instanceof SubscribedUser) {  user.offerDiscounts();} user.makePurchases();}// 使用Tell, don't ask原则,我们将重构上述代码public class SubscribedUser extends User { @Override  public void makePurchases() {  this.offerDiscounts();  super.makePurchases();  } public void offerDiscounts() {...}}//main method codefor(User user : listOfUsers) {  user.makePurchases(); }

4. 接口隔离原则ISP(Interface Segregation Principle)

根据接口隔离原则,不应该强迫客户端处理它们不使用的方法。我们应该根据需要将较大的接口划分为较小的接口。它还有另外一个意思是:降低类之间的耦合度。

假设我们有一个ShoppingCart接口:

public interface ShoppingCart {     void addItem(Item item);     void removeItem(Item item);    void makePayment();     boolean checkItemAvailability(Item item);}

付款和检查物品的可用性不是购物车的应该做的。我们很有可能遇到这个接口的实现不使用那些方法。因此,最好将上述接口分开

public interface BaseShoppingCart {    void addItem(Item item);    void removeItem(Item item);}public interface PaymentProcessor {    void makePayment();}public interface StockVerifier {    boolean checkItemAvailability(Item item);}
接口隔离原则(ISP)也加强了其他原则   
  1. 单一职责原则:实现较小接口的类通常更内聚,并且通常具有单一目的   

  2. Liskov替换原则:对于较小的接口,我们有更多的机会让实现它们的类完全替代接口

5. 依赖倒置原则DIP(Dependency Inversion Principle)

它是最流行和最有用的设计原则之一,因为它促进了对象之间的松散耦合。抽象不应该依赖于细节,细节应当依赖于抽象,要针对接口编程,而不是针对实现编程。

高级模块告诉我们软件应该做什么。用户授权和支付是高级模块的示例。另一方面,低层模块告诉我们软件应该如何完成各种任务,即它涉及到实现细节。底层模块的一些例子包括安全(OAuth)、网络、数据库访问、IO等。

public interface UserRepository {    List<User> findAllUsers();}public class UserRepository implements UserRepository {      public List<User> findAllUsers() {               //queries database and returns a list of users                       ...      } }

我们在这里提取了接口中模块的抽象。现在,假设我们有高级模块UserAuthorization,它检查用户是否被授权访问系统。我们将只使用UserRepository接口的引用

public class UserAuthorization { ...  public boolean isValidUser(User user) { UserRepository repo = UserRepositoryFactory.create();  return repo.getAllUsers().stream().anyMatch(u -> u.equals(user));     }}

另外,我们使用一个工厂类来实例化一个UserRepository。请注意,我们只是依靠抽象而不是具体。因此,我们可以轻松地添加UserRepository的更多实现,而不会对我们的高级模块造成太大影响。

依赖倒置原则的注意事项和细节:   
  1. 低层模块尽量都要有抽象类或者接口,或者俩者都有,程序稳定性更好 

  2. 变量的声明类型尽量是抽象类或者接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,有利于程序扩展和优化    

  3. 继承时遵循里氏替换原则

6. 迪米特法则DP(Demeter Principle)

一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

迪米特法则还有几种定义形式,包括:不要和”陌生人“说话,只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:  

  1. 当前对象本身  

  2. 以参数形式传入到当前对象方法中的对象  

  3. 当前对象的成员对象  

  4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友  

  5. 当前对象所创建的对象任何一个对象,如果满足上面的条件之一,就是当前对象的”朋友“,否则就是”陌生人“。

在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与”陌生人“发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。

迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用,即通过引入一个合理的第三者来降低现有对象之间的耦合度。

在将迪米特法则运用到系统设计中时,要注意下面几点:    
  1. 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及。    

  2. 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限。    

  3. 在类的设计上,只要有可能,一个类型应当设计成不变类。    

  4. 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。    

  5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。   

  6.  谨慎使用序列化(Serializable)功能。

【例1】明星与经纪人的关系实例。

分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽谈等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则,其类图如图 1 所示

设计模式--简析

7. 合成复用原则(Composite Reuse Principle)

尽量使用合成/聚合的方式,而不是使用继承。

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。

  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。

  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。

  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。

  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
    下面以汽车分类管理程序为例来介绍 合成复用原则的应用。
【例1】汽车分类管理程序。

分析:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。图 1 所示使用继承关系实现的汽车分类的类图。

设计模式--简析

从图 1 可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,

其类图如图 2 所示。

小结:

本文简单介绍了设计模式3wh(where、what、why及how)、分类以及SRP、OCP、LSP、ISP、DIP、DP和CRP的七种设计原则,在设计面向对象的系统时,我们应该尽可能地坚持这些原则。这些原则帮助我们设计一个更具可扩展性、可理解性和可维护性的系统。随着应用程序规模的增长,使用这些原则可以帮助我们节省大量工作。


  欢迎关注:程序员架构之路

                                                                            点“在看”的你,更好看

以上是关于设计模式--简析的主要内容,如果未能解决你的问题,请参考以下文章

功能强大的图片截取修剪神器:Android SimpleCropView及其实例代码重用简析(转)

简析正则表达式

Entitas实现简析

设计模式简析

第五节:JQuery框架源码简析

设计模式--简析