什么是设计模式?
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
23中设计模式背后是7大设计原则,也就是说,每个设计模式都属于一个或多个设计原则
7大原则的背后是一个字 分
开闭原则、单一职责原则、里氏替换原则、依赖倒置原则、迪米特法则(最少知道原则)、接口隔离原则、组合优于继承原则
对象不应承担太多功能,正如一心不能而用,比如太多的工作(种类)会使人崩溃。唯有专注才能保证对象的高内聚;唯有唯一,才能保证对象的细粒度。
每个类、方法、框架只专一做一件事情
例如 统计文本文件有多少个句子
public static void main(String[] args) throws Exception {
//统计一个文本文件中,有多少字符
//reaer默认查询的码表是与操作系统一致的 码表,我们操作系统默认中文,即为gbk编码
//而gbk码表一个汉字占两个字符,而且汉字的开头都是1开头的
//---------------------负责加载文件并转换成字符串-------------
FileReader reader = new FileReader("C:\\Users\\Lenovo\\Desktop\\gzmm.txt");
BufferedReader br = new BufferedReader(reader);
String line = null;
StringBuilder sb =new StringBuilder("");
while ((line =br.readLine())!=null ){
sb.append(line);
sb.append("");
}
//---------------------------字符串的切割,统计------------------
String[] words = sb.toString().split("[.?!。]+");
for (String string:words){
System.out.println(string+"@");
}
System.out.println(words.length);
br.close();
}
上面的写法就违反了单一职责,同一个main方法中,我们即让它去加载文件,也让它去做分割,这样做的坏处是,每当我们需要调用其中的一个功能时(如我有文件只需要分割),仍需要重写一遍。
//----------------只负责文件的读取转换成string字符串
public static String load(String path) throws IOException {
FileReader reader = new FileReader(path);
BufferedReader br = new BufferedReader(reader);
String line = null;
StringBuilder sb = new StringBuilder("");
while ((line = br.readLine()) != null) {
sb.append(line);
sb.append("");
}
br.close();
return sb.toString();
}
//----------------只负责字符串的切割、解析
public static int parse(String sb ,String regex){
String[] words = sb.toString().split(regex);
for (String string:words){
System.out.println(string+"@");
}
return words.length;
}
public static void main(String[] args) throws IOException {
String str = loadFile("F:\\1.txt");
String regex ="[^a-zA-Z]+";
System.out.println(Textlength(str,regex));
}
优点
通过单一职责,可以提高代码的重用性,通过单一职责的方法得到的数据我们不再有耦合,拿来可以做的事也不再局限。 提高系统的可维护性
代码的可读性提高了、变更风险更低、
降低类、方法、模块的复杂度
软件对象(类、模块、方法等)应该对于扩展是开放的,对修改是关闭的。比如:一个网络模块,原来只有服务端功能,而现在要加入客户端功能,那么应当在不用修改服务端功能代码的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将客户端和服务端分开。公共部分抽象出来。
对扩展新功能是开放的、对修改原来的功能是关闭的(加新功能不能影响原来代码)
创建一个汽车类:
`public class Car {
private String brand;
private String color;
private boolean louyou;
private double price;
@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", color='" + color + '\'' +
", louyou=" + louyou +
", price=" + price +
'}';
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public boolean isLouyou() {
return louyou;
}
public void setLouyou(boolean louyou) {
this.louyou = louyou;
}
public double getPrice() {
//在这里修改来原来的代码违反了开闭原则
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
当变化来临时,例如汽车的价格现在需要打折(打8折),这时,我们在Car的源代码中修改,就违反了开闭原则
违反开闭原则的实例:
public double getPrice() {
//在这里修改来原来的代码违反了开闭原则
return price*0.8;
}
正确的方式:
public class DiscountCar extends Car{
@Override
public double getPrice() {
//在这里修改子类的方法并不会影响父类的代码
return super.getPrice() * 0.8;
}
}
开闭原则应该遵循应用场景去考虑,如果源代码就是你自己写的,而且需求是稳定的,那么,直接修改源代码也是一个简单的做法,但当源代码是别人的代码或架构是,我们就要去符合开闭原则,防止破坏结构的完整性!
为什么要使用开闭原则?
1、只要是面向对象的编程,在开发过程中都会强调开闭原则
2、是最基础的设计原则,其他五个设计原则都是开闭原则的具体形态
3、可以提高代码的复用性
4、可以提高代码的可维护性
如何使用开闭原则?
1、抽象约束
抽象对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放。
2、元数据控件模块行为
3、制定项目章程
4、封装变化
将相同的变化封装到一个接口或抽象类中,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同变化出现在同一个接口或抽象类中。
接口隔离原则(Interface Segregation Principle):
1、客户端不应依赖它不需要的接口
2、类间的依赖关系应该建立在最小的接口上
其实通俗来理解就是,不要在一个接口里面放很多的方法,这样会显得这个类很臃肿。接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,使接口更加灵活轻便。或许有的人认为接口隔离原则和单一职责原则很像,但两个原则还是存在着明显的区别。单一职责原则是在业务逻辑上的划分,注重的是职责。接口隔离原则是基于接口设计考虑。例如一个接口的职责包含10个方法,这10个方法都放在同一接口中,并且提供给多个模块调用,但不同模块需要依赖的方法是不一样的,这时模块为了实现自己的功能就不得不实现一些对其没有意义的方法,这样的设计是不符合接口隔离原则的。接口隔离原则要求"尽量使用多个专门的接口"专门提供给不同的模块。
反例:
public interface Animal {
void eat();
void swim();
void fly();
}
public class Dog implements Animal{
@Override
public void eat() {
System.out.println("狗喜欢吃骨头");
}
@Override
public void swim() {
System.out.println("狗会狗刨");
}
@Override
public void fly() {
throw new RuntimeException("The dog cannt fly ,you can you up , no can no bb!");
}
}
public class Bird implements Animal{
@Override
public void eat() {
System.out.println("鸟吃虫子");
}
@Override
public void swim() {
throw new RuntimeException("鸟不会游泳");
}
@Override
public void fly() {
System.out.println("鸟会飞");
}
}
这种总的接口写了很多方法,但是其他类去实现的时候会实现很多没有意义的方法,正确的方式应该是,分成很多小接口,不同接口分胆不同的功能,让类自己去选择实现自己需要的方法:
public class Bird implements EnableEat,EnableFly{
@Override
public void eat() {
System.out.pritnln("狗吃骨头")
}
@Override
public void fly() {
//....
}
}
public class Dog implements EnableEat,EnableFly{
@Override
public void eat() {
//...
}
@Override
public void fly() {
//...
}
}
上层不能依赖于下层,他们都应该依赖抽象
什么是上层? 调用别人的方法的那一层
什么是下层? 方法被调用,提供方法的那一层
类与类之间的关系有几种?
依赖、继承、关联、组合、聚合
经典Uml类图:
讲解类图博客链接
违反依赖倒置的反例:人喂动物
public class AppTest {
//此时来了一个需求想要喂养猫、鹦鹉、金鱼等等
public static void main(String[] args) {
Person person = new Person();
Dog dog = new Dog();
person.feed(dog);
Cat cat= new Cat();
person.feed(cat);
}
}
class Person{
public void feed(Dog dog){
System.out.println("人一撒狗粮");
dog.eat();
}
//此时及违反了开闭原则(修改了源代码)又违反了依赖倒置原则
public void feed(Cat cat) {
cat.eat();
}
}
class Dog{
public void eat(){
System.out.println("狗吃骨头");
}
}
class Cat{
public void eat(){
System.out.println("猫吃鱼天经地义");
}
}
在上面的代码中,人要喂狗,依赖于有一条狗,人作为上层依赖于下层,这样有什么坏处呢?
坏处是,当变化来临时,比如,人又养了一只猫,那么上层人这个类当中,就必须在添加喂猫的方法,每当下层变动时,上层也会跟着变动,而我们希望下层变动时,上层不会跟着改变
正例:他们都应该依赖于抽象
public class AppTest {
public static void main(String[] args) {
Person person = new Person();
Dog dog = new Dog();
person.feed(dog);
Cat cat= new Cat();
person.feed(cat);
}
}
interface Animal{
void eat();
}
class Person{
public void feed(Animal animal){
System.out.println("人一撒粮");
animal.eat();
}
}
class Dog implements Animal{
public void eat(){
System.out.println("狗吃骨头");
}
}
class Cat implements Animal{
public void eat(){
System.out.println("猫吃鱼天经地义");
}
}
迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。英文简写为: LoD 只和朋友通信
1.类中的字段
2.方法的返回值
3.方法的参数
4.方法中的实例对象
5.对象本身
6.集合中的泛型
反例:关电脑的操作
public class AppTest {
public static void main(String[] args) {
Person person = new Person();
person.closeTheComputer();
}
}
class Computer{
public void saveData(){
System.out.println("保存数据");
}
public void killProgress(){
System.out.println("关掉程序");
}
public void closeScreen(){
System.out.println("关掉屏幕");
}
public void powerOff(){
System.out.println("关掉计算机的电源");
}
}
class Person{
private Computer c =new Computer();
//person对于computer知道的细节太多了
//这样代码的复杂度就太高了
public void closeTheComputer(){
c.saveData();
c.killProgress();
c.closeScreen();
c.powerOff();
}
}
当用户关闭电脑时,需要调用计算机的各个方法,但是这些方法的细节太多了,会出现用户流程出错,遗漏调用等等,对于用户来言,他只需要知道关机按钮就够了
正例:封装细节,提供接口
class Computer {
private void saveData() {
System.out.println("保存数据");
}
private void killProgress() {
System.out.println("关掉程序");
}
private void closeScreen() {
System.out.println("关掉屏幕");
}
private void powerOff() {
System.out.println("关掉计算机的电源");
}
public void closeComputer() {
saveData();
killProgress();
closeScreen();
powerOff();
}
}
class Person {
private Computer c = new Computer();
//person对于computer知道的细节太多了
//这样代码的复杂度就太高了
public void closeTheComputer() {
c.closeComputer();
}
}
里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
ps:子类替换父类时,不能比父类的访问修饰更严格,不能抛出父类不存在的异常
使用时,需要考虑:
1.是否有“is-a”的关系
2.有is-a关系后,要考虑子类替换父类后会不会出现逻辑变化
反例:正方形非长方形
public class AppTest {
public static void main(String[] args) {
Rectangle rectangle = new Square();
rectangle.setWidth(12);
rectangle.setLength(20);
System.out.println(rectangle);
Util.transform(rectangle);
}
}
class Rectangle{
private double length;
private double width;
@Override
public String toString() {
return "Rectangle{" +
"length=" + length +
", width=" + width +
'}';
}
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
public Rectangle() {
}
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
class Util{
public static void transform(Rectangle rectangle){
while (rectangle.getWidth()<=rectangle.getLength()){
rectangle.setWidth(rectangle.getWidth()+1);
System.out.println("{"+rectangle.getLength()+":"+rectangle.getWidth()+"}");
}
}
}
class Square extends Rectangle{
private double sideLength;
@Override
public double getLength() {
return sideLength;
}
@Override
public void setLength(double length) {
this.sideLength=length;
}
@Override
public double getWidth() {
return sideLength;
}
@Override
public void setWidth(double width) {
this.sideLength =width;
}
}
● 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
● 提高代码的重用性;
● 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
● 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
● 提高产品或项目的开放性。
● 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
● 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
● 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。
在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
WSDL 是基于 XML 的用于描述 Web Services 以及如何访问 Web Services 的语言。
里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。
1.子类必须完全实现父类的方法
2.子类可以有自己的个性
3.覆盖或实现父类的方法时输入参数可以被放大
4.覆写或实现父类的方法时输出结果可以被缩小
反例:继承hashset,重写父类add方法,每次add元素时count+1;
public class AppTest {
public static void main(String[] args) {
MySet mySet = new MySet();
HashSet<String> set = new HashSet<>();
set.add("a");
set.add("c");
set.add("f");
set.add("h");
mySet.addAll(set);
System.out.println(mySet.getCount());
}
}
class MySet extends HashSet
{
private int count= 0;
public boolean add(Object e){
count ++;
return super.add(e);
}
public int getCount(){
return count;
}
@Override
public boolean addAll(Collection c) {
count+=c.size();
return super.addAll(c);
//父类 的addall中回调了add方法
// public boolean addAll(Collection extends E> c) {
// boolean modified = false;
// for (E e : c)
// if (add(e))
// modified = true;
// return modified;
}
}
假如我们继续重写addAll方法添加相应的判断时,又会出现新的问题,我们的count并没有正确的累计,因为在HashSet的源码addAll方法中,回调了add方法,并没有解决需求
那我们不重写addAll,反正它会回调add,完成计数,就没有问题了吗?
其实并不能解决问题,hashset的源码我们不能保证永远不会更改,假如在下一个版本中,hashset的作者更改了addAll方法,那么我们的功能也会不能正常实现了!
当继承的父类作者不是我们自己的时候,我们没有办法保证父类代码不会变更,假如我们继承了这个父类,那么我们最好是只去复用父类的代码,避免去重写或新建方法,防止源码结构变更带来的打击
也就是说,在我们需要重用代码,并且重用的代码作者并不是我们自己的时候,我们要采用组合的方式。
正例:组合优于继承
public class AppTest {
public static void main(String[] args) {
MySet mySet = new MySet();
HashSet<String> set = new HashSet<>();
set.add("a");
set.add("c");
set.add("f");
set.add("h");
mySet.addAll(set);
System.out.println(mySet.getCount());
}
}
class MySet {
private int count = 0;
Set set = new HashSet();
public boolean add(Object e) {
count++;
return set.add(e);
}
public int getCount() {
return count;
}
public boolean addAll(Collection c) {
count += c.size();
return set.addAll(c);
}
}
这样来写,我们类中的add和addAll方法跟HashSet中的add和addAll方法的不在有关系,也能解决这个问题