保研笔记二 软件工程与计算卷二(13-16章)

目录

第13章 详细设计中的模块化与信息隐藏

1.耦合与内聚(名词解释)

(1)耦合

(2)内聚

2.信息隐藏基本思想

第14章 面向对象的模块化

14.1.访问耦合

 14.1.1隐式耦合:Cascading Message 级联调用问题

14.2.解耦方法

14.2.1.针对接口编程

14.2.2.接口最小化/接口分离原则

14.2.3.迪米特法则

 14.3.继承耦合

14.4.解耦方法

14.4.1.liskov替换原则

14.4.2.组合替代继承

14.3.内聚

14.3.1. 方法内聚

14.4. 提高内聚的方法

 14.4.1.单一责任原则(SRP)

14.5.Summary:Principles from Modularization 模块化的原则

第15章  面向对象的信息隐藏

15.1.信息隐藏的含义

15.2.类的封装

15.3.开闭原则OCP

15.4.依赖倒置原则DIP

第16章 设计模式

16.1.如何实现可修改性、可扩展性、灵活性

 16.2设计模式和策略配对

16.3.策略模式

16.4.工厂模式

 16.5.单件模式

 16.6.迭代器模式


参考:南软考研大书,软工二

这位大佬:Blog of Samperson (samperson1997.github.io)

还有这位大佬:SpriCoder的博客

第13章 详细设计中的模块化与信息隐藏

1.耦合与内聚(名词解释)

(1)耦合

描述的是两个模块之间的关系的复杂程度。
耦合根据其耦合性由高到低分为几个级别:模块耦合性越高,模块的划分越差,越不利于软件的变更和重用。1、2、3不可接受,4、5可以被接受,6最理想
(1)内容耦合(一个模块直接修改另一个模块的内容,如GOTO语句;某些语言机制支持直接更改另一个模块的代码;改变另一个模块的内部数据)
(2)公共耦合(全局变量,模块间共享全局数据,例子:全局变量)
(3)重复耦合(模块间有重复代码)
(4)控制耦合(一个模块给另一个模块传递控制信息,如“显式星期天”)
(5)印记耦合(共享数据结构却只是用了一个部分)
(6)数据耦合(模块间传参只传需要的数据,最理想)

(2)内聚

表达的是一个模块内部的联系的紧密性。
内聚性由高到低分为:内聚性越高越好,越低越不易实现变更和重用,3、4、5等价,1、2最好,6、7不能接受。
(1)信息内聚(模块进行许多操作,各自有各自的入口点,每个操作代码相对独立,而且所有操作都在相同的数据结构上进行:如栈)
(2)功能内聚(只执行一个操作或达到一个目的)
(3)通信内聚(对相同数据执行不同的操作:查书名、查书作者、查书出版商)
(4)过程内聚(与步骤有关:守门员传球给后卫、后卫传给中场球员、中场球员传给前锋)
(5)时间内聚(与时间有关:起床、刷牙、洗脸、吃早餐)
(6)逻辑内聚(一系列可替换的操作:如坐车、坐飞机)
(7)偶然内聚(多个不相关的操作:修车、烤面包、遛狗、看电影)

【题型】对实例,说明它们之间的耦合程度与内聚,给出理由。书上P226习题

2.信息隐藏基本思想

每个模块都隐藏一个重要的设计决策——职责。职责体现为模块对外的一份契约,并且在这份契约之下隐藏的只有这个模块知道的决策或者说秘密,决策实现的细节仅自己知道。
模块的信息隐藏:模块的秘密(容易变更的地方):根据需求分配的职责、内部实现机制。

【题型】对实例,说明其信息隐藏程度好坏。教材222页

【项目实践】耦合的处理?
分层风格:仅程序调用与简单数据传递
包设计:消除重复
分包:接口最小化
创建者模式:不增加新的耦合
控制者模式:解除View与Logical的直接耦合

内聚的处理?
分层:层间职责分配高内聚,层内分包高内聚
信息专家模式
控制器与委托式控制风格

信息隐藏处理?
分层与分包:消除职责重复、最小化接口:View独立?数据库连接独立?
模块信息隐藏:模块需求分配与接口定义
类信息隐藏:协作设计,接口定义
变化设计?分层风格、RMI

第14章 面向对象的模块化

14.1.访问耦合

 14.1.1隐式耦合:Cascading Message 级联调用问题

  • 避免隐式耦合,变为显式耦合,降低耦合度
  • 使用委托的方式来解决,委托给一个类来完成这个业务

14.2.解耦方法

14.2.1.针对接口编程

  1. 编程到所需的接口,不仅是受支持的接口
  2. 按照约定设计
    1. 模块/类合同:所需方法/提供的方法
    2. 方法合同:前提条件,后置条件,不变式
    3. 前置条件( precondition):用例在调用某个方法时必须满足的条件。
    4. 后置条件(postcondition):实现在方法返回时必须达到的要求。
    5. 副作用(side effects):方法可能对对象产生的任何其他变更。
  3. 在考虑(非继承的)类与类之间的关系时,一方面要求值访问对方的接口,另一方面要避免隐式访问。

14.2.2.接口最小化/接口分离原则

独立接口避免不必要偶合

14.2.3.迪米特法则

  1. 通俗说法
    1. 你可以自己玩。(this)
    2. 你可以玩自己的玩具,但不能拆开它们(自己的成员变量)
    3. 你可以玩送给你的玩具。(方法)
    4. 你可以玩自己制作的玩具。(自己创建的对象)
  2. 更加形式化的说法:
    1. 每个单元对于其他单元只能拥有优先的知识,只是与当前单元紧密联系的单元
    2. 每个单元只能和它的朋友交谈,不能和陌生单元交谈
    3. 只和自己的直接的朋友交谈
  3. 书本p233

 

 14.3.继承耦合

  1. 在以上的各种类型的继承关系中,修改规格、修改实现、精化规格是不可以接受的。
  2. 扩展是最好的继承耦合

14.4.解耦方法

14.4.1.liskov替换原则

这人写得蛮清楚的:深度解析设计模式七大原则之——里氏替换原则

  1. 子类可以实现父类中的抽象方法,但是不能重写(覆盖)父类的非抽象方法。
  2. 当子类需要重载父类中的方法的时候,子类方法的形参(入参)要比父类方法输入的参数更宽松(范围更广)。
  3. 重写或者实现父类方法的时候,方法的返回值可以被缩小,但是不能放大。

“在派生类中重新定义一种方法时,只能用一个较弱的方法代替其先决条件,而用一个较强的方法代替其后置条件” — B. Meyer,1988年

问题案例

Is a Square a Rectangle?

Rect r = new Rect();
setWidth = 4;
setHeight = 5;
assert(20 == getArea());
class Square extends Rect{
   // Square invariant, height = width
   setWidth(x) {
      setHeight()=x;
   }
   setHeight(x) {
      setWidth(x)
   }
} // violate LSP?

正方形的长宽相同,不必输入width和height两个数。 子类比父类条件更强,多限制条件。说明前置条件过强。

Penguin is a bird?

class Bird {
   // has beak, wings,...
   public: virtual void fly();
   // Bird can fly
};
class Parrot : public Bird {
   // Parrot is a bird
   public: virtual void mimic();
   // Can Repeat words...
};
class Penguin : public Bird {
   public: void fly() {
      error ("Penguins don’t fly!");
   } 
};

void PlayWithBird (Bird abird) {
   abird.fly();
   // OK if Parrot.
   // if bird happens to be Penguin...OOOPS!!
} 

企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。 

课堂练习

左边不好,因为前置条件过强,一般的门不会报警。

右边不好,door不可替代alarm

14.4.2.组合替代继承

  1. 组合优于继承
  2. 使用继承实现多态
  3. 使用委托不继承重用代码!

继承/组合 实例一

  • 如果出现一个用户既是 Passenger 也是 Agent
  • Java不允许多继承

  • 直接的想法就是直接组合
  • Person里面持有Passenger、Agent,但是这时候对于单一身份的人是很奇怪的

案例二

  • Person持有Role,Passenger和Agent实现抽象接口PersonRole
  • Role可以是一个List

案例三

class Object {  
	public: virtual void update() {}; 
	virtual void draw() {};
 	virtual void collide(Object objects[]) {}; 
}; 
class Visible : public Object { 
    public:
      virtual void draw() { 
        /* draw model at position of this object */ }; 
    private: Model* model; 
}; 
class Solid : public Object { 
    public:
      virtual void collide(Object objects[]) { 
	/* check and react to collisions with objects */ }; 
}; 
class Movable : public Object { 
    public:
      virtual void update() {
		 /* update position */ }; 
};
  • 问题:游戏引擎中存在很多的对象,三个类分别实现方法之一
  • 继承三件事但是只做了一件,Promise No Less不符合
  • 接口应该拆成3个

改进方案:

14.3.内聚

  1. 内聚的分类参考课本237页,功能内聚、信息内聚、过程内聚、时间内聚、逻辑内聚、偶然内聚。

  • 方法和属性保持一致

  • 提高内聚性:将一个类分为三个类

  • 将时间抽象出来

14.3.1. 方法内聚

  1. 一类方法是普通耦合
  2. 所有方法尽一责
    1. 信息内聚
    2. 相对功能(功能内聚)
    3. 第九个原则:单一职责原理

14.4. 提高内聚的方法

 14.4.1.单一责任原则(SRP)

“一个类只有一个改变的理由”-罗伯特·马丁(Robert Martin)

  1. 与内聚性相关并从中导出,即模块中的元素应在功能上紧密相关
  2. 班级履行某种职责的责任也是班级变化的原因
  3. 一个高内聚的类不仅要是信息内聚的,还应该是功能内聚的。

问题案例一

  • 修改的原因:
    • 业务逻辑
    • XML格式
  • 如何修改如何分开

  • 我们将两部分职责分离

案例二

  • 打电话和挂起两个职责分离开

案例三

  • 几何画板:Draw和Area的计算如何分开

案例四:

  • 解决方案:集合长方形和图形长方形一一对应

14.5.Summary:Principles from Modularization 模块化的原则

  1. 《Global Variables Consider Harmful》 全局变量被认为是有害的
  2. 《To be Explicit》让代码清晰一点
  3. 《Do not Repeat》避免重复
  4. 《Programming to Interface(Design by Contract)》面向接口编程,按照契约设计
  5. 《The Law of Demeter》迪米特法则
  6. 《Interface Segregation Principle(ISP)》接口分离原则
  7. 《Liskov Substitution Principle (LSP)》里氏替换原则:Request No More, Promise No Less
  8. 《Favor Composition Over Inheritance》 选择组合而不是继承
  9. 《Single Responsibility Principle》单一职责原理

第15章  面向对象的信息隐藏

15.1.信息隐藏的含义

每一个模块都隐藏了这个模块中关于重要设计决策的实现,以至于只有这个模块的每一个组成部分才能知道具体的细节

需要隐藏的两种常见设计决策

  • 需求(模块说明的主要秘密)与实现——暴露外部表现,封装内部结构
  • 实现机制变更(模块说明的次要秘密)——暴露稳定抽象接口,封装具体实现细节

面向对象机制

  • 封装:封装类的职责,隐藏职责的实现+预计将要发生的变更
  • 抽象类(接口)/继承(实现):抽象它的接口,并隐藏其内部实现

15.2.类的封装

  1. 目的是信息隐藏
  2. 封装将数据和行为同时包含在类中,分离对外接口内部实现
    1. 接口是模块的可见部分:描述了一个类中的暴露到外界的可见特征
    2. 实现被隐藏在模块之中:隐藏实现意味着只能在类内操作,更新数据,而不意味着隐藏接口数据。
  3. 封装的初始观点:把数据(内部结构)隐藏在抽象数据类型内
    新观点(信息隐藏):隐藏任何东西:数据与行为、复杂内部结构、其他对象、子类型信息、潜在变更
  4. 封装数据与行为:除非(直接或间接)为满足需求(类型需要),不要将操作设置为public。类型需要的操作:为了满足用户任务而需要对象在对外协作中公开的方法,例如下图的4个操作(属于后一个对象,为满足计算商品总价的任务)
    除非(直接或间接)为满足需求(类型需要),不要为属性定义getX方法和setX方法,更不要将其定义为public。例如上一示例中的getPrice()
  5. 封装结构:不要暴露内部的复杂数据结构,经验表明复杂数据结构是易于发生修改的。例如暴露了内部使用List数据结构。
    改进:Iterator模式(所有涉及到集合类型的操作都可能会出现此问题)
  6. 封装其他对象:委托而不是提供自己拥有的其他对象的引用,或者new一个新对象返回。除非Client对象已经拥有了该其他对象的引用,这时返回其引用不会增加总体设计的复杂度可以保证Sales只需要关联SalesList,不需要关联SalesLineItem和Commodity;从整个设计来讲,Sales不需要知道SalesList里面存储的是SalesLineItem的集合,更不需要知道SalesLineItem使用了Commodity类型。
  7. 封装子类(LSP:子类必须能够替换他们的基类)
  8. 封装潜在变更:识别应用中可能发生变化的部分,将其与不变的内容分离开来
    封装独立出来的潜在变化部分,这样就可以在不影响不变部分的情况下进行修改或扩展( DIP 和OCP)
     

15.3.开闭原则OCP

对扩展开放,对修改封闭
违反了OCP原则的典型标志:出现了switch或者if-else
分支让程序增加复杂度,修改时容易产生新错误(特例:创建)

就是那种有扩展的,比如:面向os和面向andriod,这时候需要用工厂模式等把他们分隔开,便于扩展(可能还有面向mac等等)。

15.4.依赖倒置原则DIP

(与工厂结合紧密,解决new的创建问题)
I. 高层模块不应依赖底层模块,两者都应依赖抽象
II. 抽象不应依赖细节,细节应依赖抽象
使用抽象类(继承)机制倒置依赖

示例:A依赖于B:B不是抽象类,所以A依赖于具体,而不是抽象,如果需要变更B的行为,就会影响到A
添加抽象类BI,让 B实现(继承)BI:A依赖于BI,B依赖于BI,BI是抽象类,所以是依赖于抽象,BI比较稳定,如果B发生变更,可以通过为BI扩展新的实现(子类型)来满足

题目类似于:

第16章 设计模式

16.1.如何实现可修改性、可扩展性、灵活性

教材263页
需要进行接口和实现的分离:通过接口和实现该接口的类;通过子类继承父类
注意:继承关系(A+B)可能使得灵活性下降,因为父类接口的变化会影响子类,这时可以通过组合关系来解决。

利用抽象类机制实现可修改性和可扩展性:只要方法的接口保持不变,方法的实现代码是比较容易修改的,不会产生连锁反应。通过简单修改创建新类的代码,就可以相当容易地做到扩展新的需求(不用修改大量与类方法调用相关的代码。
利用委托机制实现灵活性:继承的缺陷:一旦一个对象被创建完成,它的类型就无法改变,这使得单纯利用继承机制无法实现灵活性(类型的动态改变)。利用组合(委托)机制可以解决这个问题

 16.2设计模式和策略配对

  1. 策略模式:减少耦合、依赖倒置。
  2. 抽象工厂模式:职责抽象、接口重用。
  3. 单件模式:信息隐藏、职责抽象。
  4. 迭代器模式:减少耦合、依赖倒置。

16.3.策略模式

减少耦合,符合开闭原则,易于扩展;也符合依赖倒置原则,具体依赖于抽象。

test:通过new参数新建employee,由set函数设置具体策略。

Context:employee,包含了employee的个人属性,set策略,调用接口函数实现策略。

Strategy:包括接口Strategy,以及实现类具体Strategy,Context设置了具体策略后,通过调用接口实现具体策略。

简化一下的模板子:

/**
 * 策略模式
 * 当一个功能的实现可以使用多种算法或者方式的时候
 * 如果选择在业务代码if等分支语句下硬编码,在类似场景多次出现的时候如果修改会改很多处地方,违反开闭原则
 * 基于开闭,这时会想到将这些'策略'方法进行统一管理,使用的时候直接new这个管理类,调用对应的方法即可
 * 而为了将各个策略方法统一管理(如增加一些日志的打印等操作),抽象一个上下文类context对其进行统一管理
 */
public class StrategyPattern {

    public static void main(String[] args) {
        Context context = new Context();//新建上下文
        Strategy addStrategy = new AddStrategy();//添加具体策略
        context.setStrategy(addStrategy);//设置具体策略
        context.invoke(1, 2);//运算

        Strategy minusStrategy = new MinusStrategy();
        context.setStrategy(minusStrategy);
        context.invoke(4, 2);
    }

}

//抽象策略是一个接口
interface Strategy {
    //里面的策略交给具体策略实现
    void doStrategy(int a, int b);
}

//具体策略1,实现+
class AddStrategy implements Strategy {

    @Override
    public void doStrategy(int a, int b) {
        System.out.println(a + b);
    }
}

//具体策略2,实现-
class MinusStrategy implements Strategy {

    @Override
    public void doStrategy(int a, int b) {
        System.out.println(a - b);
    }
}

//上下文类,管理策略对象以及一些额外的通用逻辑
class Context {

    private Strategy strategy;
    
    //获取具体策略
    public Strategy getStrategy() {
        return strategy;
    }

    //设置具体策略
    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
    
    //根据具体策略,运行
    void invoke(int a, int b) {
        System.out.println("Context invoke start");
        strategy.doStrategy(a, b);
        System.out.println("Context invoke done");
    }
}

16.4.工厂模式

职责抽象,接口重用

 同样简化一下模板:

/**数字抽象产品*/
public interface Digit {
    public void display();
}
/**黑色数字类,充当具体产品*/
public class BlackDigit implements Digit {
    public void display(){
        System.out.println("显示黑色数字");
    }
}
/**红色数字,充当具体产品*/
public class RedDigit implements Digit {
    public void display(){
        System.out.println("显示红色数字");
    }
}


/**字母抽象产品*/
public interface Letter {
    public void display();
}
/**黑色字母,充当具体产品*/
public class BlackLetter implements Letter {
    public void display(){
        System.out.println("显示黑色字母");
    }
}
/**Summer文本框类,充当具体产品*/
public class RedLetter implements Letter {
    public void display(){
        System.out.println("显示红色子母");
    }
}


/**符号抽象产品*/
public interface Mark {
    public void display();
}
/**黑色符号类,充当具体产品*/
public class BlackMark implements Mark {
    public void display(){
        System.out.println("显示黑色符号");
    }
}
/**红色符号类,充当具体产品*/
public class RedMark implements Mark {
    public void display(){
        System.out.println("显示红色符号");
    }
}


/**字体颜色抽象工厂*/
public interface ColourFactory {
    public Digit createDigit();
    public Letter createLetter();
    public Mark createMark();
}
/**黑色具体工厂*/
public class BlackColourFactory implements ColourFactory {
    public Digit createDigit(){
        return new BlackDigit();
    }
    public Letter createLetter(){
        return new BlackLetter();
    }
    public Mark createMark(){
        return new BlackMark();
    }
}
/**红色具体工厂*/
public class RedColourFactory implements ColourFactory {
    public Digit createDigit(){
        return new RedDigit();
    }
    public Letter createLetter(){
        return new RedLetter();
    }
    public Mark createMark(){
        return new RedMark();
    }
}


/**客户端测试类*/
public class Client{
     public static void main(String args[]){
        //使用抽象层定义
        ColourFactory factory;
        Digit dt;
        Letter lt;
        Mark mk;
        //factory=(SkinFactory)XMLUtil.getBean();
        //为了开闭原则,可以利用反射机制和xml资源获取得到想使用的界面类
        factory = new RedColourFactory();//想更换颜色可以在这里做修改
        dt = factory.createDigit();
        lt = factory.createLetter();
        mk = factory.createMark();
        dt.display();
        lt.display();
        mk.display();
    }
}

 16.5.单件模式

职责抽象,隐藏单件创建的实现。具体工厂的实现只有一个实现。

 16.6.迭代器模式

猜你喜欢

转载自blog.csdn.net/LarsGyonX/article/details/125583473