OO 基础

抽象
封装
多态
继承

OO 原则

封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之间的松耦合设计而努力(观察者模式)
对扩展开发,对修改关闭(装饰者模式)
依赖抽象,不要依赖具体类(工厂模式)
最少知识原则:只和你的密友交谈。(外观模式) --得墨忒法则(Law of Demeter)
好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。(模板方法模式)
单一职责原则:一个类应该只有一个引起变化的原因。(迭代器模式)

OO 模式

策略模式 (Strategy pattern)

定义算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

要点一:

  • 知道OO基础,并不足以让你设计出良好的OO系统。

  • 良好的OO设计必须具备可复用、可扩充、可维护三个特性。

  • 模式可以让我们建造出具有良好OO设计质量的系统。

  • 模式被认为是经历史验证的OO设计经验。

  • 模式不是代码,而是针对设计问题的的通用解决方案。你可把它们应用到特定的应用中。

  • 模式不是被发明,而是被发现。

  • 大多数的模式和原则,都是着眼于软件变化的主题。

  • 大多数的模式都允许系统局部改变独立于其他部分。

  • 我们常把系统会变化的部分抽出来封装。

  • 模式让开发人员自己和你有共享的语言,能够最大化沟通的价值。

In the Java Collections framework, sorting of collections is achieved using either Comparable or Comparator interfaces.

package java.util;

public final class Arrays {

    public static <T> void sort(T[] a, Comparator<? super T> c) {

观察者模式 (Observer pattern)

在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。

要点二:

  • 观察者模式定义了对象之间一对多的关系

  • 主题(也就是可观察者)用一个共同的接口来更新观察者

  • 观察者和可观察者之间用松耦合的方式结合(loose coupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口。

  • 使用此模式时,你可以从被观察者处推(push)或拉(pull)数据(然而,推的方式被认为更"`正确`")。

  • 在多个观察者时,不可以依赖特定的通知次序。

  • Java有多种观察者模式的实现,包括了通用的 java.util.Observable

  • 要注意 java.util.Observable 实现上所带来的一些问题。

  • 如果有必要的话,可以实现自己的Observable,这并不难,不要害怕。

  • Swing大量使用观察者模式,许多GUI框架也是如此。

  • 此模式也被应用在许多地方,例如:JavaBeans、RMI。

装饰者模式 (Decorator pattern)

动态的将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择。

要点三:

  • 继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式。

  • 在我们的设计中,应该允许行为可以扩展,而无需修改现有的代码。

  • 组合和委托可用于在运行时动态地加上新的行为。

  • 除了继承,装饰者模式也可以让我们扩展行为。

  • 装饰者模式意味着一群装饰者类,这些类用来包装具体组件。

  • 装饰者类反应出被装饰的组件类型(事实上,他们具有相同的类型,都经过接口或继承实现)。

  • 装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。

  • 你可以用无数个装饰者包装一个组件。

  • 装饰者一般对组件的客户是透明的,除非客户程序依赖与组件的具体类型。

  • 装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。

In Java’s I/O classes, the decorator pattern is used extensively.

Here is an example of how it works:

InputStream inputStream = new FileInputStream("somefile.txt");
BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);

抽象工厂模式 (Abstract factory pattern)

提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

工厂方法模式 (Factory method pattern)

定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类实例化推迟到子类。

要点四:

  • 所有的工厂都是用来封装对象的创建。

  • 简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类解耦。

  • 工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法类创建对象。

  • 抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中。

  • 所有的工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合。

  • 工厂方法允许类的实例化延迟到子类进行。

  • 抽象工厂创建相关的对象家族,而不需要依赖他们的具体类。

  • 依赖倒置原则,指导我们避免依赖具体类型,而尽量依赖抽象。

  • 工厂是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程。

    • 变量不可以持有具体类的引用。 --如果使用new,就会持有具体类的引用。你可以改用工厂来避开这样的做法

    • 不要让类派生自具体类。 --如果派生自具体类,你就会依赖具体类。请派生自一个抽象(接口或抽象类)。

    • 不要覆盖基类中已实现的方法。--如果覆盖基类中已实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该有所有的子类共享。

单件模式 (Singleton pattern)

确保一个类只有一个实例,并提供全局访问点。

要点五:

  • 单件模式确保应用程序中一个类最多只有一个实例。

  • 单件模式也提供访问这个实例的全局点。

  • 在Java中实现单件模式需要私有的构造器、一个静态方法和一个静态变量。

  • 确定在性能和资源上的限制,然后小心地选择适当的方案来实现单件,以解决多线程问题(我们必须认定所有的程序都是多线程的)。

  • 如果不是采用第五版的Java 2,双重检查加锁实现会失效。

  • 小心,如果你使用多个类加载器,可能导致单件失效而产生多个实例。

  • 如果使用JVM 1.2或之前的版本,你必须建立单件注册表,以免垃圾收集器将单件回收。

  1. Singleton Pattern: The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

  2. Double-Checked Locking: In a multithreaded environment, we use double-checked locking to ensure that the Singleton instance is initialized only once. It first checks if the instance is null, and if it is, it then enters a synchronized block. Inside the synchronized block, it checks again if the instance is still null before it finally creates a new instance. This approach minimizes the use of costly synchronization and ensures that the Singleton instance is created only when needed.

  3. Volatile Keyword: The volatile keyword is used in the context of the double-checked locking idiom to ensure thread safety. It provides two key benefits: visibility and ordering.

    • Visibility: A volatile variable ensures all reads and writes are done directly from and to the main memory respectively, it guarantees that the most recently written value is visible to all threads.

    • Ordering: It creates a "happens-before" relationship that restricts reordering of instructions. This guarantees that any assignment (or shared data modification) will occur only after everything that came before it in the executing thread.

    The use of the volatile keyword ensures that multiple threads correctly observe the fully initialized state of the Singleton instance.

    Here’s the example of how this works in code again:

    public class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if(instance == null) {
                synchronized(Singleton.class) {
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    In this code, getInstance() uses double-checked locking to ensure that the Singleton instance is created only if it’s null, and the volatile keyword is used to ensure that every thread sees the same value of instance.

In C#, you can use the volatile keyword with double-check locking to implement a Singleton, similar to Java. However, C# also allows you to use the Lazy<T> type and its Value property to create a Singleton. Here is an example using volatile with a double-check lock:

public class Singleton
{
    private static volatile Singleton instance;
    private static object syncRoot = new Object();

    private Singleton() {}

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }
}

In this code, syncRoot is locked to ensure that only one thread can enter the block and create a new instance of Singleton.

However, .NET provides a simpler way to ensure thread-safety without locks, using lazy initialization:

public class Singleton
{
    private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());

    private Singleton() {}

    public static Singleton Instance { get { return lazy.Value; } }
}

In this code, the Lazy<T> type handles thread-safety and initialization. The instance is not created until the Instance property is accessed for the first time. This method is simpler and is the recommended approach for creating a thread-safe Singleton in .NET.

命令模式 (Command pattern)

将请求封装成对象,这可以让你使用不同的请求、队列,或者日志请求来参数化其他对象。命令模式也可以支持撤消操作。

要点六:

  • 命令模式将发出请求和执行请求的对象解耦。

  • 在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或多个动作。

  • 调用者通过调用命令对象的execute()发出请求,这会使接收者的动作被调用。

  • 调用者可以接受命令当作参数,甚至在运行时动态地进行。

  • 命令可以支持撤消,做法是实现一个undo()方法来回到execute()被执行前的状态。

  • 宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤消。

  • 实际操作时,很常见使用"`聪明`"命令对象,也就是直接实现了请求,而不是将工作委托给接收者。

  • 命令也可以用来实现日志和事务系统。

适配器模式 (Adapter pattern)

将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

外观模式 (Facade pattern)

提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层次接口,让子系统更容易使用。

要点七:

  • 当需要使用一个现有的类而其接口并不符合你的需要时,就是用适配器。

  • 当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观。

  • 适配器改变接口以符合客户的期望。

  • 外观将客户从一个复杂的子系统中解耦。

  • 实现一个适配器可能需要一番功夫,也可能不费功夫,视目标接口的大小与复杂度而定。

  • 实现一个外观,需要将子系统组合进外观中,然后将工作委托给子系统执行。

  • 适配器模式有两种形式:对象适配器和类适配器。类适配器需要用到多重继承。

  • 你可以为一个子系统实现一个以上的外观。

  • 适配器将一个对象包装起来以改变其接口;装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象"`包装`"起来以简化其接口。

  • 外观不只是简化了接口,也将客户从组件的子系统中解耦。

  • 外观和适配器可以包装许多类,但是外观的意图是简化接口,而适配器的意图是将接口转换成不同的接口。

  • 最少知识原则,就任何对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:

    • 该对象本身

    • 被当作方法的参数而传递进来的对象

    • 此方法所创建或实例化的任何对象

    • 对象的任何组件

模板方法模式 (Template method pattern)

在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

要点八:

  • "模板方法"定义了算法的步骤,把这些步骤的实现延迟到子类。

  • 模板方法模式为我们提供了一种代码复用的重要技巧。

  • 模板方法的抽象类可以具体方法、抽象方法和钩子。

  • 抽象方法由子类实现。

  • 钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它。

  • 为了防止子类改变模板方法中的算法,可以将模板方法声明为final。

  • 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用底层模块。

  • 你将在真实世界的代码中看到模板方法模式的许多变态,不要期待它们全都是一眼就可以被你认出来。

  • 策略模式和模板方法模式都封装算法,一个用组合,一个用继承。

  • 工厂方法是模板方法的一种特殊版本。

迭代器模式 (Iterator pattern)

提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

组合模式 (Composite pattern)

允许你将对象组成树形结构来表现“整体/部分”的层次结构。组合能让客户以一致的方式处理个别对象和对象组合。

要点九:

  • 迭代器允许访问聚合的元素,而不暴露它的内部结构。迭代器模式让我们能游走于聚合内的每一个元素,而又不暴露其内部的表示。

  • 迭代器将遍历聚合的工作封装进一个对象中。把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。

  • 当使用迭代器的时候,我们依赖聚合提供遍历。

  • 迭代器提供了一个通用的接口,让我们遍历聚合项,当我们编码使用聚合项时,就可以使用多态机制。

  • 我们应该努力让一个类只分配一个责任。

    • 单一职责原则 类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。 这个原则告诉我们,尽量让每个类保持单一责任。

    • 内聚(cohesion)

      • 内聚这个术语你应该听过,它用来度量一个类或模块紧密地达到单一目的或责任。

      • 当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有高内聚;反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。

      • 内聚是一个比单一责任原则更普遍的概念,但两者其实关系是很密切的。遵守这个原则的类容易具有很高的凝聚力,而且比背负许多责任的低内聚类更容易维护。

  • 组合模式提供一个结构,可同时包容个别对象和组合对象。

  • 组合模式允许客户对个别对象以及组合对象一视同仁。

  • 组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点。

  • 在实现组合模式时,有许多设计上的折衷。你要根据需要平衡透明性和安全性。

  • 组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。

  • 使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。

* The Composite pattern is a structural software design pattern that describes a group of objects that are treated the same way as a single instance of the same type of object. It allows you to compose objects into tree structures and enables clients to treat individual objects and compositions uniformly.

The pattern is composed of the following parts:

  • Component: An abstract class that represents both the composite (a node) and the leaf nodes. This abstract class will have methods that are common to both the composite and leaf node.

  • Leaf: Defines the behaviour for the elements in the composition structure.

  • Composite: It has leaf elements. It implements base component methods and defines additional methods for adding, removing or getting its child components.

Here’s a basic example in Java:

import java.util.*;

// Component
abstract class Component {
    void add(Component component) {
        throw new UnsupportedOperationException();
    }
    abstract void display();
}

// Leaf
class Leaf extends Component {
    private String name;

    Leaf(String name) {
        this.name = name;
    }

    @Override
    void display() {
        System.out.println(name);
    }
}

// Composite
class Composite extends Component {
    private List<Component> children = new ArrayList<>();

    void add(Component component) {
        children.add(component);
    }

    @Override
    void display() {
        for (Component child : children) {
            child.display();
        }
    }
}

In the Java Development Kit (JDK), a classic example of the composite pattern is the java.awt.Component class in AWT/Swing.

java.awt.Component is the abstract root for all AWT components (like Button, Panel, etc). java.awt.Container is a subclass of Component, and a Container can contain other Components (and other Containers). This forms a tree-like structure where every node in the tree is a Component, and so every node can be treated in the same way.

状态模式 (State pattern)

允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

要点十:

  • 状态模式允许一个对象基于内部状态而拥有不同的行为。

  • 和程序状态机(PSM)不同,状态模式用类代表状态。

  • Context会将行为委托给当前状态对象。

  • 通过将每个状态封装进一个类,我们把以后需要做的任何改变局部化了。

  • 状态模式和策略模式有相同的类图,但是它们的意图不同。

  • 策略模式通常会用行为或算法来配置Context类。

  • 状态模式允许Context随着状态的改变而改变行为。

  • 状态转换可以由State类或Context类控制。

  • 使用状态模式通常会导致设计中类的数目大量增加。

  • 状态类可以被多个Context实例共享。

代理模式 (Proxy pattern)

为另一个对象提供一个替身或占位符尾访问这个对象。(代理作为另一个对象的代表。)

要点十一:

  • 代理模式为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种。

  • 远程代理管理客户和远程对象之间的交互。

  • 虚拟代理控制访问实例化开销大的对象。

  • 保护代理基于调用者控制对象方法的访问。

  • 代理模式有许多变体,例如:缓存代理、同步代理、防火墙代理和写入时复制代理。

  • 代理在结构上类似装饰者,但是目的不同。

  • 装饰者模式为对象加上行为,而代理则是控制访问。

  • Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器。

  • 就和其他的包装者(wrapper)一样,代理会造成你的设计中类的数目增加。

复合模式 (Compound patterns)

复合模式结合两个或者两个以上的模式,组成一个解决方案,解决一再发生的一般性问题。

要点十二:

  • MVC是复合模式,结合了观察者模式、策略模式和组合模式。

  • 模型使用观察者模式,以便观察者更新,同时保持两者之间的解耦。

  • 控制器是视图的策略,视图可以使用不同的控制器实现,得到不同的行为。

  • 视图使用组合模式实现用户界面,用户界面通常组合来嵌套的组件,像面板、框架和按钮。

  • 这些模式携手合作,把MVC模型的三层解耦,这样可以保持设计干净又有弹性。

  • 适配器模式用来将新的模型适配成已有的视图和控制器。

  • Model 2是MVC在Web上的应用。

  • 在Model 2中,控制器实现Servlet,而JSP/HTML实现视图。