C#设计模式:六大原则(下)

四、 接口隔离原则(Interface Segregation Principe,ISP)

类的依赖关系应建立在最小接口上,不要都塞在一起。即客户端不应该依赖它不需要的接口。

  根据上面的定义可以看出,对接口的建立要最小化,而不是依赖所有功能都塞在一起的大而全的接口。换种说法就是,方法尽量要细化,要少。当然,也不要拆分成一个一个的,而是要把一些功能紧密绑的方法封装起来,不要暴露太多细节。哇,这是不单一职责吗?不对,它们的审视角度是不同的,一个是接口的依赖,要求接口的方法尽量要少;一个是职责的分离,类和接口的职责要单一。接口隔离原则就是要求只提供尽可能小的接口,需要高内聚,不需要的行为要隐藏起来。当然,拆分接口时,也需要满足单一职责原则。
  来看一个手机的例子吧,手机在例子的世界还是很火爆的,现在是至少人手一部了。看图4.1

图4.1 使用手机的类图

  定义了一个手机的接口,手机可以打电话,发短信,上网,玩游戏等,然后使用了一个场景类People来使用手机。看起来是不是很完美,但仔细想想,ICellphone 这个接口有没有最优设计,是不是功能有点太多了,当然,单从单一职责上考滤是没有问题的,加上接口隔离原则的话,就不同了。时间回到手机刚诞生的年代,那时候的手机是不是只能打个电话,发个短信。手机在迭代的过程中,才出现了各种功能,上网,玩游戏,在线支付等,当今平板的出现,除了上网等高级功能外,基础的打电话,发短信功能反而没有了。这样功能都封装在一起,是不是就过度了,而且都暴露到了使用场景中,不就有问题了吗。那我们根据接口隔离原则重新修改一下类图,如图4.2所示

图4.2 修改后的使用手机的类图

  这样,不管以后使用什么手机,都可保持接口的稳定。需要什么,就继承什么,如果是平板,就不需要电话和短信,保持了接口的最小化。来看一下代码实现:

/// 
/// 基础手机功能
/// 打电话、发短信
/// 
public interface IBaseCellphone
{
    void Call();
    void Text();
}

/// 
/// 上网玩游戏的功能
/// 
public interface IOnlineGameCellphone
{
    void Online();
    void PlayGame();
}

/// 
/// 现代手机,即可打电话,短信,还可以上网玩游戏
/// 
public class Cellphone : IBaseCellphone, IOnlineGameCellphone
{
    public void Call()
    {
        Console.WriteLine("打电话");
    }

    public void Text()
    {
        Console.WriteLine("发短信");
    }

    public void Online()
    {
        Console.WriteLine("手机已连网");
    }

    public void PlayGame()
    {
        Console.WriteLine("开始玩游戏");
    }
}

场景中使用

/// 
/// 现实生活中的场景,使用手机
/// 
public class People
{
    public int Id { get; set; }
    public string Name { get; set; }

    /// 
    /// 一些人只使用手机的基本功能
    /// 
    /// 
    public void UsePhone(IBaseCellphone cellphone)
    {
        Console.WriteLine("我是 {0},我只用基础的功能", this.Name);
        cellphone.Call();
        cellphone.Text();
    }

    /// 
    /// 只想上网玩游戏
    /// 
    /// 
    public void PlayOnlineGame(IOnlineGameCellphone cellphone)
    {
        Console.WriteLine("我是 {0},我只想上网玩游戏", this.Name);
        cellphone.Online();
        cellphone.PlayGame();
    }
}

五、迪米特法则(Law of Demeter,LOD)

一个对象应尽可能少的了解其它对象

  迪米特法则也称最少知识原则(Least Knowledge Principle,LKP),名字虽不同,规则确是同样的。说的都是类与类之间的关系,一个类在使用其它类时,不需要知道其细节,知道的越少越好,你内部如何复杂多变,都是你自己的事情,我不关心,我只关心调用方法,取得结果,其它的一概不关心。迪米特法则的指导思想就是使类与类之间保持松耦合的关系。主要包括如下:

  1. 不跟非直接的朋友说话
  2. 只是内部服务的属性,要封闭到内部
  3. C#中[Serializable]特性,尽量少用,牵涉到序列化的问题

还是用实例来说话吧,有一个关于学校的日常故事,老师让班长多关注下同学们的学习情况,有没有好好听课,认真写作业。让我们来用程序实现一下,先看类图,如图5.1所示:


图5.1 老师让班长检查学生学习情况

  分别定义了老师,班长,学生三个角色,Teacher通过Command方法让Monitor检查学生的学习情况,Monitor通过CheckLearn方法检查学生,来看下代码实现:

/// 
/// 学生
/// 
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
}

/// 
/// 班长
/// 
public class Monitor
{
    public void CheckLearn(List students)
    {
        students.ForEach(item =>
        {
            //检查有没有好好学习
            var learn = new Random(Guid.NewGuid().GetHashCode()).Next(0, 2) == 1 ? "有" : "没有";
            Console.WriteLine($"{item.Name},{learn}好好学习");
        });
    }
}

/// 
/// 老师
/// 
public class Teacher
{
    public void Command(Monitor monitor)
    {
        //初始化学生
        var list = new List
        {
            new Student {Id = 1, Name = "张三"},
            new Student {Id = 2, Name = "李四"},
            new Student {Id = 3, Name = "王五"},
            new Student {Id = 4, Name = "赵六"},
            new Student {Id = 5, Name = "陈七"}
        };

        //班长检查学习情况
        monitor.CheckLearn(list);
    }
}

场景中调用:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            var teacher = new Teacher();
            teacher.Command(new Monitor());
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

运行结果:
张三,没有好好学习
李四,有好好学习
王五,有好好学习
赵六,有好好学习
陈七,没有好好学习

   整个过程都实现了,貌似没有什么问题,但回过头来想想,老师有几个直接的朋友类,这里面其实就只有一个,那就是班长,直接告诉的班长。那老师也依赖了学生啊,不也是朋友类吗?迪米特法则告诉我们,像这种方法体内的类不属于朋友类,一个类只和朋友说话,班长才直接和学生有交流。Command中出现List列表,会影响程序的稳定性。同时,我们看班长检查学习情况,只需要起检查作用,学生学习是自己的事情,比如他要认真听课,好好写作业。有没有好好学习也要有个标准,不能主观来判断,上面实例中处理的也比较简单,我们来深入下,当然,也不是很深入,再加上上面朋友类的问题,来重构一下程序,修改一下类图,如图5.2所示:


图5.2 修改后的检查学习情况类图

断开了非直接的朋友,学生的学习情况也内聚到自身,具体看一下代码实现:

/// 
/// 学生
/// 
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    private readonly Random _random = new Random(Guid.NewGuid().GetHashCode());

    private int _score;  //只是内部服务的属性,要封闭到内部
    private const int GoodScore = 180;

    public void Learn()
    {
        this.Lesson();
        this.Homework();
        var learnString = this._score > GoodScore ? "有" : "没有";
        Console.WriteLine($"{this.Name},{learnString}好好学习");
    }

    /// 
    /// 听课
    /// 内部方法,尽量减少公开的方法和属性
    /// 
    private void Lesson()
    {
        this._score += _random.Next(150);
    }

    /// 
    /// 写作业
    /// 
    private void Homework()
    {
        this._score += _random.Next(150);
    }
}

/// 
/// 班长
/// 
public class Monitor
{
    public List StudentList { get; set; }

    public void CheckLearn()
    {
        StudentList.ForEach(item =>
        {
            //只关心调用方法,不需要知道细节
            item.Learn();
        });
    }
}

/// 
/// 老师
/// 
public class Teacher
{
    public void Command(Monitor monitor)
    {
        //断开了对Student的依赖,减少依赖,不跟非直接的朋友说话
        //班长检查学习情况
        monitor.CheckLearn();
    }
}

场景中调用:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            //初始化学生
            var studentList = new List
            {
                new Student {Id = 1, Name = "张三"},
                new Student {Id = 2, Name = "李四"},
                new Student {Id = 3, Name = "王五"},
                new Student {Id = 4, Name = "赵六"},
                new Student {Id = 5, Name = "陈七"}
            };

            var teacher = new Teacher();
            teacher.Command(new Monitor {StudentList = studentList});
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

六、开闭原则(Open Closed Principle,OCP)

一个软件实体应当对扩展开放,对修改封闭

   一个软件或系统在开发的过程中,以及上线生产后,随着时间的推移,都会产生变化,这是铁定的事实。那我们在设计时就要尽量适应这些变化,来提高系统的稳定性和灵活性,开闭原则就是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。开闭原则指导我们如何建立一个稳定的、灵活的系统
   开闭原则是最基础的原则,也是最重要的面向对象设计原则,在面向对象的开发时,都会提到的原则。开闭原则就是一个目标,其它五大原则都是实现手段,它就像一个口号,你们都要奔着这个口号来。要满足开闭原则,就需要对系统进行抽象化设计,抽象化是开闭原则的关键,换句说法就是对系统进行抽象约束。
   每个人心中,都有一个武侠梦,梦想到有朝一日,能成为一个侠客。如今,各种武侠类游戏也层出不穷,好多人都喜欢玩,毕竟有个圆梦的地方了。游戏中,有各种门派,如少林,武当,玩家选择各自喜欢的门派,玩的美滋滋的。我们来实现下这个过程,类图如6.1所示

图6.1 玩武侠游戏的类图

   上图中,Player可以玩游戏了,武当、少林都能玩,可是游戏是会迭代的,有一天上了一个新版本,加入了一个新的门派,不但要修改Player类,还要更改场景中的处理,违反了开闭原则。重构一下,如类图6.2所示

图6.2 重构后的玩武侠游戏的类图

   增加了一个门派接口,各门派需要实现接口。Player只依赖接口,通过场景类来确定玩家玩什么门派,来看下代码实现:

public interface IFaction
{
    void Characteristic();
}

public class WuDang : IFaction
{
    public void Characteristic()
    {
        Console.WriteLine("武当派是攻击系的,内功远程群攻,很6。");
    }
}

public class ShaoLin : IFaction
{
    public void Characteristic()
    {
        Console.WriteLine("少林派是防御系的,内功防御,你们都打不疼我。");
    }
}

public class Player
{
    public void PlayFaction(IFaction faction)
    {
        Console.WriteLine($"玩家开始玩游戏,选择的是{faction.GetType().Name}");
        faction.Characteristic();
    }
}

场景中调用:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            var player = new Player();

            //玩家玩武当
            player.PlayFaction(new WuDang());

            //玩家玩少森
            player.PlayFaction(new ShaoLin());
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

很简单,运行结果就不展示了。玩什么,就调用什么,如果新增一个门派峨眉,代码如下所示:

public class EMei : IFaction
{
    public void Characteristic()
    {
        Console.WriteLine("峨眉派是治愈系的,医术很高,有起死回生之力。");
    }
}

场景中调用:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            var player = new Player();

            //新门派上线了,我要试试
            player.PlayFaction(new EMei());
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

   看到了吧,是不是很容易,加一个门派,只在场景中很少的改动,就新增了一个门派,都是通过扩展来实现的,原有的类都未进行改动过。这就是开闭原则,对扩展开放,对修改封闭。

   至此,六大原则就说完了,写出来还真是挺不容易的,比想象中的要困难的多,如有什么错误或问题讨论,多多指正和交流。后面的23种设计模式才是真枪实弹,坚持!

你可能感兴趣的:(C#设计模式:六大原则(下))