BACKEND 十月 19, 2019

真正的`服务层`是怎么写的?

文章字数 10k 阅读约需 9 mins. 阅读次数 0

其实很多系统架构里面都有服务层,但是服务对很多开发人员来说都有很多不同的定义和写法。甚至在我待过的公司里都有不同的写法和编写模式。每个人每个团队每个项目都有对服务不同的理解。那到底什么是服务,怎么理解才是对的呢?

你们有没有过无数个夜晚里严重怀疑人生,琢磨着到底哪一种服务才是对的?哪一种才是最好的写法,哪一种才能达到服务的真正意义?因为这种执着,我开始在国外的各种网站,大神们写过的开源大项目里面和文章里面总结出一个大多数研发伙伴们认可的理解方式和编写方式。

要理解什么是服务,我们先来给服务一个定义,在系统架构里面处于什么角色作用是什么。


服务定义

角色:服务是系统架构里面的业务处理层。
作用:主要是为了高度解耦和封装不同场景的业务和功能到对应的服务,然而达到高度中心化的业务代码。

这个定义没毛病吧?赞同的童鞋在评论里举个手哈 👋。
好,有了一个优雅高尚的服务定义,我们来用一个通俗易懂的例子来理解服务。


理解服务

  • 假设是一个控制器,现在拿到了一个衣服对象参数,然后人拥有一个洗衣服方法
  • 现在人需要洗衣服,但是手洗效率太低了,所以我们写了一个多功能的洗衣机服务给到人去使用
  • 洗衣机这个服务里面有很多不同洗衣服的方法,但是其实具体洗衣机里面的每一个清洗方法人是不知道怎么实现的,人都是直接按照提供的功能直接使用。
  • 所以所有服务里面的方法都是解耦在服务里面,服务要提供的方法是可以方便人使用的。

这样说是不是很好理解了?所以最简单的理解就是:

服务是用来封装业务逻辑代码,是一个独立的逻辑层,高度封装解耦后提供给控制器或者其他需要用到这个服务的地方使用的。


编写思路

错误例子

把所有洗衣机的方法提供给人使用,那就等同于让人来决定所有洗衣机的参数和清洗步骤。那人放衣服到洗衣机后,要选择先加水,加多少水,然后清洗开始,清洗多久,再甩干等等。

就想想这个洗衣机就不想用了,洗个衣服那么多选项,还要想那个设置顺序才是对的! 我太难了!洗个鸡腿哦!(ノ`□ ´)ノ⌒┻━┻

⭕️ 正确例子

洗衣机服务实现了很多不同的常用洗衣服的模式, 比如快速清洗,毛衣清洗,地毯清洗,风干,甩干等等。都是一些常用的功能。
每个功能方法里面其实调用了很多洗衣机封装好的流程和方法。这样人使用洗衣机根本不需要知道这些功能是怎么实现的,只要知道自己要干嘛,洗衣机有这个模式,直接用就好了。

(✧ᗜ✧)👍哇! 介么人性化的么!这种洗衣机给我来一打谢谢!
思路我们整理清楚了,那么可以开始看看用这种思维模式写成代码是怎么样的。来上机械键盘,开始快乐滴敲代码了!

服务写法

Controller 控制器

首先我们写一个人控制器PersonController.php,作为一个优秀的人类,我们天生就会洗衣服,但是人嘛天生就是懒惰的。所以我们买了一台洗衣机(实现洗衣机服务)并且我们学会了使用洗衣机来洗衣服。(实现wash方法)٩(◦`꒳´◦)۶

一个人PersonController,有一个洗衣服方法wash,需要洗衣服的时候实例洗衣服务new WashingMachineServer(),然后只要把衣服传入洗衣机服务的快洗方法,洗衣机服务就会开始快速quickWash($cloth)清洗了。

// 人控制器
class PersonController
{
    /**
    * 洗衣服方法
    * 
    * @param object $cloth 衣服对象
    */
    public function wash($cloth)
    {
        $washingMachine = new WashingMachineService();
        $washingMachine->quickWash($cloth); // 调用洗衣机的快速清洗功能
    }
}

我们好奇的童鞋们,肯定会好奇,那这个洗衣机(WashingMachineService.php服务) 到底是怎么实现的呢?它的快洗功能是怎么做的呢?那我们就来自己建一部洗衣机,自然就懂了。

Service 服务

动手之前我们要先思考,先分析,养成这样的好习惯,代码再也不难写了。

分析的重点分为服务的运作流程, 可变动的属性,最后就是有那些可以提供的模式

  • 洗衣机应该怎么运作流程的:
    1. 把衣服放入洗衣机 addCloth()
    2. 注入水到洗衣机里 addWater()
    3. 开始洗衣服(开始旋转和各种累活)wash()
    4. 把水排除洗衣机 flushWater()
    5. 把衣服取出 fetchClouth()
  • 洗衣机可变动的属性
    • 要把衣服放入洗衣机,我们就需要有个东西来装着,然后才能清洗,所以我们应该有一个洗衣桶 $bucket
    • 根据衣服的量,使用的水量是应该可以调节的。(对我们要节约用水嘛)$washDuration
  • 洗衣机最常用的模式
    • 快速洗 quickWash()

⚠️ 需要注意:

  • 所有洗衣机的内部方法都是 private 私有方法,因为都是给洗衣机使用的,外部的人是不能使用的;
  • 快速清洗取衣服这两个方法是 public 共有方法,因为是洗衣机提供出去给人使用的方法;
  • 所有属性都是 protected 保护属性,是洗衣机独有的属性。

现在我们就要使用程序员的魔法,把以上的逻辑和属性转换成代码。(∩◉ω◉)⊃----★

class WashingMachineService
{
    /**
    * 清洗时长 (分钟)
    * @var integer
    */
    protected $washDuration = 60;

    /**
    * 洗衣机的洗衣桶
    * @var array
    */
    protected $bucket;

    /**
    * 改变默认洗衣机的清洗时长
    * @param integer $duration
    */
    public function changeWashDuration($duration)
    {
        $this->washDuration = intval($duration);

        return $this;
    }

    /**
    * 往洗衣机的桶加入水
    */
    private function addWater()
    {
        array_merge($this->bucket, ['water' => 'cold water']);

        return $this;
    }

    /**
    * 把衣服加入洗衣机桶内
    */
    private function addCloth($cloth)
    {
        array_merge($this->bucket, ['cloths' => $cloth]);

        return $this;
    }

    /**
    * 旋转桶把开始洗衣服
    */
    private function wash()
    {
        // 使用洗衣机的清洗时长来全换清洗衣服
        for ($duration = $this->washDuration; $duration > 0; $duration--) {
            array_rand($this->bucket, 3);
        }

        return $this;
    }

    /**
    * 把桶里面的水清除掉
    */
    private function flushWater()
    {
        unset($this->bucket['water']);

        return $this;
    }

    /**
    * 从洗衣桶里面把衣服拿回出来
    */
    private function fetchCloths()
    {
        return $this->bucket['cloths']
    }

    /**
    * 快速清洗衣服方法
    */
    public function quickWash($cloth)
    {
        return $this->changeWashDuration(10) // 重新设置洗衣服的时长
                    ->addCloth($cloth) // 加入衣服
                    ->addWater() // 加入水
                    ->wash() // 开始清洗
                    ->flushWater() // 清除水
                    ->fetchCloths(); // 最后取出衣服返回
    }
}

以上就是一个最基础的服务,有独立的内部方法可以让服务运作起来,也有提供出去的服务模式方法。

⚠️ 需要注意:
服务的重点特性在最后这个 quickWash 快速清洗方法。实现快速清洗是通过使用特定顺序组合方式调用洗衣机内部方法。这种服务的实现方式,可以把一个服务里面的业务逻辑拆分成多个逻辑块,然后通过不同的顺序和组合来实现某种模式或者功能。这样的服务就非常有弹性,而且所有逻辑块复用性极高。这个也是设计模式里面的模版方法模式(Template Method)

上面的例子只是写了一个洗衣机10%不到的功能,一个完整的洗衣机还会有很多的逻辑方法。那问题就来了,方法多了这个服务就会开始臃肿。这个时候我们就要想一套解耦封装服务的方式方法。接下来我们来讲解一下怎么更深度的服务封装。


服务封装

在日常开发过程中,我们有各种各样的封装和解耦方式。包括内部Trait, 内部服务工厂设计模式。这几种都是可以用来深度封装服务的方式方法。找到了方法,下一步就是要找到怎么封装才是最优解耦思路。解耦的原理就是找到共通点公用点。然后把这些方法封装起来,解耦出去。

封装思路

在上面写的洗衣机服务,里面的洗衣桶是很通用的和独立的业务逻辑。所以它是可以解耦封装在一起的。

  • 洗衣机的bucket洗衣桶属性的方法其实可以封装起来。单独做为一个洗衣桶的服务。
  • 所有涉及洗衣桶操作的功能和流程都封装到洗衣桶服务里面给洗衣机调用。

使用上面的逻辑,我们可以把洗衣机服务洗衣桶服务拆分成两块。来吧上机械键盘!


封装编写

  • 洗衣机服务 WashingMachineService.php
class WashingMachineService
{
    /**
    * 清洗时长 (分钟)
    * @var integer
    */
    protected $washDuration = 60;

    /**
    * 改变默认洗衣机的清洗时长
    * @param integer $duration
    */
    public function changeWashDuration($duration)
    {
        $this->washDuration = intval($duration);

        return $this;
    }

    /**
    * 快速清洗衣服方法
    */
    public function quickWash($cloth)
    {
        $washingBucket = new WashingBucketService();

        $this->changeWashDuration(10) // 重新设置洗衣服的时长

        // 调用洗衣机的桶去清洗衣服
        return $washingBucket->addCloth($cloth) // 加入衣服
                    ->addWater() // 加入水
                    ->wash($this->washDuration) // 开始清洗
                    ->flushWater() // 清除水
                    ->fetchCloths(); // 最后取出衣服返回
    }
}
  • 洗衣桶服务 - WashingBucketService.php
class WashingBucketService
{
    /**
    * 洗衣机的洗衣桶
    * @var array
    */
    protected $bucket;

    /**
    * 往洗衣机的桶加入水
    */
    public function addWater()
    {
        array_merge($this->bucket, ['water' => 'cold water']);

        return $this;
    }

    /**
    * 把衣服加入洗衣机桶内
    */
    public function addCloth($cloth)
    {
        array_merge($this->bucket, ['cloths' => $cloth]);

        return $this;
    }

    /**
    * 旋转桶把开始洗衣服
    */
    public function wash($washDuration)
    {
        // 使用洗衣机的清洗时长来全换清洗衣服
        for ($duration = $washDuration; $duration > 0; $duration--) {
            array_rand($this->bucket, 3);
        }

        return $this;
    }

    /**
    * 把桶里面的水清除掉
    */
    public function flushWater()
    {
        unset($this->bucket['water']);

        return $this;
    }

    /**
    * 从洗衣桶里面把衣服拿回出来
    */
    public function fetchCloths()
    {
        return $this->bucket['cloths']
    }
}

提供和调用

模块与模块或者系统与系统直接都会使用到服务来互相打通业务。这个时候服务就要有一个方式提供出去让外部的模块或者系统调用。

⚠️ 需要注意:
这里说的是外部模块或者系统调用,这个是要考虑到如果是微服务的话,每个模块都会在不同的服务器和域名下,这个时候就需要异步调用。这种情况下如果还是用类实例的方式来提供和调用服务后面要改就很麻烦了。

这种情况下目前最优的方式就是服务提供者用Trait给到服务使用者来注入到业务代码里面。

  • 洗衣机服务Trait - WashingMachineProvider.php
trait WashingMachineProvider
{
    /**
    * 提供洗衣机服务类
    */
    public washingMachine()
    {
        return new \WashingMachineService();
    }
}

⚠️ 需要注意:
这里是使用了命名空间来实例洗衣机服务类的。但是如果改成了微服务,那我们只需要改掉所有这些服务提供Trait,把服务类实例改为服务发现,或者异步服务调用就可以了。再也不用花钱去买霸王洗发水了。٩(^ᴗ^)۶


总结

经历了千辛万苦,无数个失眠的夜晚。终于知道服务到底是什么,应该怎么写,怎么写才是对的。写好服务可以提高代码的维护性,编写的代码也会有更强的逻辑和条理。好的服务也会有更好的弹性和扩张性。下面我们来总结一下编写服务的重点。

角色: 服务是系统架构里面的业务处理层。
作用: 主要是为了高度解耦和封装不同场景的业务和功能到对应的服务,然而达到高度中心化的业务代码。
思路: 逻辑要独立,分解成逻辑块,保持复用性高,尽量不要限定逻辑使用的顺序和高弹性的组合性。
编写: 高度封装,高内聚的原理来编写服务,细化分解通用性,公用性的业务,然后封装成一个服务。


#通过技术悟出人生道理# 💭
“大千世界每一件事都有千百万种做法,
吸收,打磨,专研,总结,进步,
才会找到最适合的做法。” ~ 三·钻 TriDiamond

0%