软件设计的“七宗罪”及设计模式的七大原则
编写软件过程中,面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序,具有更好的代码重用性、可读性、可扩展性、可靠性,使程序呈现高内聚低耦合的特性。
软件设计的“七宗罪”:
- 僵化性
- 脆弱性
- 牢固性
- 粘滞性
- 不必要的重复
- 不必要的复杂性
- 晦涩性
1. 僵化性
僵化性是指难以对软件进行改动,即使是简单的改动。如果单一的改动会导致有依赖关系的模块中的连锁改动,那么设计就是僵化的。必须要改动的模块越多,设计就越僵化。
2. 脆弱性
脆弱性是指在进行一个改动时,程序的许多地方就可能出现问题,即设计易于遭破坏。并且,往往是出现新问题的地方与改动的地方并没有概念上的关联。
3. 牢固性
牢固性是指设计中包含了对其他系统有用的部分,但要想把这些部分分离出来所需要的努力和风险是巨大的,即设计难以复用。
4. 粘滞性
有的时候,一个改动可以以保持原有的设计意图和原有的设计框架的方式进行,也可以以破坏原始的意图和框架的方式进行。第一种办法无疑会对系统的未来有利,第二种办法是去权宜之计,可以解决短期问题,但是会牺牲长期利益。如果第二种办法比第一种办法容易得多的话,程序员就有可能牺牲长期利益,采取权宜之计,在一个通用的逻辑中建立一种特例,以便解决眼前的问题。一个系统的设计,如果总是使得第二种办法比第一种办法来得容易,说明粘滞性过高。一个粘滞性过高的系统会诱使维护它的程序员采取错误的维护方案。
5. 不必要的重复
大量的重复代码往往是由于开发人员忽略了抽象,从而使系统不易理解,而且,软件中的重复代码,也会使系统的改动变得困难,不易于系统的维护。
6. 不必要的复杂性
不必要的复杂性是指设计中包含了当前没有用的部分,即过分设计。例如,对于逻辑复杂、技术先进的过度追求,导致了技术框架虽看似华丽却复杂难用。再例如,在设计产品功能或界面交互时,过度追求体验完美、需求满足却导致实际体验下降、功能没人用。所以,软件设计应“有所为有所不为”。
7. 晦涩性
晦涩性是指模块难于理解。代码随时间而不断演化,往往会变得越来越晦涩、可读性差。代码晦涩难懂常体现在如下几点:
- 代码不良
- 代码的格式不正确或不一致
- 代码中包含冗余代码
- 代码中包含未备注的低层次优化
- 代码逻辑过于复杂
设计模式七大原则有:
- 单一职责原则
- 接口隔离原则
- 依赖倒置原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
1. 单一职责原则
单一职责原则SRP(The Single Responsibility Principle)指的就是一个类应该仅有一个引起它变化的原因。这是最简单、最容易理解却最不容易做到的一个设计原则。
对类来说的,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。
单一职责原则注意事项和细节:
- 降低类的复杂度,一个类只负责一项职责。
- 提高类的可读性,可维护性
- 降低变更引起的风险
2. 接口隔离原则
接口隔离原则ISP(The Interface Segregation Principle)指的是“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上”。就是说,“不应该强迫客户依赖于它们不用的方法”。再通俗点说,不要强迫客户使用它们不用的方法,如果强迫客户使用它们不需要的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。
下面是一个违反了接口隔离原则的例子:
interface IWorker {public void work();public void eat();
}class Worker implements IWorker { // 普通工人public void work() {// ...}public void eat() {// ...}
}class SuperWorker implements IWorker { // 高级工人public void work() {// ...}public void eat() {// ...}
}public Manager { // 管理工人的管理者private Worker worker;public void setWorker(IWorker w) {this.worker = w;}public void manage() {worker.work();}
}
上述例子中,有普通和高级两种工作者,他们都需要工作和吃饭。但是,现在来了一批机器人,机器人作为公司的工作者,一方面需要工作,需要实现IWorker接口;另一方面机器人不需要吃饭,又不需要实现IWorker接口。这种情况下,IWorker就被认为是一个被污染了的接口。
如果我们保持上面那样的设计,那么Robot类就将被迫实现eat()方法,当然我们可以写一个哑类让它什么也不做,但是这会对程序造成不可预料的结果,例如管理者看到报表中显示被带走的午餐多于实际的人数。
通过接口隔离原则,我们应该把IWorker分离成两个接口,如果Robot类需要添加特有而工人没有的方法,例如充电功能,我们可以再创建一个新的IRechargeable接口,其中包含一个重新充电的方法recharge。
更改后的代码如下:
interface IWorkable {public void work();
}interface IFeedable {public void eat();
}interface IRechargeable {public void recharge();
}class Worker implements IWorkable, IFeedable { // 普通工人public void work() {// ...}public void eat() {// ...}
}class SuperWorker implements IWorkable, IFeedable{ // 高级工人public void work() {// ...}public void eat() {// ...}
}class Robot implements IWorkable, IRechargeable { // 机器人public void work() {// ...}public void recharge() {// ...}
}public Manager { // 管理工人的管理者private Worker worker;public void setWorker(IWorker w) {this.worker = w;}public void manage() {worker.work();}
}
总之,接口隔离原则是对接口进行规范约束,其包含以下含义:
- 接口尽量要小,这是接口隔离原则的核心定义,不要出现臃肿的接口
- 接口要高内聚
- 定制服务,只提供访问者需要的方法
- 接口设计是有限度的
3. 依赖倒置原则
依赖倒置原则DIP(The Dependency Inversion Principle)指的是“高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象”。
好抽象的定义,我们直接上代码,先看一个不符合DIP原则的例子
public abstract class Light {public abstract void turnOn();public abstract void turnOff();
}public class BulbLight extends Light {@Overridepublic void turnOn() {System.out.println("BulbLight turned on...");}@Overridepublic void turnOff() {System.out.println("BulbLight turned off...");}
}public class TubeLight extends Light {@Overridepublic void turnOn() {System.out.println("TubeLight turned on...");}@Overridepublic void turnOff() {System.out.println("TubeLight turned off...");}
}public class ToggleSwitch { // 开关类public void toggle(Light light) {light.turnOn();light.turnOff();}public static void main(String[] args) {ToggleSwitch toggleSwitch = new ToggleSwitch();toggleSwitch.toggle(new BulbLight());toggleSwitch.toggle(new TubeLight());}
}
上述例子中,开关类ToggleSwitch依赖于Light类,在目前的设计中,ToggleSwitch可以控制灯,但是控制一台电视机就很困难,因为无法让电视机继承Light。这里ToggleSwitch属于高层模块,它依赖了低层模块Light,不符合依赖倒置原则的“高层模块不应该依赖于低层模块”。所以我们可以再定义一个开关接口,让ToggleSwitch依赖于改接口,而Light和TV抽象类只需继承该接口,让其子类去实现即可。
public interface Switchable {void turnOn();void turnOff();
}public abstract class Light implements Switchable {
}public abstract class TV implements Switchable {
}public class BulbLight extends Light {@Overridepublic void turnOn() {System.out.println("BulbLight turned on...");}@Overridepublic void turnOff() {System.out.println("BulbLight turned off...");}
}public class TubeLight extends Light {@Overridepublic void turnOn() {System.out.println("TubeLight turned on...");}@Overridepublic void turnOff() {System.out.println("TubeLight turned off...");}
}public class Television extends TV {@Overridepublic void turnOn() {System.out.println("Television turned on...");}@Overridepublic void turnOff() {System.out.println("Television turned off...");}
}public class ToggleSwitch {public void toggle(Switchable switchable) {switchable.turnOn();switchable.turnOff();}public static void main(String[] args) {ToggleSwitch toggleSwitch = new ToggleSwitch();toggleSwitch.toggle(new BulbLight());toggleSwitch.toggle(new TubeLight());toggleSwitch.toggle(new Television());}
}
依赖倒置原则的核心就是面向接口编程,在实际编程中,一般需要做到如下三点:
- 低层模块尽量都要有抽象类或接口,或者两者都有
- 变量的声明类型尽量是抽象类或接口
- 使用继承时要遵循里氏替换原则
4. 里氏替换原则
里氏替换原则LSP(The Liskov Substitution Principle)指的是“如果对每个类型为T1的对象o1,都有类型为T2的对象o2,对于所有定义了T2的所有程序P来说,在所有的对象o2都被替换成o1并且T1是T2的子类型时,程序P的行为没有发生变化”。通俗地讲,就是子类型能够完全替换父类型,而不会让调用父类型的客户程序从行为上有任何改变。
下面举一个例子来说明里氏替换原则
“鸵鸟非鸟”是一个理解里氏替换原则的经典例子。我们设计一个与鸟有关的系统,我们先假设鸵鸟属于鸟,将鸵鸟类继承鸟类,鸟类的所有特性和行为都被鸵鸟类继承,比如,羽毛,有翅膀,飞行;当然鸵鸟不会飞,只能把飞行速度设为0了。假设有以下鸟类:
class Bird {double velocity;public abstract void fly();public void setVelocity(double velocity) {this.velocity = velocity;}public double getVelocity() {return this.velocity;}
}
鸵鸟类:
class Ostrich extends Bird {double velocity;public void fly() {System.out.println("I'm Ostrich, I cant't fly...");}@Overridepublic void setVelocity(double velocity) {this.velocity = 0;}@Overridepublic double getVelocity() {return 0;}
}
测试类TestBird:
测试不同鸟飞行3000米所需的时间
class TestBird {public double calcFlyTime(Bird bird) {double distance = 3000;return distance / bird.getVelocity;}
}
我们拿上述代码来测试,只要是会飞的鸟,速度再慢也不可能为0吧,所有应该都是没问题的;但如果我们用鸵鸟来测试,程序就会抛出一个 / by zero 的异常,明显不符合我们的预期。
我们得出结论:在calaFlyTime方法中,Bird类型的参数是不能被Ostrich类型的参数所代替,如果进行了替换就得不到预期的结果。因此,Ostrich类和Bird类之间的继承关系违反了里氏替换原则,它们之间的继承关系不成立,所以,鸵鸟不是鸟!
简言之,里氏替换原则为继承定义一个规范,简单地概括为4层含义:
- 子类必须完全实现父类的方法,且方法对子类是有意义的
- 子类可以有自己的个性
- 覆盖或者实现父类方法时输入参数可以被放大(只要传入的参数是子类的类型,都可以当做参数进行传递)
- 覆盖或者实现父类方法时输出参数可以被缩小(父类的一个方法的返回值是一个类型T,子类的相同方法(重载或重写)的返回值是S,要么S和T是同一个类型,要么S是T的子类)
5. 开闭原则
开闭原则OCP(The Open Closed Principle)指的是一个软件实体应当对扩展开放,对修改关闭
下面通过一个例子来说明开闭原则。
假设现在需要实现一个加法的功能,代码如下:
public class Calculate {public int add(int a, int b) {return a + b;}
}
现在的问题是,需求变了,要求是需要实现一个减法的功能,如下:
public class Calculate {public int add(int a, int b) {return a + b;}public int sub(int a, int b) {return a - b;}
}
如果需求再变,还要求实现乘法和除法的工程,继续修改代码:
public class Calculate {public int add(int a, int b) {return a + b;}public int sub(int a, int b) {return a - b;}public int mul(int a, int b) {return a * b;}public int div(int a, int b) {return a / b;}
}
如果需求再变,那么又要推翻之前设计的系统,很明显这样的做法是不可取的,在设计上出现了问题,明显违反了“开闭原则”。对此,我们可以通过创建抽象来隔离以后将要发生的同类变化。
6. 迪米特法则
迪米特法则DP(Demeter Principle)又称最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息。
迪米特法则还有个更简单的定义:只与直接的朋友通信。直接的朋友是指:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
下面我们通过一个例子来说明:
假设一公司有技术部和销售部两个部门,两个部门的员工类分别为TechDepEmployee和SaleDepEmployee ,公司方便管理员工,两个部门都有独自的管理者,分别为TcehDepManager 和SaleDepManager ;另外,又有一个公司的总管理者EnterpriseManager ,设计如下:
public class TechDepEmployee { // 技术部员工private String id;// ...
}public class SaleDepEmployee { // 销售部员工private String id;// ...
}public class TcehDepManager { // 技术部管理者public List<TechDepEmployee> getAllEmployee() {// ...}
}public class SaleDepManager { // 销售部管理者public List<SaleDepEmployee> getAllEmployee() {// ...}
}public class EnterpriseManager { // 公司管理者public void printAllEmployee(TcehDepManager tech, SaleDepManager sale) {List<TechDepEmployee> techList = tech.getAllEmployee();List<SaleDepEmployee> saleList = sale.getAllEmployee();for (TechDepEmployee e : techList) {System.out.println(e);}for (SaleDepEmployee e : saleList) {System.out.println(e);}}
}
现在我们的公司的管理者想要查看两个部门的员工信息,调用了printAllEmployee方法,在这个方法中,传入的参数TcehDepManager 类和SaleDepManager 类是EnterpriseManager 类的直接朋友,而TechDepEmployee类和SaleDepEmployee类都是通过调用getAllEmployee()方法得到的,不是EnterpriseManager 类的直接朋友,所以违反了 迪米特法则
改进后的代码如下:
public class TechDepEmployee {private String id;// ...
}public class SaleDepEmployee {private String id;// ...
}public class TcehDepManager {public List<TechDepEmployee> getAllEmployee() {// ...}public void printAllEmployee() {List<TechDepEmployee> list = this.getAllEmployee();for (TechDepEmployee e : list) {System.out.println(e);}}
}public class SaleDepManager {public List<SaleDepEmployee> getAllEmployee() {// ...}public void printAllEmployee() {List<SaleDepEmployee> list = this.getAllEmployee();for (SaleDepEmployee e : list) {System.out.println(e);}}
}public class EnterpriseManager {public void printAllEmployee(TcehDepManager tech, SaleDepManager sale) {tech.printAllEmployee();sale.printAllEmployee();}
}
迪米特法则注意事项和细节:
- 迪米特法则的核心是降低类之间的耦合
- 由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系
7. 合成复用原则
合成复用原则CRP(Composite Reuse Principle)又叫组合/聚合复用原则,指的是尽量使用组合/聚合的方式,而不是使用继承。因为继承是对象间耦合度最大的一种关系,而在程序中增加耦合无疑是给后期的开发和维护增加负担。
关于组合/聚合等类与类之间的关系可以参考UML类图简介及类与类之间的关系
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!