初识软件架构设计原则


title: 初识软件架构设计原则
date: 2020-11-26 13:40:31
tags: 软件架构 java


软件架构设计原则

​ 设计原则是设计模式的基础,在实际开发中,并不是要求所有代码都遵循设计原则,我们要根据实际情况(例如人力、时间、成本一集质量),不能一味的追求完美,否则就是家中负担了。我们需要在合适的场景下面去遵循设计原则,达到一种平衡取舍,这样的话就可以帮我们设计出更加优雅的代码结构。

  • 开闭原则

    开闭原则是指一个软件实体应该对扩展开放,对修改关闭。用抽象构造框架,用扩展实现细节。例如许多公司规定每天上班八小时,但是不规定几点到,几点走。只要每天上够八小时就可以。下面以蛋糕售卖为例子进行介绍。

    1. 首先创建一个蛋糕的基本信息的接口ICake:

      public interface ICake {
          Double getPrice();
          String getName();
      }
      
    2. 由于蛋糕有很多的口味(草莓,巧克力,奶油等),这里我们选择一个苹果口味的蛋糕做例子(这个口味有点怪哈,哈哈),创建一个苹果味蛋糕的类:

      public class AppleCake implements ICake{
          private Double price;
          private String name;
      
          public AppleCake(Double price, String name) {
              this.price = price;
              this.name = name;
          }
      
          @Override
          public Double getPrice() {
              return this.price;
          }
      
          @Override
          public String getName() {
              return this.name;
          }
      }
      
    3. 现在我们苹果味的蛋糕可以正常售卖了,我们知道了它是苹果味的蛋糕和价钱,而不是草莓味的。但是,我们这个连锁店有一家店铺由于地理位置不好,只能打折销售,怎么办呢??改代码?但是其他店现在是不需要的啊,或许以后有需要。而且其他代码的逻辑都已经写好了,改的话万一有bug怎么办?所以我们就需要在不动原来代码的基础上,在增加一个苹果蛋糕打折的类就好了。如下:

      public class AppleDiscountCake extends AppleCake{
          public AppleDiscountCake(Double price, String name) {
              super(price, name);
          }
      
          /**
           * 苹果味的蛋糕原价钱
           * @return
           */
          public Double getOriginPrice() {
              return super.getPrice();
          }
      
          /**
           * 苹果味的蛋糕打折后的价钱
           * @return
           */
          public Double getPrice() {
              return super.getPrice() * 0.65;
          }
      }
      

      这样就可以解决问题了。

  • 依赖倒置原则

    依赖倒置原则是指设计代码结构时,上层模块不能依赖下层模块,二者都应该依赖抽象模块。抽象不能依赖细节,细节去依赖抽象,达到减少累与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性。我们还以蛋糕为例作味介绍。

    1. shmilylyp最近非常尝蛋糕,先买了一块苹果味的蛋糕,然后又买了一块草莓味的蛋糕。代码如下:

      public class Shmilylyp {
          public void buyAppleCake() {
              System.out.println("shmilylyp买了一个苹果味的蛋糕");
          }
      
          public void buyStrawberryCake() {
              System.out.println("shmilylyp买了一个草莓味的蛋糕");
          }
      
          public static void main(String[] args) {
              Shmilylyp shmilylyp = new Shmilylyp();
              shmilylyp.buyAppleCake();
              shmilylyp.buyStrawberryCake();
          }
      }
      

      shmilylyp这两块蛋糕吃的非常爽。

    2. shmilylyp吃完了两块蛋糕发现还想尝尝巧克力蛋糕怎么办呢??在写一个方法??但是如果还想吃其他的好多种类呢??继续写?不怕胖的哈。这样的话我们就需要不停的修改main方法中的代码,而且也需要修改上层也就是Shmilylyp这个吃货中的代码。这会造成代码的不稳定的。在说shmilylyp要吃那种还的人家店主去做那种,又不是他家开的。所以我们就需要优化代码了。优化后的代码如下:

      public class Shmilylyp {
          private ICake cake;
      
          public void setCake(ICake cake) {
              this.cake = cake;
          }
      
          public void buyCake() {
              cake.buyCase();
          }
      
          public static void main(String[] args) {
              Shmilylyp shmilylyp = new Shmilylyp();
              shmilylyp.setCake(new StrawberryCake());
              shmilylyp.buyCake();
              shmilylyp.setCake(new AppleCake());
              shmilylyp.buyCake();
          }
      }
      
      public interface ICake {
          void buyCase();
      }
      
      public class AppleCake implements ICake {
          @Override
          public void buyCase() {
              System.out.println("shmilylyp 买了一个苹果蛋糕");
          }
      }
      
      public class StrawberryCake implements ICake{
          @Override
          public void buyCase() {
              System.out.println("shmilylyp 购买了一个草莓味的蛋糕");
          }
      }
      

      这样的话,由店主提前把他能做的蛋糕做好拿来买(假设只做两种哈,后面在慢慢加的),shmilylyp就可以选择做好的进行购买了。这样店主不依靠shmilylyp想吃什么而去做什么样的蛋糕,shmilylyp只能去选择店主摆出来的蛋糕。也就是依赖倒置原则,高层模块不依赖低层模块,低层模块依赖高层模块。这样的雇佣才是买家与买家的关系,不是那种专门雇佣的了哈。

  • 单一职责原则

    单一原则顾名思义就是自己做自己份内的,擅长的事(有点偏哈)。指不要存在多于一个导致类变更的原因。假如我们有一个类负责两个事情,一旦其中一个事情发生了变更,那么有可能就会影响到另外一个,导致发生故障,这就违反了单一职责原则了。还是拿我们吃的蛋糕来说。

    1. 蛋糕和馒头的做法是有差别的,做法是不一样的。一个需要精心雕刻,贵一点,一个只需要简单蒸一下就好了。我们来看看代码是这样的
    public class PastryDemo1 {
    
        public void makePastry(String pastryName) {
            if ("蛋糕".equals(pastryName)) {
                System.out.println(pastryName + "是需要精加工的");
            } else {
                System.out.println(pastryName + "是比较简单的");
            }
        }
    
        public static void main(String[] args) {
            PastryDemo1 pastryDemo1 = new PastryDemo1();
            pastryDemo1.makePastry("蛋糕");
            pastryDemo1.makePastry("小馒头");
        }
    }
    

    但是,我们现在变了,蛋糕的种类多了,蛋糕需要加草莓了,馒头不需要。又或者馒头有其他的要求了,这样的话我们不只是需要变动馒头或者蛋糕一种,还需要动另外一种。这样就会容易出现问题。所以我们最好是把他两分开。

    1. 我们可以创建一个馒头的类和一个蛋糕的类,这样他们就可以自己随便改变而不需要影响另外一个类。代码如下:
    public class PastryDemo2 {
        public static void main(String[] args) {
            Cake cake = new Cake();
            cake.make();
            SmallBun smallBun = new SmallBun();
            smallBun.make();
        }
    }
    
    public class Cake {
        public void make() {
            System.out.println("做了一个精加工的草莓蛋糕");
        }
    }
    
    public class SmallBun {
        public void make() {
            System.out.println("做了一个比较简单的小馒头");
        }
    }
    
  • 接口隔离原则

    接口隔离原则是指用多个专门的借口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。一个类对另一类的依赖应该建立在最小的接口之上。同时应该建立单一饿接口,不要建立庞大臃肿的接口,尽量细化接口。

    这次我们不料吃的了,通过动物行为来进行分析。

    鸟和狗都属于动物,鸟有吃,飞的行为,狗有吃,看门的行为。我们看以下代码:

    public interface IAnimal {
        // 吃
        void eat();
        // 飞
        void fly();
        // 看门
        void gatekeeper();
    }
    
    public class Bird implements IAnimal{
        @Override
        public void eat() {
            System.out.println("我是一只鸟,我要吃东西");
        }
    
        @Override
        public void fly() {
            System.out.println("我是一只鸟,我可以飞");
        }
    
        @Override
        public void gatekeeper() {
    
        }
    }
    
    
    public class Dog implements IAnimal{
        @Override
        public void eat() {
            System.out.println("我是一只狗,我可以吃");
        }
    
        @Override
        public void fly() {
    
        }
    
        @Override
        public void gatekeeper() {
            System.out.println("我是一只狗,我可以看门");
        }
    }
    

    以上代码中,狗和鸟都有吃的行为,但是狗不能飞,鸟不能看门,这两个行为就只能空着了。这样设计当然是不可能的。所以我们可以根据不同的行为来设计接口,如下:

    public class Dog implements IGatekeeper, IEat{
        @Override
        public void eat() {
            System.out.println("我是一只狗,我可以吃");
        }
    
        @Override
        public void gatekeeper() {
            System.out.println("我是一只狗,我可以看门");
        }
    }
    
    public class Bird implements IEat, IFly{
    
        @Override
        public void eat() {
            System.out.println("我是一只鸟,可以吃");
        }
    
        @Override
        public void fly() {
            System.out.println("我是一只鸟,可以飞");
        }
    }
    
    public interface IEat {
        void eat();
    }
    
    public interface IFly {
        void fly();
    }
    
    public interface IGatekeeper {
        void gatekeeper();
    }
    
  • 迪米特原则

    也叫最少知道原则。就是指一个对象应该对其他对象保持最少的了解,尽量降低类与类之间的耦合度。我们用上架的蛋糕种类来说明一下这个问题。

    假如经理需要知道这个分店上架了多少种类的蛋糕。这时候,店长就需要去统计,然后在告诉经理。

    public class Result {
        public static void main(String[] args) {
            Manager manager = new Manager();
            manager.checkNum(new StoreManager());
        }
    }
    
    public class Manager {
        public void checkNum(StoreManager storeManager) {
            // 经理一个一个的数,店长一个一个的记
            List cakeList = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                cakeList.add(i + "种");
            }
            storeManager.checkCakeNum(cakeList);
        }
    }
    
    public class StoreManager {
        public int checkCakeNum(List cakeList) {
            return cakeList.size();
        }
    }
    

    很显然,正常情况下,经理不会去一个一个的去数蛋糕的种类,这也不符合迪米特原则。经理只需要从店长哪里拿到一个结果就可以了。至于店长怎么知道的,自己数的还是员工说的,这就没有必要关心了。代码可以修改如下:

    public class Manager {
        public void checkNum(StoreManager storeManager) {
            int checkNum = storeManager.checkCakeNum();
        }
    }
    
    public class StoreManager {
        public int checkCakeNum() {
            List cakeList = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                cakeList.add(i + "种");
            }
           return cakeList.size();
        }
    }
    
  • 里氏替换原则

    通俗点讲就是父类为其所用,那么子类必须为其所用,也一定够为其所用,而且不会改变程序的逻辑。准确点是一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能够透明的使用其子类的对象,子类对象能够替换父类对象,结果逻辑不变。也就是子类可以扩展父类的功能,但不能改变父类原有的功能。代码就不列举了。可以参考正方形、长方形和四边形的场景。正方形不能用长方形做父类。而是长方形和正方形都属于四边形。

  • 合成复用原则

    合成复用原则是指尽量使用对象组合/聚合而不是继承关系达到软件的复用目的。可以使系统更加灵活,降低类与类之间饿耦合度,一个类的变化对其他类造成的影响先对较少。例如,我们经常会封装一下工具类来进行使用。

你可能感兴趣的:(初识软件架构设计原则)