mtjmtj7的小站
mtjmtj7的小站
© mtjmtj7
All Rights Reserved.

设计模式之六大设计原则

设计模式之六大设计原则

单一原则

单一原则通俗来说就是一个类负责一个职责,每一个职责都是一条轴线,如果一个类有一个以上的职责,这些职责就会耦合在一起,这样会导致脆弱的设计,当一个职责发生变化的时候,会影响到其他职责,当多个职责耦合在一起时,会影响复用性。 例如页面与逻辑分离。

例如:

public class Animal {

    String animal;

    public Animal(String animal) {
        super();
        this.animal = animal;
    }

    public void jiao() {
        System.out.println(animal + "叫了一声");
    }
}

public class Client {

    public static void main(String[] args) {
        Animal cat  = new Animal("cat");
        cat.jiao();
        Animal dog  = new Animal("dog");
        dog.jiao();
    }
}

运行结果:

cat叫了一声
dog叫了一声


**遵循单一原则的优点在于 **

  • 可以降低程序复杂度,一个类值负责一个职责,逻辑简单。
  • 提高程序可读性,提高系统可维护性。
  • 变更风险降低。

里氏替换原则(LSP)

里氏替换原则是面向对象的基本原则之一。

任何基类出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换基类,软件功能不受影响的时候,基类才能真正被复用,而衍生类能够在基类上衍生出更多功能。里氏替换原则是对“开闭原则”的补充,实现开闭原则的关键是抽象化,而基类与子类的继承关系就是抽象化的实现,所以里氏替换原则是实现抽象化的具体步骤的规范。

定义1:如果每一个类型的T1的对象O1,都有类型T2的对象O2, 那么在T1定义的所有程序P在所有对象O1都换成O2时,P的行为不发生改变,那么T2是T1的子类。

定义2:所有引用基类的地方,都必须透明的使用子类的对象。

通俗的说,子类可以使用父类的方法,但不能改变父类的功能。

问题由来 ,有一个功能P1,由类A来实现。现在需要将功能P1扩展,功能P由P1和新功能P2组成,新功能P2由子类B来实现,因此在功新功能P2实现完成的同事,可能造成P1的故障。

解决方案 当使用继承时,遵循里氏替换原则,类B继承A时,除了新功能不要重写A的方法,也尽量不要重载A的方法。因此我们可以使用final的手段来强制限制

例如 :

public class A {

    int fun1(int a, int b) {
        return a - b;
    }
}
public class Client {

    public static void main(String[] args) {
        System.out.println("a-b=" + new A().fun1(100, 50));
        System.out.println("a-b=" + new A().fun1(0, 50));
    }
}

结果:

a-b=50
a-b=-50


现在我么又有了新的需求,要求在a-b之后再加上100。由类B来完成两个功能:

  • a-b
  • a-b+100

因为类A已经完成了a+b,因此可以让B类继承A,再完成 [a-b+100] 就可以。

public class B extends A{

    int fun1(int a, int b) {
        return a+b;
    }
    int fun2(int a, int b) {
        return fun1(a, b)+100;
    }
}

此时我们故意修改B类的fun1方法

public class Client {

    public static void main(String[] args) {
        System.out.println("a-b=" + new A().fun1(100, 50));
        System.out.println("a-b=" + new A().fun1(0, 50));
        System.out.println("a+b+100 = " + new B().fun1(0, 50));
    }
}

运行结果:

a-b=50
a-b=-50
a+b+100 = 50

我们发现现在B类的fun1已经发生故障,因为B类在给方法起名时,无意重写的A类的方法,造成原本正常运行的功能出错。在本例中,在引用基类A完成的功能,替换成子类B之后,就发生了异常。在实际编程中,我们经常会用到重写父类方法来实现新的功能,这样的写法虽然简单,但是整个继承体系的复用性会变差。

所以:子类可以扩展父类的功能,但是不能重写父类的功能。

  • 子类可以实现父类的抽象方法,但不能覆盖父类的抽象方法。
  • 子类可以增加自己的方法。
  • 当子类的方法重载 父类的方法是,前置条件(形参)比父类的更宽松。
  • 当子类的方法重载父类是,后置条件(返回值)比父类更严格。

依赖倒置原则

看了这么多文字看的一塌糊涂,通俗点来说依赖倒置原则就是面向接口开发。

场景:妈妈给孩子讲故事,只要给她一本书,她就可以照着书讲故事。

public class Book {

    String getContent() {
        return "很久很久以前...";
    }
}
public class Mother {

    void narrate(Book book) {
        System.out.println("妈妈在讲故事");
        System.out.println(book.getContent());
    }
}
public class Client {

    public static void main(String[] args) {
        Mother mother = new Mother();
        mother.narrate(new Book());
    }
}

运行结果:

妈妈在讲故事
很久很久以前…


上述是面向实现的编程,即依赖Book类这个具体的实现类。

现在该需求为:这个妈妈要讲报纸,报纸代码如下:

public class Newspaper {

    String getContent() {
        return "报纸上说...";
    }
}

显然,这位mother不会读报纸,如果以后再改需求,读网页,读新闻等等,就要频繁修改mother类。原因是mother与book的耦合性太高,必须降低耦合性。

我们引入一个接口IReader

public interface IReader {

    String getContent();
}

我们让Mother类与IReader发生依赖关系,Book,Newspaper各自去实现IReader接口,这样就符合依赖倒置原则。

public interface IReader {

    String getContent();
}
public class Book implements IReader{

    public String getContent() {
        return "很久很久以前...";
    }
}
public class Newspaper implements IReader{

    public String getContent() {
        return "报纸上说...";
    }
}
public class Mother {

    void narrate(IReader ireader) {
        System.out.println("妈妈在讲故事");
        System.out.println(ireader.getContent());
    }
}
public class Client {

    public static void main(String[] args) {
        Mother mother = new Mother();
        mother.narrate(new Book());
        mother.narrate(new Newspaper());
    }
}

妈妈在讲故事
很久很久以前…
妈妈在讲故事
报纸上说…


这样,不管Mother读什么,都不用直接修改Mother类。

原本mother类与book类耦合,必须要完成book类的功能才能修改mother类,因为mother类依赖于book类。

修改后的程序可以同时进行,互不影响。

依赖倒置有三种方式,上面的例子是接口传递,还有构造方法传递和setter方法传递(Spring常用

依赖注入的核心就是 面向接口编程

接口隔离原则

就是使用多个隔离的接口,比使用单个接口要好,接口隔离就是为了降低耦合度。

接口隔离的含义就是,简历单一接口,不要建立臃肿庞大的接口,尽量细化接口,接口中的方法尽可能少,就是为各个类建立相应的接口,而不要建立一个很庞大 的接口。

与单一职责的区别

单一职责主要在于职责,而接口隔离注重于接口依赖的隔离。

单一职责主要在于约束类,其次才是接口和方法,针对的是程序中的实现细节;接口隔离原则主要约束接口,主要针对抽象。

**采用接口隔离要注意以下几点*

  • 接口尽可能小,但也要有限度。接口过小会造成接口数量过多,使设计复杂。
  • 为依赖接口的类定制相应的接口,只暴露使用的接口,他不需要的方法隐藏起来。
  • 提高内聚,减少对外交互。使接口用最少的方法完成最多的事。

迪米特法则(最少知道原则)

定义 一个对象应该对其他对象保持最少了解

问题由来 类与类之间联系越紧密,当一个类发生故障是会影响到另外一个类。

解决方案 尽量降低类与类之间的耦合。

场景: 假如某司分 直属部分 和 分公司。现在要求打印员工编号。

//总公司员工
class Employee{
    private String id;
    public void setId(String id){
        this.id = id;
    }
    public String getId(){
        return id;
    }
}

//分公司员工
class SubEmployee{
    private String id;
    public void setId(String id){
        this.id = id;
    }
    public String getId(){
        return id;
    }
}

class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for(int i=0; i<100; i++){
            SubEmployee emp = new SubEmployee();
            //为分公司人员按顺序分配一个ID
            emp.setId("分公司"+i);
            list.add(emp);
        }
        return list;
    }
}

class CompanyManager{

    public List<Employee> getAllEmployee(){
        List<Employee> list = new ArrayList<Employee>();
        for(int i=0; i<30; i++){
            Employee emp = new Employee();
            //为总公司人员按顺序分配一个ID
            emp.setId("总公司"+i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmployee(SubCompanyManager sub){
        List<SubEmployee> list1 = sub.getAllEmployee();
        for(SubEmployee e:list1){
            System.out.println(e.getId());
        }

        List<Employee> list2 = this.getAllEmployee();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }
}

public class Client{
    public static void main(String[] args){
        CompanyManager e = new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
    }
}

现在这个设计的问题出现在CompanyManager根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:

class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for(int i=0; i<100; i++){
            SubEmployee emp = new SubEmployee();
            //为分公司人员按顺序分配一个ID
            emp.setId("分公司"+i);
            list.add(emp);
        }
        return list;
    }
    public void printEmployee(){
        List<SubEmployee> list = this.getAllEmployee();
        for(SubEmployee e:list){
            System.out.println(e.getId());
        }
    }
}

class CompanyManager{
    public List<Employee> getAllEmployee(){
        List<Employee> list = new ArrayList<Employee>();
        for(int i=0; i<30; i++){
            Employee emp = new Employee();
            //为总公司人员按顺序分配一个ID
            emp.setId("总公司"+i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmployee(SubCompanyManager sub){
        sub.printEmployee();
        List<Employee> list2 = this.getAllEmployee();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }
}

修改后,我们为分公司添加打印员工编号方法,直接由总公司调用,而不是直接在总公司内写打印员工方法。

迪米特法则是为了降低类之间的耦合度,由于每个类都减少了不必要的依赖,可以降低耦合度。

但是过分的使用迪米特法则,会产生大量中介和传递类,导致系统复杂度变大。

开闭原则

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,需要面向接口编程。

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。

如果仔细思考以及仔细阅读很多设计模式的文章后,会发现其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。

开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

说到这里,再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

参考 :https://www.cnblogs.com/pony1223/p/7594803.html

打赏
2019-03-01
14 阅读
暂无评论

发表评论