软件设计与重构的六个原则

软件设计与重构的六个原则

再读《重构》和《架构整洁之道》

目录
开放封闭原则 --- OCP
依赖倒置原则 --- DIP
单一职责原则 --- SRP
Liskov替换原则 --- LSP
接口隔离原则 -- ISP
迪米特法则(最小知识原则) -- LOD

一、开放封闭原则 --- OCP

软件中的基础结构(函数、类或模块)对于功能扩展是开放的,但是对于修改是封闭的。

可实施的具体行为

面向接口编程,不要面向实现编程
依赖倒置原则
Liskov替换原则

eg.

//Method One
int DoSomeFunction(code,  param1, param2)
{
    int rtnCode = ...
    switch(code)
    {
    case CODE_A:
    {
        code block A
        break;
    }
    case CODE_B:
    {
         code block B
        break;
    }
    ....
    default:
        rtnCode = not support error code

    return rtnCode;
}

//Method Two
int ProcessForCodeA(param1, param2)
{
    code block A
}

int ProcessForCodeB(param1, param2)
{
    code block B
}

struct 
{
    int code;
    int (*Processor)(param1, param2);
}ITEM;

//Table-Driven Methods
static ITEM processItems[] = 
{
    {CODE_A, ProcessForCodeA},
    {CODE_B, ProcessForCodeB},
     ......
};

int DoSomeFunction(code,  param1, param2)
{
    for_each(item in processItems)
    {
        if(item.code == code)
        {
             return item.Processor(param1, param2);
        }
    }

    return not supoort error code;
}

 

 

软件设计与重构的六个原则_第1张图片

 

#define  MAX_PROCESS_ITEMS    32

ITEM processItems[MAX_PROCESS_ITEMS] = { 0 };

int DoSomeFunction(code,  param1, param2)
{
    ......
}

bool RegisterProcessor(int code, Processor func)
{
    add {code, func} to processItems[]
}

void DeregisterProcessor(int code)
{
    remove code process item from processItems[]
}

 

软件设计与重构的六个原则_第2张图片

 

可实施的设计模式:

Strategy 策略模式
Template Method 模板方法模式
Visitor 访问者模式

开放封闭原则 --- Strategy 模式

意图:定义一些列算法,把它们一个个封装起来,并且使它们可以互相替换。本模式使得算法可独立于使用它的客户而变化

软件设计与重构的六个原则_第3张图片

适用性:

  • 许多相关的类仅仅是行为有差异,这个模式提供一种用多个行为中的一个来配置一个类的方法
  • 需要使用一个算法的不同变体
  • 算法使用客户不应该知道的数据,使用策略模式可以避免暴露复杂的、与算法相关的数据结构
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。

开放封闭原则 --- Template Method 模式

意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。本模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

软件设计与重构的六个原则_第4张图片

适用性:

  • 一次性定义一个算法的不变部分,并将可变的行为留给子类来实现
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复 控制子类扩展。
  • 模板方法只在特定的点调用“Hook”操作,遮掩个旧只允许在这些点进行扩展。

开放封闭原则 --- Visitor 模式

意图:表示一个作用于某对象结构中的各个元素的操作。本模式使得你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

软件设计与重构的六个原则_第5张图片

适用性:

  • 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖与其具体类的操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。
  • 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作比较好。
class Client
{
    Element *element;

    void SomeOperation()
    {
        生成一个ContreteVisitor1的实例 v
        element->Accept(&v);
        v.ShowSomething(); //用ContreteVisitor1定义的方式输出结果
    }
    void AnotherOperation()
    {
        生成一个ContreteVisitor2的实例 v
        element->Accept(&v);
        v.ShowSomething(); //用ContreteVisitor2定义的方式输出结果
    }
};

 

二、依赖倒置原则 --- DIP

传统的层次化设计模型,上层和下层业务分离,上层依赖下层提供的功能,下层不能反向依赖上层。
依赖倒置不是简单的依赖方向翻转,它的核心仍然是抽象接口。
1、高层模块不应该依赖于底层模块(二者都应该依赖于抽象)
2、抽象不应该依赖于实现,实现应该依赖于抽象

软件设计与重构的六个原则_第6张图片    软件设计与重构的六个原则_第7张图片

可实施的具体行为:

关键是抽象,面向接口编程
Liskov替换原则

依赖倒置原则 --- ATM提款机

取款业务逻辑流程是稳定的
提款机的实现是变化的

软件设计与重构的六个原则_第8张图片  软件设计与重构的六个原则_第9张图片  软件设计与重构的六个原则_第10张图片

A:  bool  Withdraw(账户验证参数,金额)
B: int GeiQian(其他参数,账户验证参数,金额)

将取款业务流程中对提款机的操作接口抽象出来,定义一组提款的逻辑接口。
抽象接口的提出,解除了取款业务和提款机的耦合关系,在满足Liskov替换原则的基础上,替换不同厂家的提款机变的非常容易。

class Teller
{
public:
   ...
    bool IsConnected(环境参数) = 0;
    bool GetBalance(位置信息, 余额信息) = 0;
    bool Pay(上下文信息,支付金额) = 0;
    ...
};

//具体不同厂家的提款机,就是这个抽象接口的实现者
class IntelTeller : public Teller
{
    IntelTeller(TellerMgmt& tm)
    {    tm.Register(this); }
    ...
    bool IsConnected(环境参数)
    {    Intel 的实现   }
    bool GetBalance(位置信息, 余额信息)
    { Intel 的实现    }
    bool Pay(上下文信息,支付金额)
    { Intel 的实现    }
    ...
};
class MoftTeller : public Teller
{
    ...
    bool IsConnected(环境参数)
    { Moft 的实现 }
    bool GetBalance(位置信息, 余额信息)
    { Moft 的实现 }
    bool Pay(上下文信息,支付金额)
    { Moft 的实现 }
    ...
};

//提供注册接口,由能提供者注册自己提供的功能
class TellerMgmt
{
    void Register(Teller *teller) 
    { ...... }
    Teller *GetTeller(...) 
    { ...... }
protected:
    //管理注册的Teller们;
}
TellerMgmt& GetTellerMgmtObj()
{
    static TellerMgmt tm;

    if(tm 没有初始化)
    {
        对tm初始化,并设置初始化标志
    }

    return tm;  
}

//取款业务模块
Teller *teller = tm.GetTeller();

if(teller->IsConnected(...))
{
    ......
}

依赖倒置原则 --- Singleton单实例模式

意图:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。

适用性:
当类只有一个实例,并且客户可以从一个众所周知的访问点访问它时

重要:
不要把单实例模式当全局变量用

三、单一职责原则 --- SRP

描述:对一个类而言,应该只有一个引起它变化原因。
现在的描述:任何一个软件模块都应该只对某一类行为者负责

  • 降低代码复杂度,一个类只负责一项职责,逻辑也简单
  • 提高代码可读性
  • 当发生变化的时候,能减少变化影响的范围,并且受影响的类的变化情况更好预知

软件设计与重构的六个原则_第11张图片    软件设计与重构的六个原则_第12张图片

软件设计与重构的六个原则_第13张图片   软件设计与重构的六个原则_第14张图片

ConnectionMgmt::AddModem(Connection *modem)
{
    Add modem to modems list
}

ConnectionMgmt::Connect(...user param...)
{
    for_each(modem in modems list)
    {
        modem->Dial(...);
    }
}

ModemImplemention modem1;
ModemImplemention modem2;

......

ConnectionMgmt cm;
cm.AddModem(&modem1);
cm.AddModem(&modem2);
......
cm.Connect(...);

可实施的具体行为:

高内聚原则
分离变化的部分和不变的部分
TDD 测试驱动开发
最小知识原则 LOD

可实施的设计模式:

Facade 外观模式
Proxy 代理模式
Adapter 适配器模式

单一职责原则 --- Facade 模式

意图:为子系统中的一组接口提供一个一致的界面。本模式定义了一个高层接口,这个接口使得这一子系统更加容易使用

软件设计与重构的六个原则_第15张图片

软件设计与重构的六个原则_第16张图片

单一职责原则 --- Proxy 模式

意图:为其他对象提供一种代理,以控制对这个对象的访问。本模式定义了一个代理对象,通过代理对象屏蔽原对象的一些接口

软件设计与重构的六个原则_第17张图片

单一职责原则 --- Adapter 模式

意图:将一个类的接口转换成客户希望的另一个接口。本模式使得原本由于接口不兼容而不能在一起工作的那些类可以在一起工作

软件设计与重构的六个原则_第18张图片

软件设计与重构的六个原则_第19张图片

AdapterA::OperationA(...)
{
    ...
    opl->Function1(...);
}

AdapterB::OperationB(...)
{
    ...
    opl->Function3(...);
}

四、Liskov替换原则 --- LSP

描述:子类型(subtype)必需能够替换掉它们的基类型

OO背后的主要机制是抽象和多态,在C++和Java这样的静态语言中,支持抽象和多态的关键机制之一就是继承。如果继承体系中某个类的实现不满足LSP原则,那么这个体系就会变的很脆弱,失去健壮性。

违反LSP原则,将导致对OCP原则的违反。

class A 
{
}

class B : public A
{
    bool Operation();
};

class C : public A
{
    bool Operation();
};

void TestFunc(const A& ta)
{
    if(ta is a B)
    {
        static_cast(ta).Operation();
    }
    else if(ta is a C)
    {
        static_cast(ta).Operation();
    }
    else if(...)
    {
    ...
    }
    ...
}
//new design
class A 
{
    virtual bool Operation();
};

void TestFunc(const A& ta)
{
    ...
    ta.Operation();
    ...
}
/*
正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。
比如增加一个新类D: 
*/
class D : public A 
{     
    bool Operation(); 
}; 
//使用基类类型的函数TestFunc()不需要做任何修改就可以支持D: 
D d; 
TestFunc(d)

 

类的继承体系设计要遵循 IS-A 原则,并且这个 IS-A 是关于行为的,不是关于数据的。  
IS-A 原则只能作为子类型定义的含义过于宽容,应该将子类型的“可替换性”作为子类型定义的必要条件。

软件设计与重构的六个原则_第20张图片软件设计与重构的六个原则_第21张图片

潜在的违反LSP的情况:
没有 IS-A 关系的继承
派生类中退化了某个函数
还有一些语言层面上的错误,会导致违反LSP原则,比如某个子类的Operation()内部抛出了异常(而不是按照约定返回错误值),这会使得TestFunc的行为发生不可控的变化。

软件设计与重构的六个原则_第22张图片软件设计与重构的六个原则_第23张图片

五、接口隔离原则 --- ISP

描述:不应该强迫客户依赖于它们不用的方法。换句话说,一个类对另一个类的依赖应该是建立在最小的接口范围上的。

强迫客户依赖它们不使用的方法,那么客户就要面临着这些未使用的方法的改变所带来的变更,无形中增加了不必要的耦合关系,潜在地违反SRP原则。

可实施的具体行为:
通过拆分职责分离接口
使用委托分离接口
分离变化的部分和不变的部分
使用多继承分离接口

软件设计与重构的六个原则_第24张图片

软件设计与重构的六个原则_第25张图片

使用委托分离接口

class TimedDoor: public Door
{
    void DoorTimeOut(...)
}

class TimerClient
{
    virtual void TimeOut() = 0;
}

class DoorTimedAdapter: public TimerClient
{
    DoorTimedAdapter(TimedDoor& door) { aTimedDoor = door; }
    virtual void TimeOut() { aTimedDoor.DoorTimeOut(); }
    TimedDoor  aTimedDoor;
}

class Timer
{
    RegisterTimeClient(TimerClient *tc)  
    { 
        //add tc to tcs 列表
    }
    
//事件发生时:
for_each( tc in tcs)
    tc->TimeOut(...);
}



TimedDoor door;

Timer timer;

timer.RegisterTimeClient(new DoorTimedAdapter(door));

door.Open();

class Timer
{
    friend static Timer& GetTimer()
    public:
    RegisterTimeClient(TimerClient *tc)  { add tc to tcs 列表}
    protected:
    Timer() {}
//事件发生时:
for_each( tc in tcs)
    tc->TimeOut(...);
}

Timer& Timer::GetTimer()
{
    static Timer timer;
    if(....)
    {
    }

    return timer;
}


class TimedDoor: public Door
{
    TimedDoor()
    {
        Timer& timer = Timer::GetTimer();
         timer.RegisterTimeClient(new DoorTimedAdapter(*this));
    }
    virtual void DoorTimeOut(...);
}


TimedDoor door;

door.Open();

使用多重继承分离接口

软件设计与重构的六个原则_第26张图片

class TimerClient
{
    virtual void TimeOut() = 0;
}

class TimedDoor: public Door, public TimerClient
{
    TimedDoor()
    {
        Timer& timer = Timer::GetTimer();
         timer.RegisterTimeClient(this);
    }
    virtual void TimeOut()
    {
        do alarm report
    }
    // other interface inherit from Door
}

TimedDoor door;

door.Open();

可实施的设计模式:
Facade 外观模式
Proxy 代理模式

ISP原则和SRP原则
SRP原则强调的是“只有一个原因能造成对象的改变”,这就潜在地对一个类的接口个数和接口内方法的个数提出了要求。一般来说,接口个数越多,接口内的方法个数越多,越容易违反SRP原则。所以具体实施的时候,都要求一个类只实现一个接口。
ISP原则强调的是“隔离”,对一个类实现的接口数量和各个接口内方法的数量都没有要求,只要求这些接口之间相互隔离,并且没有多余的接口。ISP在具体实施的时候,可以使用多继承的方式使用那些相互隔离的接口。
虽然ISP所采用多继承的时候会潜在地造成一个类的接口的增加,但是这两个原则本质上是不矛盾的,因为每一个被分离出来的接口都应该是满足SRP原则的。

六、迪米特法则 --- LOD

描述:迪米特法则(Law of Demeter)又叫作最少知识原则,一个对象应当对其他对象有尽量少的了解。

“不要和陌生人说话”,一个软件实体应该尽量少的与其他软件实体发生相互作用,换句话说,对其他软件实体有尽量少的知识(了解)。
迪米特法则的初衷是降低类之间的耦合,减少对其他类的依赖。

可实施的设计模式:
Facade 外观模式
Mediator 中介者模式

迪米特法则 --- Mediator 模式

意图:用一个中介对象封装一些列的对象交互。本模式使得对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互

软件设计与重构的六个原则_第27张图片

软件设计与重构的六个原则_第28张图片    软件设计与重构的六个原则_第29张图片

三个对象的协作:TextField,Button 和 StaticText

软件设计与重构的六个原则_第30张图片

class WelcomeDialog
{
    TextField aUserNameText;
    Button aNextButton;
    StaticText aStaticWelcome;

    WelcomeDialogMediator mediator;
    ......
};

//WelcomeDialog类的初始化部分:

......
aUserNameText.SetMediator(mediator);
aNextButton.SetMediator(mediator);
aStaticWelcome.SetMediator(mediator);

mediator.btnNext = aNextButton;
mediator.staticWelcome = aStaticWelcome;
    ......
};


void TextField::TextChange(String text)
{
    ......
    mediator.OnTextChange(this->self_ID, text);
    ......
};

void WelcomeDialogMediator::OnTextChange(int id, String text)
{
    //检查 id 是否是 aUserNameText,不同的控件触发的TextChange可以有不同的响应处理
    boolean isEnable = text.IsEmpty() ? false : true;
    ...
    btnNext.SetState(isEnable);
    staticWelcome.SetText(text);
    ...
}

适用性:

  • 一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解。
  • 一个对象引用其他很多对象并且直接与这些对象通信,导致该对象难以复用。
  • 想定制一个分布在多个类中的行为,而又不想生成太多的子类。

你可能感兴趣的:(软件设计与重构的六个原则)