架构师之路-七大软件设计的原则


架构师之路-七大软件设计的原则

引文

设计的原则,可以帮助我们设计出更加优雅的代码结构,增加代码的可读性以及可维护性,但是在实际应用的过程当中,并不一定非得完全可以的去遵循,需要结合实际的业务需求以及外部因素(人力,时间),从中进行取舍。

软件设计原则包含七大设计原则,分别是:

  • 开闭原则
  • 依赖倒置原则
  • 单一职责原则
  • 接口隔离原则
  • 迪米特法则
  • 里氏替换原则
  • 合成复用原则

开闭原则(OCP)

Software entities like classes, modules and functions should be open for extension but closed for modifications.

开闭原则 (Open Closed Principle ) :一个软件实体,如类,模块和函数应该对扩展开放,对修改关闭。

开闭原则的目的是提高软件系统的可复用性以及可维护性,在代码过程当中,用抽象的方式去设计框架,由实现去扩展细节,所以,开闭原则的设计思想就是面向抽象的编程

样例:假设现在我们有一个需求,我有一个衬衣,它的品牌是鸿星尔克。我们依照开闭原则,进行设计:

  • 首先,我们解读它的需求,他现在是衬衣,以后也有可能夹克,T恤等,但是他们都是衣服,所以定义一个顶层设计:
public interface IClothes {
    /** 品牌 */
    String brand();

    /** 类型 */
    String type();   
}
  • 接着,我们定义抽象类,并实现接口的方法
public abstract class AbstractClothes implements IClothes {
 	private String type;
    private String brand;
    @Override
    public String brand() {
        return brand;
    }
    @Override
    public String type() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public void setBrand(String brand) {
        this.brand = brand;
    }
}
  • 此时,为了不去修改原本获取品牌与类型的逻辑,我们需要集成这个类,并由这个类去调用对应的方法
public class Shirt extends AbstractShirt{
    @Override
    public void setType(String type) {
        super.setType("衬衣");
    }

    @Override
    public void setBrand(String brand) {
        super.setBrand("鸿星尔克");
    }
}

这样,我们可以不用去修改调用的方法,还可以横向无限拓展。在Spring Security源码中Authentication的写法就完美的符合开闭原则 它的类图如下:

Authentication关系图

依赖倒置原则 (DIP)

High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

依赖倒置原则 (Dependence Inversion Principle,DIP) : 高层模块不应该依赖底层模块, 二者都应该依赖其抽象。 抽象不应该依赖细节;细节应该依赖抽象。

定义可能模糊,其实,它的目的是让我们面向接口的编程,而不是面向实现的编程,定义中的高层指的是调用者,而底层则是被调者。面向实现的编程,那就是正向依赖,相当于我在这就得这么干,而面向接口的编程,则是依赖倒置,通过依赖倒置,可以减少类与类之间的耦合性,提离系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所造成的风险。

依旧通过一篇代码来说明下依赖倒置原则

  • 面向实现的编程

    底层

    public class Shopping {
        public void buyErke(){
            System.out.println("买鸿星尔克牌的衬衣");
        }
        public void buyHla(){
            System.out.println("买海澜之家牌的衬衣");
        }
    }

    高层

    public class DoShopping {
        public static void main(String[] args) {
            Shopping shopping = new Shopping();
            shopping.buyErke();
            shopping.buyHla();
        }
    }

    通过面向实现的编程,我们可以看到,如果不再局限于买这两个牌子的衬衣,我想在买李宁牌的衬衣,那我不光要在Shopping类增加代码,我还要再高层修改代码,高层完全依赖底层。

  • 面向接口的编程

    底层-抽象类

    public abstract class AbstractClothes implements IClothes {
        //此处省略与开闭原则重复代码,增加抽象方法
        public abstract void shopping();
    }

    底层-Erke牌衬衣

    public class ErkeShirt extends AbstractClothes {
        @Override
        public void shopping() {
            System.out.println("买鸿星尔克牌的衬衣");
        }
    }

    底层-Hla牌衬衣

    public class HlaShirt extends AbstractClothes {
        @Override
        public void shopping() {
            System.out.println("买海澜之家牌的衬衣");
        }
    }

    底层-改造后的Shopping

    public class Shopping {
        public void doShopping(AbstractClothes clothes){
            clothes.shopping();
        }
    }
    

    高层

    public class DoShopping {
        public static void main(String[] args) {
            Shopping shopping = new Shopping();
            shopping.doShopping(new ErkeShirt());
            shopping.doShopping(new HlaShirt());
        }
    }

    这样,我不管想购买什么牌子的衬衣,我只需要在创建一个新的牌子类,在高层调用即可,完全不用去修改底层的类。所以,在工作中,如果拿到一个需求,看看是不是可以从顶层再到具体细节再来设计

单一职责原则 (SRP)

There should never be more than one reason for a class to change

单一职责 (Simple Responsibility Pinciple, SRP) 永远不应该有多于一个原因来改变某个类。

对于一个类,有且仅有一个可以使它变动的原因,假设这个类有两个职责,一旦需求变更,修改其中一个会导致另外一个出现bug,那等同于这个类存在于两个导致影响它的原因,这就好比我们的团队,各司其职不受影响,一旦插手可能越来越乱。

例如下面的例子:

public class Shopping {
  public void shop(String brand){
      if (brand.equals("hla")) {
          doSomething(brand);
          System.out.println("我给自己买了");
      } else {
          doSomething(brand);
          System.out.println("我买给别人");
      }
  }
  public void doSomething(String brand){
      System.out.println("我准备刷卡买" + brand);
  }
}

上面的例子判断了逛的牌子是不是HLA,是与不是都是刷卡买,只是给不给自己买的区别,如果这个时候,我发现,HLA店里跟支付宝合作了!有大量优惠,我不想刷卡买了,这个时候我如果动了doSomething方法,势必会影响到其他的方法,因为这类的的职责,不光管理着hla,还管理着其他,这就是所谓的修改了其中一个会导致另一个出现问题。

接口隔离原则 (ISP)

The dependency of one class to another one should depend on the smallest possible interface.

接口隔离原则(Interface Segregation Principle): 一个类与另一个类之间的依赖性,应该依赖于尽可能小的接口。

此原则设计思想,就是我们常说的高内聚,低耦合 , 定义的解释我们可以这么理解,在设计接口的时候,我们一定要充分的考虑业务模型,不要对外暴漏没有任何意义的接口,要建立单一细化的接口,而不是臃肿的接口。例如:

  • 设计一个顶层接口

    //动物顶层接口
    public interface IAnimal {
        //吃
        void eat();
        //跑
        void run();
        //飞
        void fly();
    }
  • 两个简单的实现

    //老虎
    public class Tigger implements IAnimal{
        @Override
        public void eat() {
        }
        @Override
        public void run() {
        }
        @Override
        public void fly() {
        }
    }
    //鸟
    public class Bird implements IAnimal{
        @Override
        public void eat() {
        }
        @Override
        public void run() {
        }
        @Override
        public void fly() {
        }

    大家发现问题没,是不是很搞笑,老虎会飞吗? 鸟会跑吗? 对于老虎暴漏fly接口方法没有任何意义,同理,对鸟暴漏run接口方法也没意义,所以这时候,我们就要细化接口

    public interface IEatAnimal {
        void eat();
    }
    public interface IFlyAnimal {
        void fly();
    }
    public interface IRunAnimal {
        void run();
    }

    当前动物需要什么行为就去实现对应行为,如

    public class Tigger2 implements IEatAnimal, IRunAnimal {
        @Override
        public void eat() {
        }
        @Override
        public void run() {
        }
    }

迪米特法则 (LOD)

Only talk to you immediate friends

迪米特原则(Lawof Demeter) 又叫最少知道原则(LeastKnowledge Principle) 简称LKP: 只与你最直接的朋友交流。

在代码设计中—个对象应该对其他对象保持最少的了解,意思就是,一个类不要去依赖太多的类,尽量降低类与类之间的耦合。所谓直接交流的朋友,可以理解为出现在成员变量中,方法的输入及输出中的类,而出现在方法内部的类,则不是朋友。案例如下

public class Fish {
}
public class River {
    public void countFish(List<Fish> list) {
        System.out.println("河里有" + list.size() + "条鱼");
    }
}
public class Person {
    public void checkRiver(River river){
        ArrayList<Fish> fish = new ArrayList<>();
        fish.add(new Fish());
        fish.add(new Fish());
        fish.add(new Fish());
        river.countFish(fish);
    }
}

上面关系我们根据迪米特法则,可以发现,其实Person最后只想知道的河里有几条鱼,而鱼的数量应该由当前的River统计而不是人去数,改造变更为如下代码:

public class Fish {
}
public class River {
    public void countFish() {
        ArrayList<Fish> fish = new ArrayList<>();
        fish.add(new Fish());
        fish.add(new Fish());
        fish.add(new Fish());
        System.out.println("河里有" + fish.size() + "条鱼");
    }
}
public class Person {
    public void checkRiver(River river){
        river.countFish();
    }
}

这样,完成了迪米特法则的要求

里氏替换原则 (LSP)

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

里氏替换原则 (Liskov Substitution Principle)是指使用基类的指针或引用的函数,必须是在不知情的情况下,能够使用派生类的对象。

定义可能是比较抽象的,其实我们可以这么理解,如果一个程序适用与一个实体的父类,那么这个父类的子类也必定可以在这个程序中适用,程序原本逻辑还不会被改变,反之则不一定。这样,我们可以在继承的概念上再往下引申一条,就是子类可以增加自己特有的方法,但不能覆盖父类的非抽象方法

它的优点很明显:

  • 约束继承泛滥
  • 需求变更的兼容性提高,以及程序的可维护性,可拓展性的提高

我们用代码做个示例:

  • 创建个父类-青蛙
//青蛙
public class Frog {
    
    /**
     * 腿
     * */
    private int leg;
	//几条腿
    public static int getArea(Frog area) {
        return area.getLeg();
    }

    public int getLeg() {
        return leg;
    }

    public void setLeg(int leg) {
        this.leg = leg;
    }
}
  • 创建个子类-蝌蚪
public class Tadpole extends Frog{
    public static int getArea(Tadpole tadpole) {
        if (tadpole.getLeg() > 0) {
            return 0;
        }
        return 0;
    }
}

从上面代码我们可以看见,蝌蚪是没有退的,如果这时候把子类蝌蚪调用改成青蛙,势必只返回了青蛙0条腿,这样就违背了里氏替换原则

合成复用原则 (CARP)

Favor delegation over inheritance as a reuse mechanism

合成复用原则 (Composite/Aggregate Reuse Principle,CARP) 指当要扩展类的功能时,优先考虑使用组合,而不是继承。

换言之就是,当我们需要对某个类的功能进行拓展时,尽量使用对象组合(has-a)/聚合(contanis-a),而不是继承关系达到拓展要用的目的。

优点就是,类中的具体实现细节不会暴漏给外界

示例如下

  • 创建一个逛街的细节

    public class doShopping {
        public String shopping(){
            return "拎着包带着钱去逛街";
        }
    }
  • 创建个去逛街的人

    public class Person {
    
        private doShopping doShopping;
    
        public void whoDoShop(){
            System.out.println("老婆" + doShopping.shopping());
        }
        public com.timeroar.blog.framework.design.carp.doShopping getDoShopping() {
            return doShopping;
        }
    
        public void setDoShopping(com.timeroar.blog.framework.design.carp.doShopping doShopping) {
            this.doShopping = doShopping;
        }
    }

这样,我们就在没有继承doShopping类的情况下,完成了对逛街人的拓展。


文章作者: TimeRoar
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 TimeRoar !
评论
  目录