OO大原则

系统软件的常见问题

  1:僵化:牵一发而动全身,系统不可修改或扩展。

   2:复杂或重复。过分复杂,难于理解。

         3:不可复用,过于僵化而不可服用,不能剥离出独立的服用组件。

   4:不够稳定。常常出错而又无法解决问题,系统运行不够可靠。

设计的原则:

  降低耦合,来实现软件的复用和扩展,这正是设计原则的最终奥义。

       随着面向对象的发展,形成了以封装,继承,多态为主的完整体系。继承了以抽象来封装变化,降低耦合实现复用的精髓。

      而设计模式是对经验的总结与提炼,是对重复发生问题进行的总结和最佳解决策略的探索。

经典的5个设计原则:

  1:单一职责原则

      一个类,应该仅有一个引起它变化的原因。不要将变化原因不同的职责封装在一起,而应该分离。

  2:开放封闭原则

      软件实体,应该对外修改关闭,对外扩展开放。

  3:依赖倒置原则

      依赖于抽象,而不要依赖于具体,因为抽象相对稳定。

  4:Liskov替换原则

      子类必须能够替换其基类

   5:合成/聚合复用原则

      在新对象中聚合已有对象,使之成为新对象的成员,从而通过操作这些对象达到复用的目的,合成方式较继承方式耦合更松散。所以应该少继承,多聚合。

   6:迪米特法则

      又叫最少知识原则,指软件实体应该尽可能少的和其他软件实体发生作用。

单一职责原则的核心思想:

       一个类最好只做一件事,只有一个引起它变化的原因。

        单一职责原则可以看作是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,引起他变化的原因就越多,这将导致职责依赖,相互之间产生影响。从而极大的损伤其内聚性和耦合度。单一职责,通常意味着单一的功能,因此不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。

      因此,SRP原则的核心就是要求对类的改变只能是一个,对于违反这一原则的类应该进行重构。

 在以上设计中,DBManager类将对数据库的操作和用户权限的判别封装在一个类中实现,已添加记录为例。

  

public class DBManager
{
    public void Add()
    {
        if (GetPermission(id) == "CanAdd") {
            Console.WriteLine("管理员可以增加数据。");
        }    
    }
}

这显然是一个充满僵化味道的实现,如果权限设置的规则发生改变,那么必须修改所有的数据库操作逻辑。

重新设计的思路:

  

 以Proxy模式调整之后,有效实现了职责的分离,DBManager类只关注数据操作和逻辑,而不用关系权限判断逻辑。

  

public class DBManager:IDBAction
{
    public void Add()
    {
          
    }
}

而将权限的判断交给DBManagerProxy代理类来完成。

  

public class DBManagerProxy : IDBAction
{
    private IDBACtion dbManager;
    public DBManagerProxy(IDBAction dbAction)
    {
        dbManager = dbAction;   
    }

    //处理权限判断的逻辑
    public string GetPermission(string id)
    {
        //处理权限判断
    }

    public void Add()
    {
        if (GetPermission(id) == "CanAdd") {
                dbManager.Add();    
        }
    }
}

通过代理,将数据操作和权限判断两个职责分离,而实际的数据操作由DBManager来执行,此时客户端的调用就变得非常简单。

public class DBClient
{
    public static void Main()
    {
        IDBAction DBManager = new DBManagerProxy(new DBManager("CanAdd"));
        DBManager.Add();    
    }
}

 开放封闭原则:

      开放封闭原则(OCP)是面向对象原则的核心,软件设计本身的目标就是封装变化,降低耦合。

      核心思想:

          软件实体应该是可拓展,而不可修改的。也就是说,对拓展是开放的,而对修改是封闭的。

      因此开放封闭原则主要体现在2个方面:

          对外拓展开放,意味着有新的需求或变化时,可以对现有代码进行拓展,以适应新的情况。

          对修改封闭,意味着一旦设计完成,就可以完成独立完成其工作,而不要对类进行任何修改。

      需求总是变化。对软件设计者来说,必须在不需要原有系统进行修改的情况下,实现灵活的系统拓展,只有依赖于抽象。实现开放封闭的原则就是对抽象编程,而不是对具体编程,因为抽象相对稳定,而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以对拓展就是开放的,这是实施开放封闭原则的基本思路。

应用反思:

    

思路:银行处理员

  

class BusyBankStaff
{
    private BankProcess bankProc = new BankProcess();

    //定义银行业务员工的业务操作
    public void HandleProcess(Client client)
    {
        switch (client.ClientType) {
            case "存款用户":
                bankProce.Deposit();
                break;
            case "转款用户":
                bankProce.Transfer();
                break;
            case "取款用户":
                bankProce.DrawMoney();
                break;
        }
}

将业务功能抽象为接口,当业务员依赖于固定的抽象时,对于修改就是封闭的,而通过继承和多态机制,从抽象体派生出新的拓展思路,就是对拓展的思路。

 

 

 按照上述设计实现,用细节体现为:

  

interface IBankProcess
    {
        void Process();
    }

//按银行按业务进行分类
    class DespositProcess : IBankProcess
    {
        public void Process()
        {
            //办理存款业务
        }
    }


    class TransferProcess : IBankProcess
    {
       public void Process()
        {
            //办理转账业务
        }
    }

    class DrawMoneyProcess : IBankProcess
    {
        public void Process()
        {
            //办理取款业务
        }
    }

思路的转变,会让复杂的问题变得简单,使系统各负其责,人人实惠。有了上述的重构,银行工作人员变成一个彻底的EasyBankStaff.

  

class EasyBankStaff
    {
        private IBankProcess bankProc = null;

        public void HandleProcesss(ClientCertificateOption client) {
            //业务处理
            bankProc = client.CreateProcess();
            bankProc.Process();
                }
    }

银行业务可以像这样就自动的实现了。

  

  class BankProcess
    {
        public static void Main()
        {
            EasyBankStaff bankStaff = new EasyBankStaff();
            bankStaff.HandleProcesss(new Client("转账用户"));
        }
    }

当有新的业务增加时,银行经理不必为重新组织业务流程而担心,只需为新增的业务实现IBankProcess接口,系统的其他部分丝毫不受影响。

 对应的实现为:

  

  class FundProcess : IBankProcess
    {
        public void Process()
        {
            //办理基金业务
        }
    }

 依赖倒置原则:

    依赖倒置原则核心思想:

        依赖于抽象。

    具体而言:

        高层模块不应该依赖于底层模块,两者都应该依赖于抽象。抽象不应该依赖于具体,具体应该依赖于抽象。

    依赖,一定会存在于类与类、模块与模块之间。面向对象设计在某种层次上,就是一个关于关系处理的哲学,而依赖倒置正是这种哲学思想在具体应用中的体现。当两个模块之间存在紧耦合关系时,最好的方法就是分离接口和实现,使得高层调用接口的方法,底层模块实现接口的定义。

    同时,业务员EasyBankStaff、业务IBankProcess和客户Client之间,明显违背了依赖倒置原则,业务员和业务类依赖于具体的客户,而非抽象。

    

     CreateProcess在创建业务类别时,是必须依托于ClientType为判断条件的,从而也决定了HandleProcess的执行也受制于ClientType的条件时,我们必须从HandleProcess的处理过程了解这一点。

public class Program
{
    public static void Main(string[] args)
    {
        EasyBankStaff bankStaff = new EasyBankStaff();
        bankStaff.HandleProcess(new Client("转账用户"));
    }
}

  bankStaff处理HandleProcess的过程依赖于具体的Client客户,而当有新的业务类别增加时,系统中必须增加对客户类别的依赖,对于完美的设计来说,这种机制是僵化的,应该实现更好的解决方案。

需要找到潜在的对象,使EasyBankStaff依赖于抽象,而抽象的办法就是为EasyBankStaff和Client之间增加一个抽象接口。

 具体的实现为:

  

interface IClient
{
    IBankProcess CreateProcess();
}

class DepositClient : IClient
{
    IBankProcess IClient.CreateProcess()
    {
        return new DepositProcess();
    }
}

class TransferClient : IClient
{
    IBankProcess IClinet.CreateProcess()
    {
        return new TransferProcess();
    }
}

class DrawMoneyClient : IClient
{
    IBankProcess IClinet.CreateProcess()
    {
        return new DrawMoneyProcess();
    }
}

在客户端调用,不需要进行任何类别的判断,可以实现用户自动找到窗口的需求。

public class BankProcess
{
    public static void Main()
    {
        EasyBankStaff bankStaff = new EasyBankStaff();
        bankStaff.HandleProcess(new Client("转账用户"));
    }
}

HandleProcess自行受理其业务,通过依赖于抽象,实现了Client对象的依赖倒置,Client不被依赖,可以实现更多的灵活性。当有新的业务类别增加,只需要实现Client接口。

class FundClient : IClinet
{
    IBankProcess IClient.CreateProcess()
    {
        return new FundProcesss();
    }
}

  抽象的稳定性决定了系统的稳定性,因为抽象是保持不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心思想。

  依赖于抽象是一个通用的规则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一成不变的。

  依赖于抽象,就是要对接口编程,不要对实现编程。

接口隔离原则:

  核心思想:

      使用多个小的专门的接口,而不要使用一个大的总接口。  

  具体实现:

      接口应该是内聚的,应该避免出现"胖"接口。

      一个类对另一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的接口,这是一种接口的污染。

  接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离原则强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口所有的方法、属性等。在设计上,这是一种浪费,而且在实施上会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,这是一种灾难。

  将胖接口分解为多个特定的定制化方法,使得客户端仅仅依赖于他们实际调用的方法,从而解除了客户端不会依赖于他们不用的方法。因此,按照客户需求将客户分组,并依赖这种分组来实现接口,是接口隔离的重要方法。分离的主要手段有两种:

  委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统开销。

  多重继承分离,通过接口多继承来实现客户需求,值得推荐。

应用反思:

  

 

  IComputerUser是一个典型的"胖"接口,对于Aduit来说,他既需要工作,又需要娱乐,对于学生,只需要学习就行了,工作对他来说是浪费。

  重新设计:

    

 

interface IComputerLearn
{
    void ToLearn();
}

interface IComputerWork
{
    void ToWork();
}

interface IComputerBeFun
{
    void ToBeFun();
}

class Aduit
{
    private IComputerWork myWork();
    private IComputerBeFun myFun;

    public void UseComputer
    {
        //主要是工作
        myWork.ToWork();
        //还可以娱乐
        myFun.ToBeFun();
    }
}

class Child
{
    private IComputerLearn myLearn;

    public void UseComputer()
    {
        myLearn.ToLearn();
    }
}

建议:

  将功能接近的接口合并,可能造成接口污染,实现内聚的接口才是接口设计的基本原则。

  接口隔离原则,能够保证系统拓展和修改的影响不会拓展到系统的其他部分,

Liskov替换原则:

  Liskov替换原则是关于继承机制的应用原则,是实现开放封闭原则的具体应用规范。

  只有子类能够替换其基类时,才能保证系统在运行时期内识别子类,这是保证继承复用的基础。

  

原文地址:https://www.cnblogs.com/maxuefeng/p/15672747.html