铁伞怪侠有电视剧:一些面向对象的设计法则

来源:百度文库 编辑:九乡新闻网 时间:2024/04/30 03:44:46

一些

面向对象的设计

法则

 

Bob Tarr 著

outmyth

mahope

      

 


n          

法则#1:将类和成员的可访问性最小化

[Minimize The Accessibility of Classes and Members]

抽象的含义

n         Tony Hoare: “抽象起源于对真实世界中的对象、事态、过程之间的相似性的认识,以及在聚集这些相似性的同时忽略差异性的决心” ;

n         Grady Booch: “抽象表示一个对象区别与其他类别的对象的本质特征,从而相对于观察者的角度来说,为其提供了明确定义的概念边界”;

n         抽象是处理复杂事物的基本方法之一;

n         抽象关注对象的外部视图并将其行为与它的实现隔离开来。

封装

n         Grady Booch: “封装是划分抽象的元素的过程,这些元素构成了抽象的结构和行为;封装的功能是将抽象的契约接口与其实现分开”;

n         Craig Larman: “封装是隐藏对象的数据、内部结构和实现细节的机制。所有与对象的交互都是通过公共操作这个接口”;

n         类应该是不透明的;

n         类不应暴露其内部实现细节。

Java中的信息隐藏

n         尽可能第使用私有成员和相应的访问器/修改器(accessors and mutators)

n         举例:

替换

public double speed;

     private double speed;

     public double getSpeed()

     {

         return (speed);

     }

     public void setSpeed(double newSpeed)

     {

         speed = newSpeed;

}

使用访问器/修改器,而不是共有成员

n         你可以在值上附加约束条件

     public void setSpeed(double newSpeed)

     {

         if (newSpeed < 0)

         {

              sendErrorMessage(...);

              newSpeed = Math.abs(newSpeed);

         }

         speed = newSpeed;

     }

n         如果使用你的类的用户直接访问类的字段,那么他们也会担负检查约束条件的责任

n         你可以改变类的内部表示而不用改变接口

     // Now using metric units (kph, not mph)

     public void setSpeedInMPH(double newSpeed)

     {

         speedInKPH = convert(newSpeed);

     }

     public void setSpeedInKPH(double newSpeed)

     {

         speedInKPH = newSpeed;

     }

n         你可以执行任意的附加效果

     public double setSpeed(double newSpeed)

     {

         speed = newSpeed;

         notifyObservers();

     }

n         如果使用你的类的用户直接访问类的字段,那么他们也会担负执行附加效果的责任

法则#2:优先使用(对象)组合,而非(类)继承

[ Favor Composition Over Inheritance ]

组合

n         (对象)组合是一种通过创建一个组合了其它对象的对象,从而获得新功能的复用方法。

n         将功能委托给所组合的一个对象,从而获得新功能。

n         有些时候也称之为“聚合”(aggregation)或“包容”(containment),尽管有些作者对这些术语赋予了专门的含义

n         例如:

F        聚合:一个对象拥有另一个对象或对另一个对象负责(即一个对象包含另一个对象或是另一个对象的一部分),并且聚合对象和其所有者具有相同的生命周期。(译者注:即所谓的“同生共死”关系,可参见GOF的Design Patterns: Elements of Reusable Object-Oriented Software的引言部分。)

F        包容:一种特殊类型的组合,对于其它对象而言,容器中的被包含对象是不可见的,其它对象仅能通过容器对象来访问被包含对象。(Coad)

 

n         包含可以通过以下两种方式实现:

F        根据引用(By reference)

F        根据值(By value)

n         C++允许根据值或引用来实现包含。

n         但是在Java中,一切皆为对象的引用!

组合的优点和缺点

n         优点:

n         :

F        容器类仅能通过被包含对象的接口来对其进行访问。

F        “黑盒”复用,因为被包含对象的内部细节对外是不可见。

F        对装性好。

F        实现上的相互依赖性比较小。(译者注:被包含对象与容器对象之间的依赖关系比较少)

F        每一个类只专注于一项任务。

F        通过获取指向其它的具有相同类型的对象引用,可以在运行期间动态地定义(对象的)组合。

 

n         缺点:

F        从而导致系统中的对象过多。

F        为了能将多个不同的对象作为组合块(composition block)来使用,必须仔细地对接口进行定义。

继承

n         (类)继承是一种通过扩展一个已有对象的实现,从而获得新功能的复用方法。

n         泛化类(超类)可以显式地捕获那些公共的属性和方法。

n         特殊类(子类)则通过附加属性和方法来进行实现的扩展。

继承的优点和缺点

n         优点:

F        容易进行新的实现,因为其大多数可继承而来。

F        易于修改或扩展那些被复用的实现。

n         缺点:

F        破坏了封装性,因为这会将父类的实现细节暴露给子类。

F        “白盒”复用,因为父类的内部细节对于子类而言通常是可见的。

F        当父类的实现更改时,子类也不得不会随之更改。

F        从父类继承来的实现将不能在运行期间进行改变。

继承 vs 组合 的示例

n         这个例子来自Effective Java(Joshua Bloch著)

n         假设我们希望一个HashSet变量跟踪试图插入的数量,那么我们采取继承HashSet如下所示:

public class InstrumentedHashSet extends HashSet

{

     // The number of attempted element insertions

     private int addCount = 0;

     public InstrumentedHashSet(Collection c) { super(c); }

     public InstrumentedHashSet(int initCap, float loadFactor)

     {

         super(initCap, loadFactor);

     }

     public boolean add(Object o)

     {

         addCount++;

         return super.add(o);

     }

     public boolean addAll(Collection c)

     {

         addCount += c.size();

         return super.addAll(c);

     }

     public int getAddCount()

     {

         return addCount;

     }

}

 

n         看起来都还不错。但是让我们检验一下!

     public static void main(String[] args)

     {

         InstrumentedHashSet s = new InstrumentedHashSet();

         s.addAll(Arrays.asList(new String[] { "Snap", "Crackle", "Pop" }));

         System.out.println(s.getAddCount());

     }

n         我们得到6这个结果,而不是所期望的3。为什么?

n         因为超类HashSet自身中的addAll()的内部实现调用了add()方法。所以首先我们在InstrumentedHashSet 的addAll()中给addCount加了3,接着我们调用HashSet的addAll()。对于每个元素,这个addAll()调用add()方法,后者因为被InstrumentedHashSet重写,对每个元素都加上1。结果是每个元素都被二次计数了。

n         有很多办法解决这个问题,但是注意子类的脆弱性。子类的实现细节影响了其上的操作。

n         解决这个问题最好的办法是使用组合。让我们写一个组合了Set对象的InstrumentedSet 类,我们的InstrumentedSet 类将复制Set的接口,但所有的Set操作实际上将被前转到内部的Set对象。

n         InstrumentedSet被称为包装类,因为它包装了一个Set对象的实例。

n         这是一个通过组合代理的例子!

public class InstrumentedSet implements Set

{

     private final Set s;

     private int addCount = 0;

     public InstrumentedSet(Set s) { this.s = s; }

     public boolean add(Object o)

     {

         addCount++;

         return s.add(o);

     }

     public boolean addAll(Collection c)

     {

         addCount += c.size();

         return s.addAll(c);

     }

     public int getAddCount() { return addCount; }

     // Forwarding methods (the rest of the Set interface methods)

     public void clear() { s.clear(); }

     public boolean contains(Object o) { return s.contains(o); }

     public boolean isEmpty() { return s.isEmpty(); }

     public int size() { return s.size(); }

     public Iterator iterator() { return s.iterator(); }

     public boolean remove(Object o) { return s.remove(o); }

     public boolean containsAll(Collection c)

     { return s.containsAll(c); }

     public boolean removeAll(Collection c)

     { return s.removeAll(c); }

     public boolean retainAll(Collection c)

     { return s.retainAll(c); }

     public Object[] toArray() { return s.toArray(); }

     public Object[] toArray(Object[] a) { return s.toArray(a); }

     public boolean equals(Object o) { return s.equals(o); }

     public int hashCode() { return s.hashCode(); }

     public String toString() { return s.toString(); }

n         注意几点:

F        这个类是一个Set

F        它有一个构造器参数为Set

F        被包含的Set对象可以是任何实现了Set接口的类(并不仅仅是HashSet)

F        这个类非常灵活,可以包装任何已存在的Set对象

n         举例

List list = new ArrayList();

     Set s1 = new InstrumentedSet(new TreeSet(list));

     int capacity = 7;

     float loadFactor = .66f;

     Set s2 = new InstrumentedSet(new HashSet(capacity, loadFactor));

Coad规则

仅当下列的所有标准被满足时,方可使用继承:

n         子类表达了“是一个…的特殊类型”,而非“是一个由…所扮演的角色”。

n         子类的一个实例永远不需要转化(transmute)为其它类的一个对象。

n         子类是对其父类的职责(responsibility)进行扩展,而非重写或废除(nullify)。

n         子类没有对那些仅作为一个工具类(utility class)的功能进行扩展。

n         对于一个位于实际的问题域(Problem Domain)的类而言,其子类特指一种角色(role),交易(transaction)或设备(device)。

继承/组合示例1


图表 1

n         “是一个…的特殊类型”,而非“是一个由…所扮演的角色”

F        失败。乘客是人所扮演的一种角色。代理人亦然。

n         永远不需要转化

F        失败。随着时间的发展,一个Person的子类实例可能会从Passenger转变成Agent,再到Agent Passenger。

n         扩展,而非重写和废除

F        通过。

n         不要扩展一个工具类

F        通过。

n         在问题域内,特指一种角色,交易或设备

F        失败。Person不是一种角色,交易或设备。

继承并非适用于此处!

 

使用组合进行挽救!


图表 2

继承/组合示例2


图表 3

n         “是一个…的特殊类型”,而非“是一个由…所扮演的角色”

F        通过。乘客和代理人都是特殊类型的人所扮演的角色。

n         永远不需要转化

F        通过。一个Passenger对象将保持不变;Agent对象亦然。

n         扩展,而非重写和废除

F        通过。

n         不要扩展一个工具类

F        通过。

n         在问题域内,特指一种角色,交易或设备

F        通过。PersonRole是一种类型的角色。

继承适用于此处!

继承/组合示例3


图表 4

n         “是一个…的特殊类型”,而非“是一个由…所扮演的角色”

F        通过。预订和购买都是一种特殊类型的交易。

n         永远不需要转化

F        通过。一个Reservation对象将保持不变;Purchase对象亦然。

n         扩展,而非重写和废除

F        通过。

n         不要扩展一个工具类

F        通过。

n         在问题域内,特指一种角色,交易或设备

F        通过。是一种交易。

继承适用于此处!

继承/组合示例4

图表 5

n         “是一个…的特殊类型”,而非“是一个由…所扮演的角色”

F        失败。预订不是一种特殊类型的observable。

n         永远不需要转化

F        通过。一个Reservation对象将保持不变。

n         扩展,而非重写和废除

F        通过。

n         不要扩展一个工具类

F        失败。Observable就是一个工具类。

n         在问题域内,特指一种角色,交易或设备

F        不适用。Observable是一个工具类,并非一个问题域的类。。

继承并非适用于此处!

继承/组合总结

n         组合与继承都是重要的重用方法

n         在OO开发的早期,继承被过度地使用

n         随着时间的发展,我们发现优先使用组合可以获得重用性与简单性更佳的设计

n         当然可以通过继承,以扩充(enlarge)可用的组合类集(the set of composable classes)。

n         因此组合与继承可以一起工作

n         但是我们的基本法则是:

优先使用对象组合,而非(类)继承
[ Favor Composition Over Inheritance ]

法则#3:针对接口编程,而非(接口的)实现

[ Program To An Interface, Not An Implementation ]

接口

n         接口是一个对象在对其它的对象进行调用时所知道的方法集合。

n         一个对象可以有多个接口(实际上,接口是对象所有方法的一个子集)

n         类型是对象的一个特定的接口。

n         不同的对象可以具有相同的类型,而且一个对象可以具有多个不同的类型。

n         一个对象仅能通过其接口才会被其它对象所了解。

n         某种意义上,接口是以一种非常局限的方式,将“是一种…”表达为“一种支持该接口的…”。

n         接口是实现插件化(pluggability)的关键

实现继承和接口继承

n         实现继承类继承):一个对象的实现是根据另一个对象的实现来定义的。

n         接口继承子类型化):描述了一个对象可在什么时候被用来替代另一个对象。

n         C++的继承机制既指类继承,又指接口继承。

n         C++通过继承纯虚类来实现接口继承。

n         Java对接口继承具有单独的语言构造方式-Java接口。

n         Java接口构造方式更加易于表达和实现那些专注于对象接口的设计。

接口的好处

n         优点:

F        Client不必知道其使用对象的具体所属类。

F        一个对象可以很容易地被(实现了相同接口的)的另一个对象所替换。

F        对象间的连接不必硬绑定(hardwire)到一个具体类的对象上,因此增加了灵活性。

F        松散藕合(loosens coupling)。

F        增加了重用的可能性。

F        提高了(对象)组合的机率,因为被包含对象可以是任何实现了一个指定接口的类。

n         缺点:

F        设计的复杂性略有增加

(译者注:接口表示“…像…”(LikeA)的关系,继承表示“…是…”(IsA)的关系,组合表示“…有…”(HasA)的关系。)

接口实例

/**

     * Interface IManeuverable provides the specification

     * for a maneuverable vehicle.

     */

     public interface IManeuverable

     {

         public void left();

         public void right();

         public void forward();

         public void reverse();

         public void climb();

         public void dive();

         public void setSpeed(double speed);

         public double getSpeed();

     }

     public class Car

         implements IManeuverable { // Code here. }

     public class Boat

         implements IManeuverable { // Code here. }

     public class Submarine

         implements IManeuverable { // Code here. }

 

n         该方法是指其它的一些类可以进行交通工具的驾驶,而不必关心其实际上是什么(汽车,轮船,潜艇)或者其在什么继承架构里面。

     public void travel(IManeuverable vehicle)

     {

         vehicle.setSpeed(35.0);

         vehicle.forward();

         vehicle.left();

         vehicle.climb();

     }

法则#4:开放-封闭法则(OCP)

软件组成实体应该是可扩展的,但是不可修改的。

[ Software Entities Should Be Open For Extension, Yet Closed For Modification ]

开放-封闭法则

n         开放-封闭法则认为我们应该试图去设计出永远也不需要改变的模块。

n         我们可以添加新代码来扩展系统的行为。我们不能对已有的代码进行修改。

n         符合OCP的模块需满足两个标准:

F        可扩展,即“对扩展是开放的”(Open For Extension)-模块的行为可以被扩展,以需要满足新的需求。

F        不可更改,即“对更改是封闭的”(Closed for Modification)-模块的源代码是不允许进行改动的。

n         我们能如何去做呢?

F        抽象(Abstraction)

F        多态(Polymorphism)

F        继承(Inheritance)

F        接口(Interface)

 

n         一个软件系统的所有模块不可能都满足OCP,但是我们应该努力最小化这些不满足OCP的模块数量。

n         开放-封闭法则是OO设计的真正核心。

n         符合该法则便意味着最高等级的复用性(reusability)和可维护性(maintainability)。

OCP示例

n         考虑下面某类的方法:

public double totalPrice(Part[] parts)

     {

         double total = 0.0;

         for (int i = 0; i < parts.length; i++)

         {

              total += parts[i].getPrice();

         }

         return total;

     }

n         以上函数的工作是在制订的部件数组中计算各个部件价格的总和。

n         若Part是一个基类或接口且使用了多态,则该类可很容易地来适应新类型的部件,而不必对其进行修改。

n         其将符合OCP

n         但是在计算总价格时,若财务部颁布主板和内存应使用额外费用,则将如何去做。

n         下列的代码是如何来做的呢?

     public double totalPrice(Part[] parts)

     {

         double total = 0.0;

         for (int i = 0; i < parts.length; i++)

         {

              if (parts[i] instanceof Motherboard)

                   total += (1.45 * parts[i].getPrice());

              else if (parts[i] instanceof Memory)

                   total += (1.27 * parts[i].getPrice());

              else

                   total += parts[i].getPrice();

         }

         return total;

     }

n         这符合OCP吗?

n         当每次财务部提出新的计价策略,我们都不得不要修改totalPrice()方法!这并非“对更改是封闭的”。显然,策略的变更便意味着我们不得不要在一些地方修改代码的,因此我们该如何去做呢?

n         为了使用我们第一个版本的totalPrice(),我们可以将计价策略合并到Part的getPrice()方法中。

 

n         这里是Part和ConcretePart类的示例:

// Class Part is the superclass for all parts.

public class Part

{

     private double price;

     public Part(double price) { this.price = price; }

     public void setPrice(double price) { this.price = price; }

     public double getPrice() { return price; }

}

// Class ConcretePart implements a part for sale.

// Pricing policy explicit here!

public class ConcretePart extends Part

{

     public double getPrice()

     {

         // return (1.45 * price); //Premium

         return (0.90 * price); //Labor Day Sale

     }

}

n         但是现在每当计价策略发生改变,我们就必须修改Part的每个子类!

n         一个更好的思路是采用一个PricePolicy类,通过对其进行继承以提供不同的计价策略:

 

 

/**

 * Class Part is the superclass of all parts.

 * This Par class now has contained a PricePolicy object.

 */

public class Part

{

     private double price;

     private PricePolicy pricePolicy;

     public void setPricePolicy(PricePolicy pricePolicy)

     {

         this.pricePolicy = pricePolicy;

     }

     public void setPrice(double price) { this.price = price; }

     public double getPrice() { return pricePolicy.getPrice(price); }

}

 

/**

* Class PricePolicy implements a given price policy.

*/

public class PricePolicy

{

     private double factor;

     public PricePolicy(double factor)

     {

         this.factor = factor;

     }

     public double getPrice(double price) { return price * factor; }

}

 

/**

* Class SalePrice implements a sale pricing policy.

*/

public class SalePrice extends PricePolicy

{

     private double discount;

     public void setDiscount(double discount) { this.discount = discount; }

 

     public double getPrice() { return (basePrice * discount); }

}

n         看起来我们所做的就是将问题推迟到另一个类中。但是使用该解决方案,我们可通过改变Part对象,在运行期间动态地来设定计价的策略。

n         另一个解决方案是使每个ConcretePart从数据库或属性文件中获取其当前的价格。

单选法则

单选法则(the Single Choice Principle)是OCP的一个推论。

单选法则:

无论在什么时候,一个软件系统必须支持一组备选项,理想情况下,在系统中只能有一个类能够知道整个的备选项集合。

法则#5:Liskov替换法则(LSP)

使用指向基类(超类)的引用的函数,必须能够在不知道具体派生类(子类)对象类型的情况下使用它们。

[ Function Thar Use Referennces To Base(Super) Classes Must Be Able To Use Objects Of Derived(Sub) Classes Without Knowing It ]

Liskov替换法则

n         显而易见,Liskov替换法则(LSP)是根据我所熟知的“多态”而得出的。

n         例如:

public void drawShape(Shape s) {

// Code here.

}

n         方法drawShape应该可与Sharp超类的任何子类一起工作(或者,若Sharp为Java接口,则该方法可与任何实现了Sharp接口的类一起工作)

n         但是当我们在实现子类时必须要谨慎对待,以确保我们不会无意中违背了LSP。

 

n         若一个函数未能满足LSP,那么可能是因为它显式地引用了超类的一些或所有子类。这样的函数也违背了OCP,因为当我们创建一个新的子类时,会不得不进行代码的修改。

LSP示例

n         考虑下面Rectangle类:

// A very nice Rectangle class.

public class Rectangle

{

     private double width;

     private double height;

     public Rectangle(double w, double h)

     {

         width = w;

         height = h;

     }

     public double getWidth() { return width; }

     public double getHeight() { return height; }

     public void setWidth(double w) { width = w; }

     public void setHeight(double h) { height = h; }

     public double area() { return (width * height); }

}

 

n         现在,Square类会如何呢?显然,一个正方形是一个四边形,因此Square类应该从Rectangle类派生而来,对否?让我们看一看!

n         观察可得:

F        正方形不需要将高和宽都作为属性,但是总之它将继承自Rectangle。因此,每一个Square对象会浪费一点内存,但这并不是一个主要问题。

F        继承而来的setWidth()和setHeight()方法对于Square而言并非真正地适合,因为一个正方形的高和宽是相同。因此我们将需要重写setWidth()和setHeight()方法。不得不重写这些简单的方法有可能是一种不恰当的继承使用方式。

 

n         Square类如下:

// A Square class.

public class Square extends Rectangle

{

     public Square(double s) { super(s, s); }

     public void setWidth(double w)

     {

         super.setWidth(w);

         super.setHeight(w);

     }

     public void setHeight(double h)

     {

         super.setHeight(h);

         super.setWidth(h);

     }

}

 

n         看起来都还不错。但是让我们检验一下!

public class TestRectangle

{

     // Define a method that takes a Rectangle reference.

     public static void testLSP(Rectangle r)

     {

         r.setWidth(4.0);

         r.setHeight(5.0);

         System.out.println("Width is 4.0 and Height is 5.0" +

         ", so Area is " + r.area());

         if (r.area() == 20.0)

              System.out.println("Looking good!\n");

         else

              System.out.println("Huh?? What kind of rectangle is this??\n");

     }

     public static void main(String args[])

     {

         //Create a Rectangle and a Square

         Rectangle r = new Rectangle(1.0, 1.0);

         Square s = new Square(1.0);

         // Now call the method above. According to the

         // LSP, it should work for either Rectangles or

         // Squares. Does it??

         testLSP(r);

         testLSP(s);

     }

}

n         测试程序输出:

 

Width is 4.0 and Height is 5.0, so Area is 20.0

Looking good!

Width is 4.0 and Height is 5.0, so Area is 25.0

Huh?? What kind of rectangle is this??

 

n         看上去好像我们违背了LSP!

 

n         这里的问题出在哪里呢?编写testLsp()方法的程序员做了一个合理的假设,即改变Rectangle的宽而保持它的高不变。

n         在将一个Square对象传递给这样一个方法时产生了问题,显然是违背了LSP

n         Square和Rectangle类是相互一致和合法的。尽管程序员对基类作了合理的假设,但其所编写的方法仍然会导致设计模型的失败。

n         不能孤立地去看待解决方案,必须根据设计用户所做的合理假设来看待它们。

 

n         一个数学意义上的正方形可能是一个四边形,但是一个Square对象不是一个Rectangle对象,因为一个Square对象的行为与一个Rectangle对象的行为是不一致的!

n         从行为上来说,一个Square不是一个Rectangle!一个Square对象与一个Rectangle对象之间不具有多态的特征。

总结

n         Liskov替换法则(LSP)清楚地表明了ISA关系全部都是与行为有关的。

n         为了保持LSP(并与开放-封闭法则一起),所有子类必须符合使用基类的client所期望的行为。

n         一个子类型不得具有比基类型(base type)更多的限制,可能这对于基类型来说是合法的,但是可能会因为违背子类型的其中一个额外限制,从而违背了LSP!

n         LSP保证一个子类总是能够被用不论其基类在哪里被使用