无尽加班何时休–状态模式

继上一篇“就不能不换DB吗?——抽象工厂模式”后,本文继续讲解《大话设计模式》第16章“无尽加班何时休-状态模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文正式开始


1 加班,又是加班

时间:4月19日 23点 地点:小菜大鸟住所的客厅 人物:小菜、大鸟

“小菜,你们的加班没完没了了?”大鸟为晚上十点才到家的小菜打开了房门。

“嗨,没办法,公司的项目很急,所以要求要加班。”

“有这么急吗?这星期四天来你都在加班,有加班费吗?难道周末也要继续?”

“哪来什么加班费,周末估计是逃不了了。”小菜显然很疲惫,“经理把每个人每天的工作都排得满满的,说做完就可以回家,但是没有任何一个人可以在下班前完成的,基本都得加班,这就等于是自愿加班。我走时还有哥们在加班呢。”

“再急也不能这样呀,长时间加班,又没有加班费,士气更加低落,效率大打折扣。”

“可不是咋地!上午刚上班的时候,效率很高,可以写不少代码,到了中午,午饭一吃完,就犯困,可能是最近太累了,但还不敢休息,因为没有人趴着睡觉的,都说项目急,要抓紧。所以我就这么迷迷糊糊的,到了下午三点多才略微精神点,本想着今天任务还算可以,希望能早点完成,争取不要再加班了。哪知快下班时才发现有一个功能是我理解有误,其实比想象的要复杂得多。嗨!苦呀,又多花了三个多钟头,九点多才从公司出来。”

“哈,那你自己也有问题,对工作量的判断有偏差。在公司还可以通过加班来补偿,要是在高考考场上,哪可能加时间,做不完直接就是玩完。”

“你说这老板对加班是如何想的呢?难道真的认为加班可以解决问题﹖我感觉这样赶进度,对代码质量没任何好处。”

“老板的想法当然是和员工不一样了。员工加班,实际上分为几种,第一,极有可能是员工为了下班能多上会网,聊聊天,打打游戏,或者是为了学习点新东西,所以这其实根本就不能算是加班,只能算下班时坐在办公座位上,第二种,可能这个员工能力相对差,技术或业务能力不过关,或者动作慢,效率低,那当然应该要加班,而且老板也不会打算给这种菜鸟补偿。”

“大鸟,讽刺我呀。”小菜有些不满。

“我又没说是指你,除非你真的觉得自己能力差、效率低,是菜鸟。”

“不过也不得不承认,我现在经验不足确实在效率上是会受些影响的,公司里的一些骨灰级程序员,也不觉得水平特别厉害,但是总是能在下班前后就完成当天任务,而且错误很少。”

“慢慢来吧,编程水平也不是几天就可以升上去的。虽然今天你很累了,但是通过加班这件事,你也可以学到设计模式。”

“哦,听到设计模式,我就不感觉累了。来,说说看。”

“你刚才曾讲到,上午状态好,中午想睡觉,下午渐恢复,加班苦煎熬。其实是一种状态的变化,

不同的时间,会有不同的状态。你现在用代码来实现一下。”

“其实就是根据时间的不同,做出判断来实现,是吧?这不是大问题。”

2 工作状态-函数版

半小时后,小菜的第一版程序。

static int Hour = 0;//钟点
static bool WorkFinished = false;//任务完成标记//写程序方法
/// <summary>
/// 定义一个“写程序”的函数,用来根据时间的不同体现不同的工作状态
/// </summary>
public static void WriteProgram()
{
  if (Hour < 12)
  {
    Console.WriteLine("当前时间:{0}点上午工作,精神百倍", Hour);
  }
  else if (Hour < 13)
  {
    Console.WriteLine("当前时间:{0}点饿了,午饭;犯困,午休。", Hour);
  }
  else if (Hour < 17)
  {
    Console.WriteLine("当前时间:{0}点下午状态还不错,继续努力", Hour);
  }
  else
  {
    if (WorkFinished)
    {
      Console.WriteLine("当前时间:{0}点下班回家了", Hour);
    }
    else
    {
      if (Hour < 21)
      {
        Console.WriteLine("当前时间:{0}点加班哦,疲累之极", Hour);
      }
      else
      {
        Console.WriteLine("当前时间:{0}点不行了,睡着了。", Hour);
      }
    }
  }
}

主程序如下:

static void Main(string[] args)
{
  Hour = 9;
  WriteProgram();
  Hour = 10;
  WriteProgram();
  Hour = 12;
  WriteProgram();
  Hour = 13;
  WriteProgram();
  Hour = 14;
  WriteProgram();
  Hour = 17;
  // 任务完成,则可以下班,否则就得加班了
  WorkFinished = true;
  //workFinished = false;
  WriteProgram();
  Hour = 19;
  WriteProgram();
  Hour = 22;
  WriteProgram();
  Console.Read();
}

“小菜,都学了这么长时间的面向对象开发,你怎么还在写面向过程的代码呀?”

“啊,我习惯性思维了,你意思是说要分一个类出来。”

“这是起码的面向对象思维呀,至少应该有个“工作”类,你的‘写程序’方法是类方法,而‘钟点’、‘任务完成’其实就是类的什么?”

“应该是对外属性,是吧?”

“问什么问,还不快去重写。”大鸟不答反而催促道。

3 工作状态-分类版

十分钟后小菜写出了第二版程序。

工作类

class Work
{
  public int Hour { get; set; }
  public bool TaskFinished { get; set; }
  public void WriteProgram()
  {
    if (Hour < 12)
    {
      Console.WriteLine("当前时间:{0}点上午工作,精神百倍", Hour);
    }
    else if (Hour < 13)
    {
      Console.WriteLine("当前时间:{0}点饿了,午饭;犯困,午休。", Hour);
    }
    else if (Hour < 17)
    {
      Console.WriteLine("当前时间:{0}点下午状态还不错,继续努力", Hour);
    }
    else
    {
      if (TaskFinished)
      {
        Console.WriteLine("当前时间:{0}点下班回家了", Hour);
      }
      else
      {
        if (Hour < 21)
        {
          Console.WriteLine("当前时间:{0}点加班哦,疲累之极", Hour);
        }
        else
        {
          Console.WriteLine("当前时间:{0}点不行了,睡着了。", Hour);
        }
      }
    }
  }
}

客户端程序如下:

static void Main(string[] args)
{
  //紧急项目
  Work emergencyProjects = new Work();
  emergencyProjects.Hour = 9;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 10;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 12;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 13;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 14;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 17;
  emergencyProjects.WriteProgram();
  // emergencyProjects.TaskFinished = true;
  emergencyProjects.TaskFinished = false;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 19;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 22;
  emergencyProjects.WriteProgram();
  Console.Read();
}

结果表现如下:

当前时间:9点上午工作,精神百倍
当前时间:10点上午工作,精神百倍
当前时间:12点饿了,午饭;犯困,午休。
当前时间:13点下午状态还不错,继续努力
当前时间:14点下午状态还不错,继续努力
当前时间:17点加班哦,疲累之极
当前时间:17点加班哦,疲累之极
当前时间:19点加班哦,疲累之极
当前时间:22点不行了,睡着了。

4 方法过长是坏味道

“若是‘任务完成’,则17点、19点、22点都是‘下班回家了’的状态了。”

“好,现在我来问你,这样的代码有什么问题?”大鸟问道。

“我觉得没什么问题呀,不然我早改了。”

“仔细看看,MartinFowler曾在《重构》中写过一个很重要的代码坏味道,叫做‘Long Method’,方法如果过长其实极有可能是有坏味道了。”

“你的意思是‘Work(工作)’类的‘WriteProgram(写程序)’方法过长了?不过这里面太多的判断,好像是不太好。但我也想不出来有什么办法解决它。”

“你要知道,你这个方法很长,而且有很多的判断分支,这也就意味着它的责任过大了。无论是任何状态,都需要通过它来改变,这实际上是很糟糕的。”

“哦,对的,面向对象设计其实就是希望做到代码的责任分解。这个类违背了‘单一职责原则’。但如何做呢?”

“说得不错,由于‘WriteProgram(写程序)’的方法里有这么多判断,使得任何需求的改动或增加,都需要去更改这个方法了,比如,你们老板也感觉加班有些过分,对于公司的办公室管理以及员工的安全都不利,于是发了一通知,不管任务再多,员工必须在20点之前离开公司。这样的需求很合常理,所以要满足需求你就得更改这个方法,但真正要更改的地方只涉及到17点到22点之间的状态,但目前的代码却是对整个方法做改动的,维护出错的风险很大。”

“你解释了这么多,我的理解其实就是这样写方法违背了‘开放-封闭原则’。”

“哈,小菜总结得好,对这几个原则理解得很透嘛。那么我们应该如何做?”

“把这些分支想办法变成一个又一个的类,增加时不会影响其他类。然后状态的变化在各自的类中完成。”小菜说道,“理论讲讲很容易,但实际如何做,我想不出来。”

“当然,这需要丰富的经验积累,但实际上你是用不着再去重复发明‘轮子’了,因为GoF已经为我们针对这类问题提供了解决方案,那就是‘状态模式’。”

5 状态模式

状态模式(State),当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。[DP]

“状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。当然,如果这个状态判断很简单,那就没必要用‘状态模式’了。”

无尽加班何时休--状态模式
状态模式结构图

State类,抽象状态类,定义一个接口以封装与Context 的一个特定状态相关的行为。

abstract class State
{
  public abstract void Handle(Context context);
}

ConcreteState类,具体状态,每一个子类实现一个与Context的一个状态相关的行为。

class ConcreteStateA : State
{
  /// <summary>
  /// 设置ConcreteStateA的下一状态是 ConcreteStateB
  /// </summary>
  /// <param name="context"></param>
  public override void Handle(Context context)
  {
    context.State = new ConcreteStateB();
  }
}
class ConcreteStateB : State
{
  /// <summary>
  /// 设置ConcreteStateB的下一状态是 ConcreteStateA
  /// </summary>
  /// <param name="context"></param>
  public override void Handle(Context context)
  {
    context.State = new ConcreteStateA();
  }
}

Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态。

class Context
{
  private State state;
  public State State
  {
    get { return state; }
    set
    {
      this.state = value;
      Console.WriteLine($"当前状态:{state.GetType().Name}");
    }
  }
  /// <summary>
  /// 定义Context的初始状态
  /// </summary>
  /// <param name="state"></param>
  public Context(State state)
  {
    this.State = state;
  }
  /// <summary>
  /// 对请求做处理,并设置下一状态
  /// </summary>
  public void Request()
  {
    State.Handle(this);
  }
}

客户端代码:

static void Main(string[] args)
{
  // 设置Context的初始状态为ConcreteStateA
  Context c = new Context(new ConcreteStateA());
  // 不断的请求,同时更改状态
  c.Request();
  c.Request();
  c.Request();
  c.Request();
  Console.Read();
}

6 状态模式好处与用处

“状态模式的好处是将与特定状态相关的行为局部化,并且将不同状态的行为分割开来[DP]。”

“是不是就是将特定的状态相关的行为都放入一个对象中,由于所有与状态相关的代码都存在于某个ConcreteState中,所以通过定义新的子类可以很容易地增加新的状态和转换[DP]。”

“说白了,这样做的目的就是为了消除庞大的条件分支语句,大的分支判断会使得它们难以修改和扩展,就像我们最早说的刻版印刷一样,任何改动和变化都是致命的。状态模式通过把各种状态转移逻辑分布到State 的子类之间,来减少相互间的依赖,好比把整个版面改成了一个又一个的活字,此时就容易维护和扩展了。”

“什么时候应该考虑使用状态模式呢?”

当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。 另外如果业务需求某项业务有多个状态,通常都是一些枚举常量,状态的变化都是依靠大量的多分支判断语句来实现,此时应该考虑将每一种业务状态定义为一个State的子类。这样这些对象就可以不依赖于其他对象而独立变化了,某一天客户需要更改需求,增加或减少业务状态或改变状态流程,对你来说都是不困难的事。”

“哦,明白了,这种需求还是非常常见的。”

“现在再回过头来看你的代码,那个‘Long Method’你现在会改了吗?”“哦,学了状态模式,有点感觉了,我试试看。”

7 工作状态-状态模式版

半小时后小菜写出了第三版程序。

无尽加班何时休--状态模式
代码结构图

抽象状态类,定义一个抽象方法“写程序”

/// <summary>
/// 抽象状态类
/// </summary>
public abstract class State
{
  public abstract void WriteProgram(Work w);
}

上午和中午工作状态类

/// <summary>
/// 上午工作状态类
/// </summary>
public class ForenoonState : State
{
  public override void WriteProgram(Work w)
  {
    if (w.Hour < 12)
    {
      Console.WriteLine($"当前时间:{w.Hour}点上午工作,精神百倍");
    }
    else
    {
      /// 超过12点,则转入中午工作状态
      w.SetState(new NoonState());
      w.WriteProgram();
    }
  }
}
/// <summary>
/// 中午工作状态类
/// </summary>
public class NoonState : State
{
  public override void WriteProgram(Work w)
  {
    if (w.Hour < 13)
    {
      Console.WriteLine($"当前时间:{w.Hour}点,饿了,午饭:犯困,午休。");
    }
    else
    {
      /// 超过13点,则转入下午工作状态
      w.SetState(new AfternoonState());
      w.WriteProgram();
    }
  }
}

下午和傍晚工作状态类

/// <summary>
/// 下午工作状态类
/// </summary>
public class AfternoonState : State
{
  public override void WriteProgram(Work w)
  {
    if (w.Hour < 17)
    {
      Console.WriteLine($"当前时间:{w.Hour}点,下午状态还不错,继续努力。");
    }
    else
    {
      /// 超过17点,则转入傍晚工作状态
      w.SetState(new EveningState());
      w.WriteProgram();
    }
  }
}
/// <summary>
/// 晚间工作状态
/// </summary>
public class EveningState : State
{
  public override void WriteProgram(Work w)
  {
    if (w.TaskFinished)
    {
      // 如果完成任务,则转入下班状态
      w.SetState(new RestState());
      w.WriteProgram();
    }
    else
    {
      if (w.Hour < 21)
      {
        Console.WriteLine($"当前时间:{w.Hour}点,加班哦,疲累之极");
      }
      else
      {
        //超过21点,则转入睡眠工作状态
        w.SetState(new SleepingState());
        w.WriteProgram();
      }
    }
  }
}

睡眠状态和下班休息状态类

/// <summary>
/// 睡眠状态
/// </summary>
public class SleepingState : State
{
  public override void WriteProgram(Work w)
  {
    Console.WriteLine($"当前时间:{w.Hour}点不行了,睡着了。");
  }
}
/// <summary>
/// 下班休息
/// </summary>
class RestState : State
{
  public override void WriteProgram(Work w)
  {
    Console.WriteLine($"当前时间:{w.Hour}点下班回家了");
  }
}

工作类,此时没有了过长的分支判断语句

public class Work
{
  public State current { get; set; }
  /// <summary>
  /// 工作初始化为上午工作状态,即上午9点开始上班
  /// </summary>
  public Work()
  {
    current = new ForenoonState();
  }
  public int Hour { get; set; }
  public bool TaskFinished { get; set; }
  public void SetState(State state)
  {
    this.current = state;
  }
  public void WriteProgram()
  {
    current.WriteProgram(this);
  }
}

客户端代码,没有任何改动。但我们的程序却更加灵活易变了。

static void Main(string[] args)
{
  //紧急项目
  Work emergencyProjects = new Work();
  emergencyProjects.Hour = 9;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 10;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 12;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 13;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 14;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 17;
  emergencyProjects.WriteProgram();
  // emergencyProjects.TaskFinished = true;
  emergencyProjects.TaskFinished = false;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 19;
  emergencyProjects.WriteProgram();
  emergencyProjects.Hour = 22;
  emergencyProjects.WriteProgram();
  Console.Read();
}

结果表现如下:

当前时间:9点上午工作,精神百倍
当前时间:10点上午工作,精神百倍
当前时间:12点,饿了,午饭:犯困,午休。
当前时间:13点,下午状态还不错,继续努力。
当前时间:14点,下午状态还不错,继续努力。
当前时间:17点,加班哦,疲累之极
当前时间:17点,加班哦,疲累之极
当前时间:19点,加班哦,疲累之极
当前时间:22点不行了,睡着了。

“此时的代码,如果要完成我所说的‘员工必须在20点之前离开公司’,我们只需要怎么样?“

“增加一个‘强制下班状态’,并改动一下‘傍晚工作状态’类的判断就可以了。而这是不影响其他状态的代码的。这样做的确是非常好。”

“哟,都半夜12点多了,快点睡觉吧。”大鸟提醒道。

“学会了状态模式,我的状态好着呢,让我再体会体会状态模式的美妙。”

“行了吧你,估计明上午的工作状态,就是睡觉打呼噜了。”

“嗨,这也是公司造成的呀。明天估计还得加班,无尽加班何时休,无尽加班何时休!”


每天学一点,不贪多。

下一篇我们接着读“第17章 在NBA我需要翻译——适配器模式 ”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:https://mp.weixin.qq.com/s/_u1rsH5gddb1gKkOUK1QwQ

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论