Java对象设计通用原则之核心原则

  由于对象设计的核心是类,所以下面的原则也都基本都是讨论类的设计问题,其它类型的元素都比较简单,基本上也符合大多数这里列出的原则。

  前面我们分析完了对象设计的基本原则,这里我将重新温习一下对象设计的核心原则 - SOLID原则。几乎所有的设计模式都可以看到这些原则的影子。

单一职责原则(SRP):做一个专一的人

  单一职责原则的全称是Single Responsibility Principle,简称是SRP。SRP原则的定义很简单:

即不能存在多于一个导致类变更的原因。简单的说就是一个类只负责一项职责。

  让一个类仅负责一项职责,如果一个类有多于一项的职责,这是比较脆弱的设计。因为一旦某一项职责发生了改变,需要去更改代码,那么有可能会引起其他职责改变。所谓牵一发而动全身,这显然是我们所不愿意看到的,所以我们会把这个类分拆开来,由两个类来分别维护这两个职责,这样当一个职责发生改变,需要修改时,不会影响到另一个职责。

  做且只做好一件事,这一条原则其实不仅仅适用于对象,也同样适用于函数,变量等一切编程元素。当然在商业模式中,将一件事做到极致就是成功,我个人觉得这一条也还是成立的。

  随便举个例子,如果大家有深入研究过迭代器的思想的话,那其实就是把存储数据和遍历数据的职责分开了,集合只负责实现存储数据的功能,而迭代器完成遍历数据的功能。

  再说我看到过的一个例子:说有一个辅助类CommonUtil,在这里面提供了所有不能归入其他模块的辅助方法,它的结构如下:

public class CommonUtil
{
 #region Canvas Helpers
 public void M1() { }
 //...
 #endregion

 #region Screen Helpers
 public void M2() { }
 //...
 #endregion

 #region Size Helpers
 public void M3() { }
 //...
 #endregion

 #region Data Helpers
 public void M4() { }
 //...
 #endregion
}

各位,你觉得这个类写的怎么样?

  这里面放进了各种不同类型的辅助方法,每个模块有辅助方法需要找地方放的时候,人们都是自觉的找到了这个类,于是这个类在每个Release中都不断有新成员加入,于是最终变成了一个庞然大物,使用的时候,光看函数列表都够大家喝一壶了。

  我的想法是,为什么不拆分成4个小类,每个类专门负责某一类型辅助功能呢?

开放封闭原则(OCP):改造世界大部分不是破坏原来的秩序

  开放封闭原则全称是Open Closed Principle, 简称OCP, 该原则的定义是:

软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

这条原则是所有面向对象原则的核心。

  软件设计所追求的第一个目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其它的原则或多或少都是为了这个目标而努力的,例如以Liskov替换原则实现最佳的、正确的继承层次,就能保证不会违反开放封闭原则。

  软件设计所最求的第二个目标就是重用。这个是继承机制的核心动力。由于通常来说抽象的东西最稳定,最不容易变化,所以抽象与继承是实现开闭原则强大的工具,但不是唯一工具,后面我们会说到实现开闭原则的另一个更加强大,更加灵活的工具:组合。

  一言以蔽之,继承与组合是封装变化,降低耦合的不二法门。能否合理的使用继承和组合是体现一名码农水平高低的又一标准。

  在实际的代码中,添加新的功能一般意味着新的对象,一个好的设计也意味着这个新的修改不要大幅度波及现有的对象。这一条理解起来最简单,实施起来却是最困难。无数的模式和解耦方法都是为了达到这个目的而诞生的。

  看一个经典的例子:

public class Component
{
 public enum Status
 {
  None,
  Installed,
  Uninstalled
 }

 Status m_status = Status.None;

 void Do()
 {
  switch (m_status)
  {
   case Status.None:
    Console.WriteLine("Error...");
    break;
   case Status.Installed:
    Console.WriteLine("Hello!");
    break;
   case Status.Uninstalled:
    Console.WriteLine("Error...");
    break;
   default:
    break;
  }
 }
}

  我们这里定义了一个组件,用户动态加载,加载完了以后程序就可以用了,为了处理方便,我们给组件定义了一些状态,在不同的状态下,这个组件有不同的行为,于是就有了上面的代码:enum定义状态,函数中使用switch实现路由。

  使用switch分支是一种经典的做法,当组件的状态类型不存在变化的可能时,该段代码无可挑剔,堪称完美。

  可是在实际项目中,过了一阶段,我们发现组件的状态不够,比如说我们需要处理组件还未配置时的行为,于是我们在枚举中加了一个状态:Configured,然后在switch中加了一个分支。

  又过了一阶段,我们又发现还需要处理组件还未初始化时的行为,于是我们在枚举中又加了一个状态:Initialized,然后在switch中加了一个分支。

  至于以后是否还需要别的状态,我们目前不得而知,应该说还是有可能的。

  上面这个行为是严重违反开闭原则的,这个不用多讲了吧。那么如何改进呢?使用我们最强大的工具吧:使用继承或/和组合封装变化点

  这里我们分析一下,该组件存在变化的地方就是组件的状态,这是一个变化点,对于变化点,对于变化点不要手软,封印它。

public class ComponentStaus
{
 public virtual void Do() { }
}
public class ComponentNone : ComponentStaus
{
 public override void Do() { Console.WriteLine("Error..."); }
}
public class ComponentInitialized : ComponentStaus
{
 public virtual void Do() { Console.WriteLine("Hello!"); }
}

public class Component
{
 ComponentStaus m_status = new ComponentNone();

 public void ChangeStatus(ComponentStaus newStatus)
 {
  m_status = newStatus;
 }

 public void Do()
 {
  m_status.Do();
 }
}

  在上面的例子中,我们发现了变化点,然后抽象出一个基类放在那,然后使用继承机制,让子类去演绎变化。当我们需要添加新的状态Configured的时候,我们只要添加一个新的子类ComponentConfigured,让它从ComponentStaus继承,并重写Do方法即可。使用的时候,在合适的时机(如事件处理中)把该子类的实例传给Component就可以了,当然也有可能是Component自己处理事件或方法时自己修改该状态实例。

  能看到开闭原则的影子吗?(当然,不要妄想对修改完全封闭,这个是不可能的,就像组件之间零依赖是不可能的一样)

里氏替换原则(LSP):长大后,我就成了你

  里氏替换原则全称Liskov Substitution Principle,简称 LSP,它的定义是:

任何基类可以出现的地方,子类一定可以出现。

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

  LSP原则保证了继承的正确实现。它希望子类不要破坏父类的接口成员。一旦破坏了,就如果人与人之间破坏合同一样,有时候会很糟糕。

  这个原则看起来也很容易,但是却也很容易和现实中的概念混淆,看个经典的小例子:长方形与正方形问题。

  在我们小学学数学的时候,就知道正方形是特殊的长方形,于是写代码的时候,自然的正方形类就继承自长方形了,代码如下:

public class Program
{
 static void Main(string[] args)
 {
  Rectangle rect = new Rectangle();
  rect.setWidth(100);
  rect.setHeight(20);
  Console.WriteLine(rect.Area == 100 * 20);

  Rectangle squ = new Square();
  rect.setWidth(100);
  rect.setHeight(20);
  Console.WriteLine(squ.Area == 100 * 20);
 }
}

class Rectangle
{
 public double m_width;
 public double m_height;

 public virtual void setWidth(double width) { m_width = width; }
 public virtual void setHeight(double height) { m_height = height; }

 public double Area
 {
  get { return m_width * m_height; }
 }
}

class Square : Rectangle
{
 public override void setWidth(double width)
 {
  m_width = width;
  m_height = width;
 }

 public override void setHeight(double height)
 {
  m_width = height;
  m_height = height;
 }
}

  很显然输入的不是两个True,根本原因就在于正方形只有长的概念,而没有长方形所期望的宽的概念,所以长方形中定义了正方形根本没有的东西,也就是说长方形不应该是正方形的基类。

  当一个基类出现了其子类不想要的接口成员时,继承关系必然是欠缺考虑的继承,也必然是违反LSP原则的。这个时候要么把想办法把基类的那个成员抽象出去,要么子类再选择从合适的基类继承。记住这个思路,在下一个原则我们还会再相见。

  此外,当我在小孩玩橡皮鸭子的时候,常常在想:橡皮鸭子能从鸭子继承吗?你觉得呢?

接口分离原则(ISP):不要一口吃成胖子

  接口分离原则全称interface segregation principle,简称ISP,它的定义是:

不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

  这一原则与单一职责原则息息相关,它们对于高内聚的追求是一致的,但是它更加强调了接口的高内聚性。

  看个例子,我们有一个服务接口,是这么定义的:

interface IService
{
 void GetUser();
 void RegisterUser();
 void LoadProducts();
 void AddProduct();
 void AcceptRequest();
 void SendResponse();
}

  因为是面向所有Client的,所以这个接口提供了所有Client需要的方法,比如用户的操纵,产品的操作,数据传输的一些操作,每个Client都可能用到其中的一部分服务。

  这个设计运行很好,服务端提供一个类Service实现这个接口,而Client,它通过某些网络服务方式获取到这个接口IService就可以了,然后直接调用相关方法就可以了。

  先说第一点,这个接口违反了单一职责原则,一个字,""。

  再说第二点,每个类型的Client只处理一种对象,比如有的Client,如工资系统只处理User,而仓库系统只处理Product,接口的其它方法对它们没用,还是一个字,""。

  于是得到下列接口:

interface IUser
{
 void GetUser();
 void RegisterUser();
}
interface IProduct
{
 void LoadProducts();
 void AddProduct();
}
interface IPeer
{
 void AcceptRequest();
 void SendResponse();
}
class Service : IUser, IProduct, IPeer {}

  这样拿到代理对象后,想处理用户的Client,将该对象转换成IUser即可,想处理产品的转换成IProduct即可。

  同样的,试想一下,如果某一天某Service只提供有关用户的服务,在原先的设计中会怎么样?

依赖倒置原则(DIP):抽象的艺术才有生命力

  依赖倒置原则全称Dependence Inversion Principle,简称DIP,它的定义有3点含义:

1、高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)
2、抽象(抽象类或接口)不应该依赖于细节(具体实现类)
3、细节(具体实现类)应该依赖抽象

  总结起来,这个原则说的就是每个类与别的类交互时,尽量只使用满足接口规范的抽象类。为啥?因为抽象类实现细节几乎没有,没什么需要变化的。这一条深刻揭示了抽象的生命力,抽象的对象才是最有表达能力的对象,因为它通常是“无形”的,可以随时填充相关的细节。

  直接看一个例子:

public class Program
{
 static void Main(string[] args)
 {
  UI layer = new UI();
  layer.SetDataAccessor(new XmlDataAccessor());
  layer.Do();
 }
}

class UI
{
 DataAccessor m_accessor;

 public void SetDataAccessor(DataAccessor accessor) { m_accessor = accessor; }
 public void Do()
 {
  m_accessor.GetUser();
 }
}

interface DataAccessor
{
 void GetUser();
 void RegisterUser();
}

class XmlDataAccessor : DataAccessor
{
 public void GetUser() { }
 public void RegisterUser() { }
}

  这里上游的组件UI依赖的是DataAccessor这样的接口,而不是依赖各种具体的子类,如XmlDataAccessor,这样当想使用其他的数据库存储数据的时候,只要增加新的DatabaseDataAccessor之类的新类,然后在设置的时候设置一下就可以了。这种手段,很多人也称为"依赖注入"。

  好了,核心原则说完了,总结一下,似乎就是一句话:"类要单纯,继承要谨慎,变化要封装,抽象类型要多用"。

时间: 2024-10-13 14:27:31

Java对象设计通用原则之核心原则的相关文章

写给自己看的小设计3 - 对象设计通用原则之核心原则

由于对象设计的核心是类,所以下面的原则也都基本都是讨论类的设计问题,其它类型的元素都比较简单,基本上也符合大多数这里列出的原则. 前面我们分析完了对象设计的基本原则,这里我将重新温习一下对象设计的核心原则 - SOLID原则.几乎所有的设计模式都可以看到这些原则的影子. 单一职责原则(SRP):做一个专一的人 单一职责原则的全称是Single Responsibility Principle,简称是SRP.SRP原则的定义很简单: 即不能存在多于一个导致类变更的原因.简单的说就是一个类只负责一项

写给自己看的小设计4 - 对象设计通用原则之扩展原则

除了前面学习的那些核心原则,还有一些衍生的原则,掌握它们,你将更好的面向对象.不妨称它们为"扩展原则"吧. 迪米特法则:尽量不与无关的类发生关系. 迪米特法则全称Law of Demeter,简称LoD,也称为最少知识原则(Least Knowledge Principle,LKP).这个原则没什么固定的定义,大体上有这么几种说法: 1. 只与你的朋友说话 2. 不和陌生人说话 3. 对象应该只与必须交互的对象通信 通俗地讲,一个类应该对自己需要调用的类知道得最少,你调用的类的内部是如

《设计模式之禅》笔记整理--面对对象设计六大原则

第一章.面对对象设计六大原则: (1).单一职责原则:应该有且只有一个原因引起类的变更. 为什么要用单一职责原则:(1).类的复杂性降低,实现什么职责都有清晰明确的定义. (2).可读性提高,复杂性降低,当然可读性提高了. (3).可维护性提高,可读性提高,当然更容易维护了. (4).变更引起的风险降低,一个接口修改,只对相应的实现类有影响. 职责划分的例子:电话过程可以划分为两个职责:(1).协议管理(2).数据传送 :RBAC模型,基于角色的访问控制 (2).里氏替换原则:目的:增强程序的健

java面向对象设计的原则

一.针对java类的6大设计原则 1.单一职责原则(Single Responsibility Principle,SRP) 即:对一个类而言,有且仅有一个引起它变化的原因.否则的话就应该把这个类进行拆分.在设计时让一个类只负责一种类型的责任. 单一职责原则的核心就是控制类的粒度大小.将对象解耦.提高内聚性.如果遵循单一职责原则将有以下优点: 降低类的复杂度.一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多. 提高类的可读性.复杂性降低,其可读性自然会提高. 提高系统的可维护性.可读性提

Java面向对象设计原则

面向对象设计原则是OOPS(Object-Oriented Programming System,面向对象的程序设计系统)编程的核心,但大多数Java程序员追逐像Singleton.Decorator.Observer这样的设计模式,而不重视面向对象的分析和设计.甚至还有经验丰富的Java程序员没有听说过OOPS和SOLID设计原则,他们根本不知道设计原则的好处,也不知道如何依照这些原则来进行编程. 众所周知,Java编程最基本的原则就是要追求高内聚和低耦合的解决方案和代码模块设计.查看Apac

Effective Java:对于所有对象都通用的方法

前言: 读这本书第1条规则的时候就感觉到这是一本很好的书,可以把我们的Java功底提升一个档次,我还是比较推荐的.这里我主要就关于覆盖equals.hashCode和toString方法来做一个笔记总结,希望能够与君共勉. 概述: 这一章主要是说明一些对于所有对象都通用的方法.我们知道Java的多态是其特色之一,而多态的体现方式中就有一种方式叫做"重写".这些概念性的东西我想在大学我们学习Java的初期,老师就会如数家珍一样地灌输给我们,不过,在那个时候有多少人真的了解了什么是重载,什

[Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- ?条

这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对对象进行相等的比较: public boolean equals(Object obj) { return (this == obj); } 那么什么时候需要覆盖equals呢? 当你的类有自己的逻辑相等,而不是对象相等时,应该自己实现equals,比如Date和Interger,他们的相等比较不仅

Effective Java读书笔记(3对于所有对象都通用的方法)

3.1 覆盖equals时请遵守通用约定 什么时候应该覆盖Object.equals()方法呢? 如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法. Object.equals()方法具有自反性.对称性.传递性.一致性和与null比较返回false的特点. 实现高质量equals方法的诀窍: (1)使用==操作符检查"参数是否为这个对象的引用".如果是,则返回true,这

浅谈Java六大设计原则

笔者刚接触设计原则的时候,觉得一头雾水,不知道他有什么用.在经历了一段时间的代码加上了解Java设计模式之后.笔者忽然觉得自己以前写的代码就是一堆*.所以,笔者认为设计原则和设计模式对于软件编程设计(非码农)来说是至关重要的事情.相信很多学习编程的人,和我有同样的感受. 我对设计模式和设计原则的理解是:如果把程序员比作武侠,那么设计模式就是修炼内功的易筋经,设计原则就是修炼内功的心法总纲,而具体的技术实现(代码编写)就是罗汉拳.如果你只想自保,那么会罗汉拳就可以了(能够用代码实现功能),不过如果