有种更小说的感觉呢。
前言
设计模式面向对象设计中必须遵守的规范,在大型程序的设计中使得软件代码具有可维护性、可读性、可扩展性与高内聚和低耦合。
- 可维护性:对于出现问题的代码修改起来很容易。
- 可读性:大家写的代码符合同一种规范,阅读起来就很方便;
- 可扩展性:在原先的代码上进行功能的增强比较容易;
- 高内聚:指的是在同一模块中,功能依赖较为强烈,打个比方,jdk的源码中一个类经常出现一个方法依赖类中另一个方法的情况,有时候甚至一个方法只需要调用另一个方法,一句了事。这样将同一种功能统一交给另一个方法来做反而更有利于阅读与重用。
- 低耦合:不同模块之间需要依赖足够的低,这样修改一个模块的代码后,另一个基本不用修改。如,如果我们做过一些小的web项目,教程中一组强调我们应该把pojo中的Date设置为java.util下的Date类,而不应该使用java.sql包下的Date,这样带来了在dao中两个Date类转换的麻烦。但是回头一想,如果真的有哪天dao层不使用数据库而使用别的技术,那pojo中的代码就需要大量的更改,维护起来就更麻烦了。
本文从设计模式由来开始,说明设计模式的地位,并介绍设计模式的七大基本原则,使用前后的区别会在代码中体现。
一、 概述
1. 1 由来
设计模式由上世纪90年代引入OOP编程,最早的提出者是从建筑学引入的思想,目的是解决软件工程中频繁出现的问题。
为什么是建筑学?
因为两者有共通之处:
编写一个软件就像是盖楼房,如果一开始就不规划好接下来的设计图,而是盖到哪一层就随机发挥,难免几层楼房就东倒西歪,整个项目就报废了。
如果一个建筑师不按照标准出牌,他的设计总是别具一格,那如果他一旦离职,其他人就难以接收他的设计工作。因此设计模式就是大家共同遵守的一种规范。
1.2 UML类图
UML是统一建模语言,是软件工程的一种设计技术,分为许多模块。详细的说明以及用法可以参考这篇文章:UML
UML类图是专门描述设计模式的图示。现在给出最基本的几个元素,之后会有内容专门讲解。
- 实体类及继承关系(实线箭头表示实体类的继承,箭头是三角形)
- 接口及实现(虚线表示实现接口):
IDEA中使用UML类图:其实好像不用设置,只需要右键点击一个类,选择Diagrams,选择第一个就能看到类图了。
java.io包下PrintWriter的UML类图。
idea似乎不能像eclipse一样在出现类之前设计类图,之后如果找到方法再来补充。
三、设计模式七大原则
3.1 单一原则
一个类的功能覆盖范围应该足够的低,足够的具体,而不应该囊括太多不相同的事物。
先来看一段代码:
@Test
public void test() {
Vehicle car = new Vehicle("汽车");
car.run();
Vehicle airplane = new Vehicle("飞机");
airplane.run();
Vehicle steamship = new Vehicle("轮船");
steamship.run();
}
class Vehicle{
private String name;
public Vehicle(String name) {
this.name = name;
}
public void run() {
System.out.println(this.name + "在公路上跑");
}
}
代码会输出:
汽车在公路上跑
飞机在公路上跑
轮船在公路上跑
这显然不合理。
原因在于Vehicle类描述的事物太多,或者说这个类的范围太大了。
正确的做法是:
@Test
public void test() {
VehicleOnRoad car = new VehicleOnRoad("汽车");
car.run();
VehicleInAir airplane = new VehicleInAir("飞机");
airplane.run();
VehicleOnWater steamship = new VehicleOnWater("轮船");
steamship.run();
}
class VehicleOnRoad{
private String name;
public VehicleOnRoad(String name) {
this.name = name;
}
public void run() {
System.out.println(this.name + "在公路上跑");
}
}
class VehicleInAir{
private String name;
public VehicleInAir(String name) {
this.name = name;
}
public void run() {
System.out.println(this.name + "在天上飞");
}
}
class VehicleOnWater{
private String name;
public VehicleOnWater(String name) {
this.name = name;
}
public void run() {
System.out.println(this.name + "在水上漂");
}
}
output:
汽车在公路上跑
飞机在天上飞
轮船在水上漂
看起来正常了。然而还是有个问题:
由于多写了几个类,甚至原来的类名都修改了,会使得使用这个类的test()大量修改,显然也不太方便。
对于这种小型程序,我们可以采用折中的方法。
@Test
public void test() {
Vehicle car = new Vehicle("汽车");
car.runOnRoad();
Vehicle airplane = new Vehicle("飞机");
airplane.runInAir();
Vehicle steamship = new Vehicle("轮船");
steamship.runOnWater();
}
class Vehicle{
private String name;
public Vehicle(String name) {
this.name = name;
}
public void runOnRoad() {
System.out.println(this.name + "在公路上跑");
}
public void runInAir() {
System.out.println(this.name + "在天上飞");
}
public void runOnWater() {
System.out.println(this.name + "在水上漂");
}
}
这样也能达到同样的效果。并且,我们只修改了几处方法的调用。
实际上,虽然类没有遵守单一原则,但是类中的方法确符合单一原则,这在小型程序中也是合理的。、
3.2 接口隔离原则
对于一个接口描述的功能,应当尽可能的少【即接口方法要少】,方便依赖他的类尽可能地实现自己真正需要的方法。
来看一个简单的Demo:
interface Vehicle{
void disign();
void create();
void run();
void setup();
void destroy();
}
class Driver{
void run(Vehicle vehicle) {
vehicle.setup();
vehicle.run();
}
}
class VehicleFactory{
void maintain(Vehicle vehicle) {
vehicle.disign();
vehicle.create();
vehicle.destroy();
}
}
@Test
public void test() {
// 我是一个驾驶员,想开车出去
Driver driver = new Driver();
driver.run(new Vehicle() {
@Override
public void disign() {
//???一脸懵逼
}
@Override
public void create() {
// 年纪轻轻难免承受太多
}
@Override
public void run() {
// setup和run才是我真正需要的
System.out.println("Driving");
}
@Override
public void setup() {
System.out.println("Setup!");
}
@Override
public void destroy() {
}
});
}
有两个类想要实现汽车接口的功能:一个类只想负责汽车的生产流水线,一个类只想驾驶汽车;
他们都不想关心对方的工作,然而接口的设计却使得他们不得不关心,他们因此实现了许多没必要的方法。
此外,还会造成一个严重的问题:
此时若有其他类要使用这个接口作为参数依赖,他会拥有很多自己本不该拥有的功能。
如Driver类拥有了销毁汽车的能力,这显然是不够安全的。
我们的做法是将汽车接口拆分为两个接口,给不同类去实现。
interface MaintainedVehicle{
void design();
void create();
void destroy();
}
interface DrivedVehicle{
void setup();
void run();
}
class VehicleFacory {
public void maintain(MaintainedVehicle maintainVehicle) {
maintainVehicle.design();
maintainVehicle.create();
maintainVehicle.destroy();
}
}
class Driver {
public void drive(DrivedVehicle drivedVehicle) {
drivedVehicle.setup();
drivedVehicle.run();
}
}
@Test
public void test() {
Driver driver = new Driver();
driver.drive(new DrivedVehicle() {
@Override
public void setup() {
System.out.println("SetUp!");
}
@Override
public void run() {
System.out.println("High...");
}
});
}
这样使用起来实现方法就方便多了,而且Driver类也不会有越权的方法调用。
然而还是不能过分的分离接口,将某些具有大关联的方法放在一个接口会更有利于设计。如,若是test()中使用的Driver类需要实现两个接口,就要在drive()方法中引入两个接口,并且在内部中都要实现,爷比较不合理。
3.3 依赖倒转原则
一个类依赖的不应该是一个具体的实现类,而是应该是一个抽象的接口或者方法。
即让实现类与需求类同时依赖接口,让接口成为两者之间的缓冲。
这样做的好处是,对于接口实现类的修改基本不会影响到依赖这个接口的类,降低了依赖关系的耦合度。
额,很懵。
总之,对于依赖的对象最好依赖其描述这个功能规范接口,看一个例子:
class UserDaoByDB{
public void findUserByID(String id) {
System.out.println("通过用户id:" + id + " 从数据库查询了用户");
}
}
class UserDaoByXML{
public void findUserByID(String id) {
System.out.println("通过用户id:" + id + " 从XML查询了用户");
}
}
class UserService{
private UserDaoByDB userDaoByDB = new UserDaoByDB();
public void findUser() {
userDaoByDB.findUserByID("1");
}
}
@Test
public void test() {
new UserService().findUser();
}
乍一看,没啥问题,没什么好黑的。
但是,如果有需要:现在我不想使用数据库来查询了,而是使用XML来查询,这样我就需要去service中把成员变量名字改了,也要把findUser()中的对象也给改了,尤其是真正开发的时候,service的方法众多,一个一个改岂不是改死人。
如果,遵从依赖倒转原则,代码就会变成这样。
interface UserDao {
public void findUserByID(String id);
}
class UserDaoByDB implements UserDao {
public void findUserByID(String id) {
System.out.println("通过用户id:" + id + " 从数据库查询了用户");
}
}
class UserDaoByXML implements UserDao {
public void findUserByID(String id) {
System.out.println("通过用户id:" + id + " 从XML查询了用户");
}
}
class UserService {
// 实际开发中,右边会变成: beanFactory.getBean("userDao"); 甚至可以使用注解自动装配
private UserDao userDao = new UserDaoByDB();
public void findUser() {
userDao.findUserByID("1");
}
}
@Test
public void test() {
new UserService().findUser();
}
现在,我们只需要将成员变量右边的实现类改一下就行,是不是一下子方便了很多?
当我们在实际使用时,甚至可以使用配置文件来指定userDao的类型,这样,我们可以做到完全不修改任何代码的情况下随意的切换dao的实现方法。
3.4 里式替换原则
里式替换原则是对继承的要求。
里式替换原则要求对于继承一个父类A的子类B不应该重写父类A已经实现的方法,而只能够在这个类上拓展。
这样做的好处是,不会引起其他类使用父类引用调用子类方法时产生的错误,比如调用者在不知情的情况下实现了完全不同的功能,避免多态时的隐形错误。
当确实有要求要改变一个类的方法时,应该让原来的类A和新的类B共同去实现一个更基础的类Base,让使用这个继承体系的程序员马上明白这是不同的两个类,因此使用方法就会有目的的找自己需要的Base类的不同实现。
如果B类对A类的其他方法有需要,又不得不对A类的某些方法重写,应该使用**组合**的方式去使用A类,即在A类中定义一个成员变量,通过B类自身的方法去间接调用A的方法。
总之,继承是件方便但是危险的工具,遵从里式替换原则可以避免因为继承带来的高耦合与未知的错误。
3. 5 开闭原则
开闭原则是面向对象最重要、最基础的原则,没有之一。任何的设计模式与设计原则就是为了服务开闭原则。
开闭原则是指:
对拓展开放,对修改关闭
扩个句:
对类、方法、函数的提供者来说扩展开放,对类、方法、函数的使用者来说修改关闭。
比如说:
JDK在不断更新:
众所周知1.8版本对1.7版本的HashMap进行了升级,变得更高效了。
然而难道我们以前使用了的HashMap的类就全部作废了?并不是。
因为他只是在原来的基础上拓展了,而我们不必为他的拓展就原来的使用类进行任何修改。
这样还是不够具体,用code说明:
//类的使用方
class VehicleFactory{
public void make(Vehicle vehicle) {
vehicle.run();
}
}
// 类的提供方
interface Vehicle{
public void run();
}
class Car implements Vehicle {
@Override
public void run() {
System.out.println("汽车跑");
}
}
// 提供方可以拓展新工能,而使用方无需更改代码
class Boat implements Vehicle {
@Override
public void run() {
System.out.println("轮船游");
}
}
@Test
public void test() {
new VehicleFactory().make(new Car());
// 我也可以使用拓展功能
new VehicleFactory().make(new Boat());
}
当提供方添加新的类Boat时,使用方VehicleFactory并没有修改代码,这样,既实现了对提供方功能的拓展,又实现了不用修改使用方的代码。这样的设计就符合开闭原则。
开闭原则是要提供方和使用方共同遵守的:
提供方不能修改原来已经实现好的方法;适用方要尽可能遵从依赖倒转。
3.6 迪米特法则
又称最少知道原则。即了解到其他类的信息以及被其他类了解越少越好。
了解是说,在本类中使用其他类。
迪米特要求:尽量只与**直接朋友**产生交流,不与**陌生类**交流。
直接朋友:类中成员变量,方法返回值,方法参数。
陌生类:在局部区域定义的变量。
迪米特法则的目的是为了降低耦合,间接要求我们多使用private和final,防止其他类对本类的修改。同时,我们要较少的使用其他类来实现功能。
总之,不要在方法中使用陌生类。
3.7 合成复用原则
若类B想使用类A的功能,应该尽量避免使用继承,而是采用聚合/组合来替代这种关系。
B想使用A的功能,可以通过定义A的成员变量,方法参数两种形式来使用,贸然使用继承会使得B和A之间产生高耦合。
四、类之间的关系
- 依赖
- 一个类A依赖另一个类B,是说A类的方法中使用了B类对象或者静态方法。
- 广义的依赖包含任何类之间产生关系的场合,即接下来的五种关系都可以说是依赖。
- 如:人看书会用到书这个类:
依赖的几种形式:
class Person{
//使用形参产生依赖
public void read(Book book) {
System.out.println("看了 " + book);
}
// 使用临时变量产生依赖(违反迪米特法则)
public void buy() {
Book book = new Book();
System.out.println("买了本" + book);
}
//使用静态方法产生依赖
public void close() {
Book.close();
}
}
class Book{
public static void close() {
System.out.println("合上了书本");
}
}
- 关联
- 类A与类B产生了关联,是说A的成员区域出现了B的引用。
关联较依赖耦合度更强,从临时范围上升到了全局范围。
关联关系双方是平等的,只是一个类恰巧需要另外一个类而已。
class Person{
Book book = new Book();
public void read() {
System.out.println("看了 " + book);
}
}
- 聚合
一个类A拥有其他几个类,有很强的主从关系。
但是聚合并没与说若A缺少了其中一些类就不是A了。
比如说我们填网站信息时候有些信息可以选择不填,网站依然可以确定我是那个唯一的用户。
聚合较关联的耦合度更强,主要体现在这种主从关系。
class User{
private String nickname;
private Email email;
private Address address;
}
- 组合
一个类A是其他几个类的组合,是说A本身就是由这几个类组成,若缺少其中一样,A就不是A了。
组合较聚合的耦合度更强,主要表项在这种缺一不可的语境中。
如人的肉体和灵魂缺一不可,否则就不是人了。
class Person{
Soul soul;
Body body;
}
class Soul{
}
class Body{
}
组合关系一种在java中的表现是级联删除:即若将对象a从数据库删除,则b也必定删除。
关联,聚合,组合的语义很微妙,需要根据表达的语境和实际生活来联想。
- 泛化
更高的耦合关系就是继承。
继承是IS A关系;
聚合/组合是Has A关系。
- 实现
实现接口。