【面向对象】面向对象设计原则及常见设计模式的总结

参考文章

  • https://juejin.im/entry/5917d38dda2f60005de8def4
  • https://www.jianshu.com/p/a3474f4fee57

面向对象思想设计原则

  • 单一职责原则
    • 其实就是开发人员经常说的“高内聚,低耦合
    • 也就是说,每个类应该只有一个职责,对外只能提供一种功能,而引起类变化的原因应该只有一个。在设计模式中,所有的设计模式都应遵循这一原则
  • 开闭原则
    • 核心思想是:一个对象对扩展开放,对修改关闭
    • 其实开闭原则的意思就是:对类的改动是通过增加代码进行的,而不是修改现有代码。
    • 也就是说开发人员一旦写出了可以运行的代码,就不应该去改动它,而是要保证它能一直运行下去,如何能够做到这一点呢?这就需要借助于抽象和多态,即把可能变化的内容抽象出来,从而使抽象的部分是相对稳定的,而具体的实现则是可以改变和扩展的。
  • 里氏替换原则
    • 核心思想:在任何父类出现的地方都可以用它的子类来替代
    • 其实就是说:同一个继承体系中的对象应该有共同的行为特征。父类的功能子类也应该能实现。
  • 依赖注入原则
    • 核心思想:要依赖于抽象,不要依赖于具体实现
    • 其实就是说在应用程序中,所有的类如果使用或依赖于其他的类,则应该依赖这些其他类的抽象类,而不是这些其他类的具体实现类。为了实现这一原则,就要求我们在编程的时候针对抽象类或者接口编程,而不是针对具体实现编程。
  • 接口分离原则
    • 核心思想不应该强迫程序依赖它们不需要使用的方法
    • 其实就是说:一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口中。
  • 迪米特原则
    • 核心思想一个对象应当对其他对象尽可能少的了解
    • 其实就是说:降低各个对象之间的耦合,提高系统的可维护性。在模块之间应该只通过接口编程,而不理会模块的内部工作原理,它可以使各个模块耦合度降到最低,促进软件的复用

设计模式

介绍

为了让我们的代码能更好地遵循上面的原则,从而达到易于维护的目的。我们就有必要了解一下设计模式。

  • 设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
  • 设计模式不是一种方法和技术,而是一种思想

  • 设计模式和具体的语言无关,学习设计模式就是要建立面向对象的思想,尽可能的面向接口编程,低耦合,高内聚,使设计的程序可复用

  • 学习设计模式能够促进对面向对象思想的理解,反之亦然。它们相辅相成

设计模式的分类

我们可以将设计模式大致这样分类:

  • 创建型模式(对象的创建):简单工厂模式,工厂方法模式,抽象工厂模式,建造者模式,原型模式,单例模式。(6个)
  • 结构型模式(对象的组成(结构)):外观模式、适配器模式、代理模式、装饰模式、桥接模式、组合模式、享元模式。(7个)
  • 行为型模式(对象的行为):模版方法模式、观察者模式、状态模式、责任链模式、命令模式、访问者模式、策略模式、备忘录模式、迭代器模式、解释器模式。(10个)

常见的设计模式

设计模式的分类虽然很多,但是我们平时常用的也就下面的几种而已

  • 简单工厂模式和工厂方法模式(接口)
  • 模版设计模式(抽象类)
  • 装饰设计模式(IO流)
  • 单例设计模式(多线程)
  • 建造者模式
  • 适配器模式(GUI)

简单工厂模式

简单工厂模式,又叫静态工厂方法模式,它定义一个具体的工厂类负责创建一些类的实例。

比如,下面是一个简单工厂模式的例子:

/*
 * 抽象的动物类,里面有抽象的方法
 */
public abstract class Animal {
    public abstract void eat();
}

/*
 * 具体的动物猫继承抽象动物类,重写抽象方法
 */
public class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

/*
 * 具体的动物狗继承抽象动物类,重写抽象方法
 */
public class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}

/*
 * 动物工厂类,可以造猫和狗
 */
public class AnimalFactory {

    private AnimalFactory() {
    }
    public static Animal createAnimal(String type) {
        switch(type){
            case "dog":
                return new Dog();
            case "cat":
                return new Cat();
            default:
                return null;
        } 
    }
}


public class AnimalDemo {
    public static void main(String[] args) {

        // 有了工厂之后,通过工厂创造动物
        Animal a = AnimalFactory.createAnimal("dog");
        a.eat();
        a = AnimalFactory.createAnimal("cat");
        a.eat();
    }
}

运行后控制台结果:

狗吃骨头

猫吃鱼

我们运用了简单工厂模式后,不用每次用的时候去new对象,而是直接去调用这个工厂类里面的具体方法,它会给我们返回一个已经new好的对象。

我们来分析一下简单工厂模式的优点与缺点

  • 优点
    • 客户端不需要负责对象的创建,明确了各个类的职责。更有利于遵守单一职责原则。
  • 缺点
    • 这个静态工厂负责所有对象的创建。每当有新的对象增加,我们都需要去修改工厂类,这样不利于后期的维护。

工厂方法模式

工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类来实现。

我们将上面的例子改为工厂方法模式:

/*
 * 抽象的动物类,里面有抽象的方法
 */
public abstract class Animal {
    public abstract void eat();
}

/*
 *  工厂类接口,里面有抽象的创造动物的方法
 */
public interface Factory {
    public abstract Animal createAnimal();
}

/*
 * 具体的猫类继承抽象动物类,重写抽象方法
 */
public class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

/*
 *  猫工厂类实现工厂类并实现它的抽象方法,返回一个猫对象
 */
public class CatFactory implements Factory {

    @Override
    public Animal createAnimal() {
        return new Cat();
    }
}

/*
 * 具体的狗类继承抽象动物类,重写抽象方法
 */
public class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("狗吃肉");
    }
}

/*
 *  狗工厂类实现工厂类并实现它的抽象方法,返回一个狗对象
 */
public class DogFactory implements Factory {

    @Override
    public Animal createAnimal() {
        return new Dog();
    }
}

public class AnimalDemo {
    public static void main(String[] args) {
        // 需求:我要买只狗
        Factory factory = new DogFactory();
        Animal dog = factory.createAnimal();
        dog.eat();

        //需求:我要买只猫
        factory = new CatFactory();
        cat = factory.createAnimal();
        cat.eat();
    }
}

观察用工厂方法模式,会发现多了几个类。但是当我们这时要加入一种动物猪的时候,不再需要修改工厂类的代码,只需创建一个猪类继承抽象动物类,重写抽象方法,再创建一个猪的工厂类实现工厂类并实现它的抽象方法即可。这样的代码有很强的维护性和扩展性。

下面分析一下工厂方法模式的优点及缺点:

  • 客户端不需要再负责对象的创建,明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性。遵守了开闭原则,减少了修改。
  • 需要额外编写代码,增加了我们的工作量。

单例模式

单例模式的出现是为了确保类在内存中只有一个对象,该实例必须自动创建,并且对外提供。

那么如何实现这个类在内存中仅有一个对象呢

  • 构造私有
  • 本身提供一个对象
  • 通过公共的方法让外界访问

下面分别介绍单例模式中的懒汉式以及饿汉式。

饿汉式

饿汉式就是在类一加载时,就创建对象。

public class Student {
    // 构造私有
    private Student() {
    }

    // 自己造一个对象
    // 静态方法只能访问静态成员变量,加静态
    // 为了不让外界直接访问修改这个值,加private
    private static Student s = new Student();

    // 提供公共的访问方式
    // 为了保证外界能够直接使用该方法,加静态
    public static Student getStudent() {
        return s;
    }
}

public class StudentDemo {
    public static void main(String[] args) {

        // 通过单例得到对象
        Student s1 = Student.getStudent();
        Student s2 = Student.getStudent();
        System.out.println(s1 == s2);   //true
    }
}

饿汉式的特点就是类一加载就创建对象,可以在代码中Student类中体现到。那么我们怎样才能在用这个对象的时候才去创建它呢,我们就要来看下懒汉式了。

懒汉式

懒汉式就是在对象使用的时候,再去创建对象.

public class Teacher {
    private Teacher() {
    }

    private static Teacher teacher = null;

    public static Teacher getTeacher() {    
        if (teacher == null) {
            teacher = new Teacher();//当我们去用这个对象的时候才去创建它
        }
        return teacher;
    }
}

public class TeacherDemo {
    public static void main(String[] args) {

        Teacher t1 = Teacher.getTeacher();
        Teacher t2 = Teacher.getTeacher();
        System.out.println(t1 == t2); //true

    }
}

这样,就完成了懒汉式单例。可以看到,它是在使用到对象的时候,才去创建的。

对比

饿汉式我们经常在开发中使用,它是不会出问题的单例模式

懒汉式我们在回答时用,它是可能会出问题的单例模式。

关于单例模式需要了解的思想:

  • 懒加载思想(延迟加载)
  • 线程安全问题(考虑下面3个方面)
    • 是否多线程环境
    • 是否有共享数据
    • 是否有多条语句操作共享数据

如果都是,就会存在线程的安全问题,我们上面的懒汉式代码是不完整的,应该给对象中的方法加上synchronized关键字,这样才算完整。

public synchronized static Teacher getTeacher() {   
    if (teacher == null) {
        teacher = new Teacher();
    }
    return teacher;
}

模板方法模式

模版方法模式就是定义一个算法的骨架,而将具体的算法延迟到子类中来实现。

下面是模板方法的示例:

步骤1: 创建抽象模板结构(Abstract Class):炒菜的步骤

public  abstract class Abstract Class {  
//模板方法,用来控制炒菜的流程 (炒菜的流程是一样的-复用)
//声明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序 
        final void cookProcess(){  
        //第一步:倒油
        this.pourOil();
        //第二步:热油
         this.HeatOil();
        //第三步:倒蔬菜
         this.pourVegetable();
        //第四步:倒调味料
         this.pourSauce();
        //第五步:翻炒
         this.fry();
    }  

//定义结构里哪些方法是所有过程都是一样的可复用的,哪些是需要子类进行实现的

//第一步:倒油是一样的,所以直接实现
void pourOil(){  
        System.out.println("倒油");  
    }  

//第二步:热油是一样的,所以直接实现
    void  HeatOil(){  
        System.out.println("热油");  
    }  

//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
//所以声明为抽象方法,具体由子类实现 
    abstract void  pourVegetable();

//第四步:倒调味料是不一样的(一个下辣椒,一个是下蒜蓉)
//所以声明为抽象方法,具体由子类实现 
    abstract void  pourSauce();


//第五步:翻炒是一样的,所以直接实现
    void fry();{  
        System.out.println("炒啊炒啊炒到熟啊");  
    }  
}

步骤2: 创建具体模板(Concrete Class),即”手撕包菜“和”蒜蓉炒菜心“的具体步骤

//炒手撕包菜的类
  public class ConcreteClass_BaoCai extend  Abstract Class{
    @Override
    public void  pourVegetable(){  
        System.out.println(”下锅的蔬菜是包菜“);  
    }  
    @Override
    public void  pourSauce(){  
        System.out.println(”下锅的酱料是辣椒“);  
    }  
}
//炒蒜蓉菜心的类
  public class ConcreteClass_CaiXin extend  Abstract Class{
    @Override
    public void  pourVegetable(){  
        System.out.println(”下锅的蔬菜是菜心“);  
    }  
    @Override
    public void  pourSauce(){  
        System.out.println(”下锅的酱料是蒜蓉“);  
    }  
}

**步骤3: **客户端调用

public class Template Method{
  public static void main(String[] args){

//炒 - 手撕包菜
    ConcreteClass_BaoCai BaoCai = new ConcreteClass_BaoCai();
    BaoCai.cookProcess();

//炒 - 蒜蓉菜心
  ConcreteClass_ CaiXin = new ConcreteClass_CaiXin();
    CaiXin.cookProcess();
    }
}

这样就成功实现了模板方法模式

下面分析一下它的优点及缺点:

  • 优点
    • 提高代码复用性
      将相同部分的代码放在抽象的父类中
    • 提高了拓展性
      将不同的代码放入不同的子类中,通过对子类的扩展增加新的行为
    • 实现了反向控制
      通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,实现了反向控制 & 符合“开闭原则”
  • 缺点
    • 引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。

装饰模式

装饰模式:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

装饰模式中有这四个角色:抽象构件、具体构件、抽象装饰类 、具体装饰类

下面是装饰模式的一个示例

组件类

abstract class Component {
    public abstract void display();
}

组件装饰者

public class ComponentDecorator extends Component{
    private Component component; // 维持对抽象构件类型对象的引用
    public ComponentDecorator(Component component){
        this.component = component;
    }

    public void display() {
        component.display();
    }
}

继承类ListBox

public class ListBox extends Component{
    public void display() {
        System.out.println("显示列表框!");
    }
}

继承类TextBox

public class TextBox extends Component{
    public void display() {
        System.out.println("显示文本框!");
    }
}

黑框装饰者

public class BlackBoarderDecorator extends ComponentDecorator{
    public BlackBoarderDecorator(Component component) {
        super(component);
    }

    public void display() {
        this.setBlackBoarder();
        super.display();
    }

    public void setBlackBoarder() {
        System.out.println("为构件增加黑色边框!");

    }
}

滚动条装饰者

public class ScrollBarDecorator extends ComponentDecorator{
    public ScrollBarDecorator (Component component) {
        super(component); // 调用父类构造函数
    }

    public void display() {
        this.setScrollBar();
        super.display();
    }

    public void setScrollBar() {
        System.out.println("为构件增加滚动条!");
    }
}

客户端调用

public class Client {
    public static void main(String args[]) {
        Component component,componentSB,componentBB;
        component = new Window();
        componentSB = new ScrollBarDecorator(component);
        componentSB.display();
        componentBB = new BlackBoarderDecorator(componentSB);
        componentBB.display();
    }
}

对装饰模式举个例子

IO流中就用到了装饰模式

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter((new OutputStreamWriter(System.out)));

下面分析一下装饰模式的优点及缺点:

  • 优点
    • 使用装饰模式,可以提供比继承更灵活的扩展对象的功能,它可以动态的添加对象的功能,并且可以随意的组合这些功能
  • 缺点
    • 正因为可以随意组合,所以就可能出现一些不合理的逻辑

适配器模式

适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够一起工作。

适配器模式分为两种

  • 类适配器模式 :通过实现Target接口以及继承Adaptee类来实现接口转换
  • 对象适配器模式:实现Target接口和代理Adaptee的某个方法来实现接口转换

角色介绍

  • 目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
  • 源(Adaptee)角色:现在需要适配的接口。
  • 适配器(Adapter)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。

类适配器模式

类的适配器模式是把适配的类的API转换成为目标类的API。

步骤1.创建Target接口

public interface Target {
    public void Request(); 
}

步骤2.创建需要适配的类Adaptee

public class Adaptee {
    public void SpecificRequest(){
    }
}

步骤3.创建适配器类Adapter

//适配器Adapter继承自Adaptee,同时又实现了目标(Target)接口。
public class Adapter extends Adaptee implements Target {
    //目标接口要求调用Request()这个方法名,但源类Adaptee没有方法Request()
    //因此适配器补充上这个方法名
    //但实际上Request()只是调用源类Adaptee的SpecificRequest()方法的内容
    //所以适配器只是将SpecificRequest()方法作了一层封装,封装成Target可以调用的Request()而已
    @Override
    public void Request() {
        this.SpecificRequest();
    }
}

步骤4.定义使用目标类,并通过Adapter类调用所需要的方法从而实现目标

public class AdapterPattern {

    public static void main(String[] args){
        Target mAdapter = new Adapter();
        mAdapter.Request();
    }
}

这样,就达到了我们的目的。让Target成功使用了Adaptee类的specificRequest方法。

对象适配器模式

与类的适配器模式相同,对象的适配器模式也是把适配的类的API转换成为目标类的API。

步骤1.创建Target接口

public interface Target {
    public void Request(); 
}

步骤2.创建需要适配的类Adaptee

public class Adaptee {
    public void SpecificRequest(){
    }
}

步骤3.创建适配器类Adapter(使用包含的方式)

public class Adapter implements Target{    
    private Adaptee adaptee;  

    // 通过构造函数传入具体需要适配的被适配类对象  
    public Adapter(Adaptee adaptee) {  
        this.adaptee = adaptee;  
    }  

    @Override
    public void Request() {  
        // 使用委托的方式完成特殊功能  
        this.adaptee.SpecificRequest();  
    }  
}

步骤4.定义使用目标类,并通过Adapter类调用所需要的方法从而实现目标

public class AdapterPattern {
    public static void main(String[] args){
        //需要先创建一个被适配类的对象作为参数  
        Target mAdapter = new Adapter(new Adaptee());
        mAdapter.Request();
    }
}

两种模式的对比

  • 类适配器模式
    • 优点
    • 使用方便,代码简化
      仅仅引入一个对象,并不需要额外的字段来引用Adaptee实例
    • 缺点
    • 高耦合,灵活性低
      使用对象继承的方式,是静态的定义方式
  • 对象适配器模式
    • 优点
    • 灵活性高、低耦合
      采用 “对象组合”的方式,是动态组合方式
    • 缺点
    • 使用复杂
      需要引入对象实例

适配器模式的优缺点分析

  • 优点
    • 更好的复用性
      系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。
    • 透明、简单
      客户端可以调用同一接口,因而对客户端来说是透明的。这样做更简单 & 更直接
    • 更好的扩展性
      在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
    • 解耦性
      将目标类和适配者类解耦,通过引入一个适配器类重用现有的适配者类,而无需修改原有代码
    • 符合开放-关闭原则
      同一个适配器可以把适配者类和它的子类都适配到目标接口;可以为不同的目标接口实现不同的适配器,而不需要修改待适配类
  • 缺点
    • 过多的使用适配器,会让系统非常零乱,不易整体进行把握

建造者模式

建造者模式可以将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 、

下面是以组装电脑为例的实例

步骤1: 定义组装的过程(Builder):组装电脑的过程

public  abstract class Builder {  

//第一步:装CPU
//声明为抽象方法,具体由子类实现 
    public abstract void  BuildCPU();

//第二步:装主板
//声明为抽象方法,具体由子类实现 
    public abstract void BuildMainboard();

//第三步:装硬盘
//声明为抽象方法,具体由子类实现 
    public abstract void BuildHD();

//返回产品的方法:获得组装好的电脑
    public abstract Computer GetComputer();
}

步骤2: 电脑城老板委派任务给装机人员(Director)

public class Director{
    //指挥装机人员组装电脑
    public void Construct(Builder builder){

        builder. BuildCPU();
        builder.BuildMainboard();
        builder. BuildHD();
    }
 }

步骤3: 创建具体的建造者(ConcreteBuilder):装机人员

//装机人员1
  public class ConcreteBuilder extend  Builder{
    //创建产品实例
    Computer computer = new Computer();

    //组装产品
    @Override
    public void  BuildCPU(){  
       computer.Add("组装CPU")
    }  

    @Override
    public void  BuildMainboard(){  
       computer.Add("组装主板")
    }  

    @Override
    public void  BuildHD(){  
       computer.Add("组装主板")
    }  

    //返回组装成功的电脑
     @Override
      public  Computer GetComputer(){  
      return computer
    }  
}

步骤4: 定义具体产品类(Product):电脑

public class Computer{

    //电脑组件的集合
    private List<String> parts = new ArrayList<String>();

    //用于将组件组装到电脑里
    public void Add(String part){
        parts.add(part);
    }

    public void Show(){
          for (int i = 0;i<parts.size();i++){    
          System.out.println(“组件”+parts.get(i)+“装好了”);
          }
          System.out.println(“电脑组装完成,请验收”);
    }
}   

步骤5: 客户端调用

public class Builder Pattern{
  public static void main(String[] args){
        //逛了很久终于发现一家合适的电脑店
        //找到该店的老板和装机人员
          Director director = new Director();
          Builder builder = new ConcreteBuilder();

        //沟通需求后,老板叫装机人员去装电脑
        director.Construct(builder);

        //装完后,组装人员搬来组装好的电脑
        Computer computer = builder.GetComputer();
        //组装人员展示电脑
        computer.Show();
    }
}

下面我们来介绍一下建造者模式的优缺点

  • 优点
    • 易于解耦
      将产品本身与产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品。也就说细节依赖抽象。
    • 易于精确控制对象的创建
      将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰
    • 易于拓展
      增加新的具体建造者无需修改原有类库的代码,易于拓展,符合“开闭原则“。
  • 缺点
    • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
    • 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
N0tExpectErr0r

N0tExpectErr0r

一名热爱代码的 Android 开发者

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>