oop设计原则SOLID详解

oop设计原则SOLID详解

简介

面向对象编程设计原则有个总结,叫SOLID原则。
再具体下去就是具体的设计模式了。

S=single Responsibility Principle 单一职责原则

动机

在这里,责任被认为是改变的一个原因。这个原则表明,如果我们有两个原因要修改一个类,我们必须将功能分为两个类。每个类将只处理一个责任,如果将来我们需要做一个更改,我们将在相应的类中处理。 当我们需要在具有更多职责的类中进行更改时,更改可能会影响与类的其他职责相关的其他功能。

单一职责原则是一个简单和直观的原则,但在实践中有时很难正确运用。

意图

一个类,应该仅有一个引起它变化的原因。

举例

让我们假设我们需要一个对象来保存电子邮件。我们将使用从下面的示例中的IEmail接口。看起来没啥问题。但仔细看看,我们可以看到,我们的IEmail接口和Email类有2个责任(改变的原因)。
一个是在一些电子邮件协议如pop3或imap中使用该类。如果必须支持其他协议,则应以另一种方式对对象进行序列化,并且应添加代码以支持新协议。
另一个是Content字段,即使内容是字符串,也许我们将来可能支持HTML或其他格式。

如果我们只保留一个类,每一个职责的改变可能会影响到另一个:

  • 添加新协议将创建需要为每个类型的字段添加用于解析和序列化内容的代码。
  • 添加新的内容类型(如html)使我们为每个协议添加代码。

//单一责任原则 - 错误示例

interface IEmail { 
    public void setSender(String sender); 
    public void setReceiver(String receiver); 
    public void setContent(String content); 
} 

class Email implements IEmail { 
    public void setSender(String sender){// set sender; } 
    public void setReceiver(String receiver){//设置接收器; } 
    public void setContent(String content){//设置内容; } 
}

我们可以创建一个新的接口和类,称为IContent和Content来分担责任。每个类只有一个责任给我们更灵活的设计:

  • 添加新协议只会导致电子邮件类中的更改。
  • 添加新类型的内容支持导致仅在Content类中的更改。
//单一责任原则 - 好的示例
interface IEmail { 
    public void setSender(String sender); 
    public void setReceiver(String receiver); 
    public void setContent(IContent content); 
} 

interface Content { 
    public String getAsString(); //用于序列化
} 

class Email implements IEmail { 
    public void setSender(String sender){// set sender; } 
    public void setReceiver(String receiver){//设置接收器; } 
    public void setContent(IContent content){//设置内容; } 
}

结论

单一责任原则代表在应用程序的设计阶段识别类的一种好方法,它提醒您想一个类可以发展的所有方式。只有当应用程序应该如何工作的全部情况都很好理解时,才能实现良好的责任分离。

O=open close 开闭原则

开放-封闭原则,是说软件实例(类、模块、函数等等)应该可以扩展,但是不可修改。

这个原则其实是有两个特征,一个是说‘对于扩展是开放的(open for extension)’,另一个是说‘对于更改是封闭的(Closed for modification)’

动机

聪明的应用程序设计和代码实现应该考虑到在应用程序的开发和维护阶段会有频繁的更改。通常,当向应用程序添加新功能时,会涉及到许多更改。应该将现有代码中的这些更改最小化,因为假设现有代码已经进行单元测试,并且已编写代码中的更改可能以不必要的方式影响现有功能。

开闭原则指出该代码的设计和实现,应该能让新功能的加入以现有的代码最小的变化来完成。设计应该允许添加新功能作为新类,尽可能保持现有代码不变。

意图

像类,模块和函数的软件实体应该是开放的扩展,但关闭修改

例子

下面是违反开放关闭原则的示例。它实现了一个图形编辑器处理不同形状的绘图。很明显,它不遵循开放关闭原则,因为GraphicEditor类必须为每个必须添加的新形状类进行修改。有几个缺点:

  • 对于每个新形状添加的GraphicEditor的单元测试应该重做。
  • 当添加新类型的形状时,添加它的时间将会很长,因为添加它的开发人员应该理解GraphicEditor的逻辑。
  • 添加新形状可能以不期望的方式影响现有功能,即使新形状完美地工作。

// Open-Close Principle  -  Bad example 
class GraphicEditor { 
 
    public void drawShape(Shape s){ 
        if(s.m_type == 1)
            drawRectangle(s); 
        else if(s.m_type == 2)
            drawCircle(s); 
    } 
    public void drawCircle(Circle r){...} 
    public void drawRectangle(Rectangle r){....} 
} 
 
class Shape { 
    int m_type; 
} 
 
class Rectangle extends Shape { 
    Rectangle(){ 
        super.m_type = 1; 
    } 
} 
 
class Circle extends Shape { 
    Circle(){ 
        super.m_type = 2; 
    } 
}

下面是支持开放关闭原则的示例。在新设计中,我们在GraphicEditor中使用抽象draw()方法绘制对象,同时在具体形状对象中来实现。使用开放关闭原则避免了先前设计的问题,因为在添加新的形状类时不会更改GraphicEditor:

  • 无需单元测试。
  • 无需了解GraphicEditor的源代码。
  • 因为绘图代码被移动到具体的形状类,当添加新的功能时,降低了影响旧功能的风险。

// Open-Close Principle  -  Good example 
class GraphicEditor { 
    public void drawShape(Shape s){ 
        s.draw(); 
    } 
} 
 
class Shape { 
    abstract void draw(); 
} 
 
class Rectangle extends Shape { 
    public void draw(){ 
        //绘制矩形
    } 
}

结论

OCP只是一个原则。做出灵活的设计需要花费额外的时间和精力,并且它引入了新的抽象层次,增加了代码的复杂性。因此,这个原则应该应用于最有可能改变的领域。

有许多设计模式可以帮助我们扩展代码而不改变它。例如装饰者模式帮助我们遵循开放原则。此外,工厂方法或观察者模式可用于设计一个易于随现有代码中的最小变化而改变的应用程序。

L=LSP(Liskov’s Substitution Principle) 里氏代换原则

子类型必须能够替换它们的父类型
一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化。

动机

一直以来,我们在设计一个程序模块,我们创建一些类层次结构。然后我们通过扩展一些类来创建一些派生类。

我们必须确保新的派生类只是扩展而不替换旧类的功能。否则,新类在现有程序模块中使用时会产生不良影响。

Likov的替换原则声明,如果程序模块使用Base类,那么对Base类的引用可以替换为Derived类,而不会影响程序模块的功能。

意图

派生类型必须能够完全替代其基本类型。

例子

下面是违反Likov替代原则的典型例子。在示例中使用2个类:Rectangle和Square。让我们假设Rectangle对象在应用程序中的某个地方使用。我们扩展应用程序并添加Square类。正方形类由工厂模式返回,基于一些条件,我们不知道将返回什么类型的对象。但我们知道这是一个矩形。我们得到矩形对象,宽度设置为5,高度设置为10,得到面积。对于宽度为5和高度为10的矩形,面积应为50,而结果却是100。



//违反Likov的替代原则
class Rectangle
{
    protected int m_width;
    protected int m_height;

    public void setWidth(int width){
        m_width = width;
    }}

    public void setHeight(int height){
        m_height = height;
    }}


    public int getWidth(){
        return m_width;
    }}

    public int getHeight(){
        return m_height;
    }}

    public int getArea(){
        return m_width * m_height;
    }}  
}}

class Square extends Rectangle 
{
    public void setWidth(int width){
        m_width = width;
        m_height = width;
    }}

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

}}

class LspTest
{
    private static Rectangle getNewRectangle()
    {
        //它可以是一些工厂返回的对象... 
        return new Square();
    }}

    public static void main(String args [])
    {
        Rectangle r = LspTest.getNewRectangle();
        
        r.setWidth(5);
        r.setHeight(10);
        //用户知道r是一个矩形。 
        //假设他能够为基类设置宽度和高度

        System.out.println(r.getArea());
        //现在他惊讶地发现该面积是100而不是50。
    }}
}}

结论

这个原则只是开放关闭原则的一个扩展,这意味着我们必须确保新的派生类是扩展基类而不改变它们的行为。

I=Interface Segregation Principle 接口隔离原则

动机

当我们设计一个应用程序时,我们应该注意如何使一个包含多个子模块的模块抽象化。考虑由类实现的模块,我们可以在接口中完成系统的抽象。但是如果我们想扩展我们的应用程序添加另一个模块,它只包含原始系统的一些子模块,我们被迫实现完整的接口和写一些虚拟方法。这样的接口被命名为fat接口或污染的接口。具有接口污染不是一个好的解决方案,并且可能在系统中引起不适当的行为。

该接口分离原则规定,客户不应该被强迫来实现它们不使用的接口。基于一组方法,每个方法服务一个子模块,而不是一个胖接口许多小接口是优选的。

意图

不应该强制客户端依赖于他们不使用的接口。

例子

下面是一个违反接口隔离原则的例子。我们有一个经理类,代表管理工人的人。我们有两种类型的工人一些平均和一些非常有效的工人。这两种类型的工人工作,他们需要每天吃午饭。但现在有些机器人来到他们工作的公司,但他们不吃,所以他们不需要吃午饭。一个新的机器人类需要实现IWorker接口,因为机器人工作。在另一方面,不必实施它,因为他们不吃。

这就是为什么在这种情况下,IWorker被认为是一个污染的接口。

如果我们保持当前的设计,新的Robot类被迫实现eat方法。我们可以写一个不做任何事情的虚拟类(假设每天吃1秒),并且在应用程序中可能会产生不良效果(例如,管理者看到的报告会报告更多的午餐,而不是人数)。

根据接口分离原则,灵活的设计不会有接口污染。在我们的情况下,IWorker接口应该分为两个不同的接口。


// interface segregation principle - bad example
interface IWorker {
    public void work();
    public void eat();
}

class Worker implements IWorker{
    public void work() {
        // ....working
    }
    public void eat() {
        // ...... eating in launch break
    }
}

class SuperWorker implements IWorker{
    public void work() {
        //.... working much more
    }

    public void eat() {
        //.... eating in launch break
    }
}

class Manager {
    IWorker worker;

    public void setWorker(IWorker w) {
        worker=w;
    }

    public void manage() {
        worker.work();
    }
}

下面是支持接口隔离原则的代码。通过在2个不同的接口中拆分IWorker接口,新的Robot类不再强制实现eat方法。此外,如果我们需要另一个功能的机器人像充电,我们创建另一个有充电方法的接口IRechargeble。


// interface segregation principle - good example
interface IWorker extends Feedable, Workable {
}

interface IWorkable {
    public void work();
}

interface IFeedable{
    public void eat();
}

class Worker implements IWorkable, IFeedable{
    public void work() {
        // ....working
    }

    public void eat() {
        //.... eating in launch break
    }
}

class Robot implements IWorkable{
    public void work() {
        // ....working
    }
}

class SuperWorker implements IWorkable, IFeedable{
    public void work() {
        //.... working much more
    }

    public void eat() {
        //.... eating in launch break
    }
}

class Manager {
    Workable worker;

    public void setWorker(Workable w) {
        worker=w;
    }

    public void manage() {
        worker.work();
    }
}

结论

如果设计已经完成,胖接口可以使用适配器模式进行隔离。
像每个原则一样接口分离原则是一个原则,需要花费额外的时间和精力在设计期间应用它,并增加代码的复杂性。
但它产生灵活的设计。
如果我们要过分地使用应用它,它将导致包含许多只有单个方法接口的代码,因此应该基于经验和常识来识别代码将来可能在哪些该地需要扩展。

D= Dependency依赖反转原则

抽象不应该依赖细节,细节应该依赖于抽象。
说白了,就是要针对接口编程,不要对实现编程

动机

当我们设计软件应用程序时,我们可以考虑低级类实现基本和主要操作(磁盘访问,网络协议,…)的类和高级类封装复杂逻辑(业务流,…)的类。最后一个依赖于低级类。实现这种结构的一种自然方式是编写低级类,一旦我们让它们编写复杂的高级类。由于高级类是根据其他定义的,这似乎是合理的做法。但这不是一个灵活的设计。如果我们需要替换一个低级类会发生什么?
让我们来看一个复制模块的典型例子,它从键盘读取字符并将它们写入打印机设备。包含逻辑的高级类是复制类。低级类是KeyboardReader和PrinterWriter。

在一个糟糕的设计中,高级类直接使用并且在很大程度上依赖于低级类。在这种情况下,如果我们要更改设计以将输出定向到一个新的FileWriter类,我们必须在Copy类中进行更改。(让我们假设它是一个非常复杂的类,有很多逻辑,真的很难测试)。

为了避免这样的问题,我们可以在高级类和低级类之间引入抽象层。由于高级模块包含复杂的逻辑,它们不应该依赖于低级模块,因此不应该基于低级模块来创建新的抽象层。低级模块将基于抽象层创建。

根据这个原则,设计类结构的方法是从高级模块开始到低级模块:
高级类 – >抽象层 – >低级类

意图

  1. 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。
  2. 抽象不应取决于细节。细节应该取决于抽象。

例子

下面是违反依赖反转原则的示例。我们有经理类,它是一个高级类,而低级类称为Worker。我们需要在我们的应用程序中添加一个新模块,以模拟由新的专业工作者雇用决定的公司结构的变化。我们为此创建了一个新类SuperWorker。

让我们假设Manager类相当复杂,包含非常复杂的逻辑。现在我们必须改变它,以引入新的SuperWorker。让我们看看缺点:

  • 我们必须更改Manager类(记住它是一个复杂的,这将涉及时间和精力进行更改)。
  • 某些来自管理器类的当前功能可能会受到影响。
  • 单元测试应重做。

所有这些问题可能需要很多时间来解决,它们可能在旧的功能中引入新的错误。如果应用程序是按照依赖性反转原则设计的,情况会有所不同。这意味着我们设计manager类,一个IWorker接口和实现IWorker接口的Worker类。当我们需要添加SuperWorker类时,我们所要做的就是为其实现IWorker接口。现有类中没有其他更改。

//依赖反转原理 - 错误的示例

class Worker { 

    public void work(){ 

        // .... working 

    } 

} 



class Manager { 

    Worker worker; 



    public void setWorker(Worker w){ 
        worker = w; 
    } 

    public void manage(){ 
        worker.work(); 
    } 
} 

class SuperWorker { 
    public void work(){ 
        // .... working more more 
    } 
}

下面是支持依赖反转原理的代码。在这个新设计中,通过IWorker接口添加了一个新的抽象层。现在来自上面的代码的问题被解决了(考虑到高级逻辑没有变化):

  • 在添加SuperWorkers时,Manager类不需要更改。
  • 最小化风险以影响Manager类中的旧功能,因为我们不更改它。
  • 无需为Manager类重做单元测试。
//依赖反转原则 - 好例子
interface IWorker { 
    public void work(); 
} 

class Manager implements IWorker { 
    public void work(){ 
        // .... working 
    } 
} 

class SuperWorker implements IWorker { 
    public void work(){ 
        // .... working more more 
    } 
} 

class Manager { 
    IWorker worker; 

    public void setWorker(IWorker w){ 
        worker = w; 
    } 

    public void manage(){ 
        worker.work(); 
    } 
}

结论

当应用这个原则时,它意味着高级类不直接与低级类工作,他们使用接口作为抽象层。在这种情况下,不能使用运算符new来在高级类(如果必要)内实例化新的低级对象。相反,可以使用一些Creational设计模式,例如Factory Method,Abstract Factory,Prototype。

模板设计模式是应用DIP原理的示例。

当然,使用这个原则意味着更多的努力,将导致更多的类和接口维护,在一些词语中更复杂的代码,但更灵活。这个原则不应盲目地应用于每个类或每个模块。如果我们有一个更有可能在未来保持不变的类功能,则不需要应用这个原则。

Tags: