title: 初识软件架构设计原则
date: 2020-11-26 13:40:31
tags: 软件架构 java
软件架构设计原则
设计原则是设计模式的基础,在实际开发中,并不是要求所有代码都遵循设计原则,我们要根据实际情况(例如人力、时间、成本一集质量),不能一味的追求完美,否则就是家中负担了。我们需要在合适的场景下面去遵循设计原则,达到一种平衡取舍,这样的话就可以帮我们设计出更加优雅的代码结构。
-
开闭原则
开闭原则是指一个软件实体应该对扩展开放,对修改关闭。用抽象构造框架,用扩展实现细节。例如许多公司规定每天上班八小时,但是不规定几点到,几点走。只要每天上够八小时就可以。下面以蛋糕售卖为例子进行介绍。
-
首先创建一个蛋糕的基本信息的接口ICake:
public interface ICake { Double getPrice(); String getName(); }
-
由于蛋糕有很多的口味(草莓,巧克力,奶油等),这里我们选择一个苹果口味的蛋糕做例子(这个口味有点怪哈,哈哈),创建一个苹果味蛋糕的类:
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; } }
-
现在我们苹果味的蛋糕可以正常售卖了,我们知道了它是苹果味的蛋糕和价钱,而不是草莓味的。但是,我们这个连锁店有一家店铺由于地理位置不好,只能打折销售,怎么办呢??改代码?但是其他店现在是不需要的啊,或许以后有需要。而且其他代码的逻辑都已经写好了,改的话万一有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; } }
这样就可以解决问题了。
-
-
依赖倒置原则
依赖倒置原则是指设计代码结构时,上层模块不能依赖下层模块,二者都应该依赖抽象模块。抽象不能依赖细节,细节去依赖抽象,达到减少累与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性。我们还以蛋糕为例作味介绍。
-
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这两块蛋糕吃的非常爽。
-
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只能去选择店主摆出来的蛋糕。也就是依赖倒置原则,高层模块不依赖低层模块,低层模块依赖高层模块。这样的雇佣才是买家与买家的关系,不是那种专门雇佣的了哈。
-
-
单一职责原则
单一原则顾名思义就是自己做自己份内的,擅长的事(有点偏哈)。指不要存在多于一个导致类变更的原因。假如我们有一个类负责两个事情,一旦其中一个事情发生了变更,那么有可能就会影响到另外一个,导致发生故障,这就违反了单一职责原则了。还是拿我们吃的蛋糕来说。
- 蛋糕和馒头的做法是有差别的,做法是不一样的。一个需要精心雕刻,贵一点,一个只需要简单蒸一下就好了。我们来看看代码是这样的
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("小馒头"); } }
但是,我们现在变了,蛋糕的种类多了,蛋糕需要加草莓了,馒头不需要。又或者馒头有其他的要求了,这样的话我们不只是需要变动馒头或者蛋糕一种,还需要动另外一种。这样就会容易出现问题。所以我们最好是把他两分开。
- 我们可以创建一个馒头的类和一个蛋糕的类,这样他们就可以自己随便改变而不需要影响另外一个类。代码如下:
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<String> cakeList = new ArrayList<>(); for (int i = 0; i < 20; i++) { cakeList.add(i + "种"); } storeManager.checkCakeNum(cakeList); } } public class StoreManager { public int checkCakeNum(List<String> cakeList) { return cakeList.size(); } }
很显然,正常情况下,经理不会去一个一个的去数蛋糕的种类,这也不符合迪米特原则。经理只需要从店长哪里拿到一个结果就可以了。至于店长怎么知道的,自己数的还是员工说的,这就没有必要关心了。代码可以修改如下:
public class Manager { public void checkNum(StoreManager storeManager) { int checkNum = storeManager.checkCakeNum(); } } public class StoreManager { public int checkCakeNum() { List<String> cakeList = new ArrayList<>(); for (int i = 0; i < 20; i++) { cakeList.add(i + "种"); } return cakeList.size(); } }
-
里氏替换原则
通俗点讲就是父类为其所用,那么子类必须为其所用,也一定够为其所用,而且不会改变程序的逻辑。准确点是一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能够透明的使用其子类的对象,子类对象能够替换父类对象,结果逻辑不变。也就是子类可以扩展父类的功能,但不能改变父类原有的功能。代码就不列举了。可以参考正方形、长方形和四边形的场景。正方形不能用长方形做父类。而是长方形和正方形都属于四边形。
-
合成复用原则
合成复用原则是指尽量使用对象组合/聚合而不是继承关系达到软件的复用目的。可以使系统更加灵活,降低类与类之间饿耦合度,一个类的变化对其他类造成的影响先对较少。例如,我们经常会封装一下工具类来进行使用。