预言需求——用好的设计,应对可能的变化

预言需求——用好的设计,应对可能的变化

楔子

认识了一位艺术家朋友,在他的引导下,开始看一些艺术展,看各种油画,水墨,水彩————屡屡为在那种固化颜料之下的展现的波光粼粼而赞叹不已。

在另一位朋友的安利下,我迷上音乐,沉迷古典。

在流连这些艺术中时,我总在不停的感叹:好的艺术,一定是不朽的,我们可以听20年前的流行,欣赏200年前的古典,欣赏百十年前的毕加索。

每有这些感慨,我总自惭形愧,作为软件工程师,代码总是暂时的,短暂的,甚至从写下的那一刻起,就是过时的。

我们甚至在今天通宵去写一个只在明天运行一次的脚本。

是的,悲观的角度看,我们一直在做无用功

于是,我总会想,如何写出’不朽’的代码,让自己的工作,尽可能的完美,尽可能延长他的运行周期。

对我来讲,这种方式,应该是预见需求,即预期在一个时间内,你接到的需求去如何的变化,当这种变化发生时,你的设计去如何的应对。

是的,想一个预言家一样,去给你的代码预知一段时间内的未来。

好的工程师,都应该是预言家

那么今天这篇,就从一个非常小的需求,聊聊我是怎么去做预言家,给代码卜卦。

扫描二维码关注公众号,回复: 9616829 查看本文章

需求

这是我在之前的东家接到的一个需求,当时我们用php的laravel框架已经实现了后台服务并预期上线。

因为我们本质上是一个理财的网站,刚上线时效果不是很好,于是PM说,我们要给新注册的用户发券,促进他们消费。

是的,我们的需求很简单,在用户注册时,给用户发券

也就是说,需求是这个样子:

在这里插入图片描述

第一次实现

在当时,我们已经有了auth/user/register接口,里面调用了userService这个类的一个newUser的方法,这个方法里实例化了一个userData,它生成了一个userModel,并把它加到数据库中。
大概是这个样子:

userControler/register
|-userService/newUser
|--userData/newUser
|---userModel/create
|----mysql ...

那么直观的解决方式
我们只要有一个coupon的实现(data/model) ,然后在userServicenewUser方法里调用newCoupon就可以了。

于时我们代码这个样子就可以:

userControler/register
|-userService/newUser
|--userData/newUser
|---userModel/create
|----mysql ...
|--couponData/newCoupon
|---couponModel/create
|----mysql ...

可是,这样真的可以了么?
这个需求,会有什么变化?

深挖需求

之前说过,我们的需求是在用户注册时,送消费券
这个需求,有两个关键点,即什么时间————用户注册,做什么事情————送消费券

那么,从时间行为两个维度考虑后继可能发生的变化:

  1. 时间问题
  • 只在用户注册时发券么?
  • 用户登录时会不会发券?
  • 用户购买的时候会发券么?
  • 用户充值的时候会发券么?
  1. 行为问题
  • 我们只会发券么?
  • 我们有没有可能发现金?(理财体验金,你懂的)
  • 我们有没有可能加入邀请返现?

也就是说,我们的需求,有可能变成这个样子:

在这里插入图片描述

在想到这些可能到来的变化后,我不得不重新审视自己的设计……

进一步的考虑

在发现我的需求可能的变化后,我开始考虑,怎么去应对未来的变化,那么我的设计的目的变成了:

  1. 以一种通用的方式实现送券;
  2. 用合适的方式获知用户注册这个时间点;
  3. 尽可能简单的把1和2串联到一起;

救星:观察者

在猜想需求变化时,我意外的发现,laravel 中的model是有观察者的,即你可以定向的观察某个model(mysql)中的操作:
像这样,先定义一个observer :

namespace App\Observers;
use App\User;
class UserObserver
{
    /**
     * Handle the User "created" event.
     * @param  \App\User  $user
     * @return void
     */
    public function created(User $user)
    {
        //
    }
    /**
     * Handle the User "updated" event.
     * @param  \App\User  $user
     * @return void
     */
    public function updated(User $user)
    {
        //
    }

    /**
     * Handle the User "deleted" event.
     * @param  \App\User  $user
     * @return void
     */
    public function deleted(User $user)
    {
        //
    }
}

然后,在serviceProvider里注册这个:

namespace App\Providers;
use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     * @return void
     */
    public function register()
    {
        //
    }
    /**
     * Bootstrap any application services.
     * @return void
     */
    public function boot()
    {
        User::observe(UserObserver::class);
    }
}

这正是标准的观察者模式,并且,重要的一点,我们在php是可以不用类型传参的(或是在java里Object)之类,所以,我们的objectServer 变成了:

class CommmonObserver
{
    
    public function created($item)
    {
        //
    }
    public function updated($item)
    {
        //
    }

    public function deleted($item)
    {
        //
    }
}

实现: 时机的问题

有了laravel底层的observer,那么用观察者模式捕获用户注册这个时间点就方便了:

class ServiceProvider
{
    /**
     * Bootstrap any application services.
     * @return void
     */
    public function boot()
    {
        User::observe(CommonObserver::class);
    }
}

如果我们的CommonObserver只给user这一个model用,那么这部分可以到此为止了,但是我们希望再有类似问题,都可以用类似的方式处理,在这种方式下,变成了:

class CommmonObserver
{
    
    public function created($item)
    {
        raise_event($item,'created');
    }
    public function updated($item)
    {
        raise_event($item,'updated');
    }

    public function deleted($item)
    {
        raise_event($item,'deleted');
    }
}

raise_event中,我们这样处理:

function raise_event($item,$event){
    //getName 
    $model_name = get_class($item);
    //map to event ..
}

这时,我们可以定义一个类似的array:

return [
    "App\User"=>[
        "created"=>"userRegister"
    ],
    "App\UserLoginLog"=>[
        "created"=>"userLogin"
    ],
    // other event base model 
];

这样可以得到这个array后,在raise_event中去将具体的事件生成;
这时,我们的需求,受设计影响成为了这样:
在这里插入图片描述

实现: 行为的问题

我们解决了时机,那么行为就变得容易了。
根根上文,不难看出,我们在实现行为时可以得到三个参数:

  1. event ---- 我们定义的事件,比如userRegister
  2. db_operation ---- db的操作,比如update,create
  3. data_item ---- 对应的数据,比如user的实体。

注意一点,其实在进行后继业务处理时,我们不用去关注db_operation这个参数,因为其实event确定的话,db_operation就确定了。

因为我们不能只关注送券这一件事情,所以,我们要有一个约定(inteface)去承接相应的事件处理;

interface IEventHandler{
    function handle($event,$item);
}

然后可以实现一个AddCouponHandler:

class AddCouponHandler{
    public function handle($event,$item){
        //相关的操作
    }
}

那么,怎么关联到一起? 哈哈,嗯,如你所愿,我们再来个array:

return [
    "userRegister"=>[
        AddCouponHandler::class,
        //other handler
    ],
    "userLogin"=>[
        //other handler
    ],
    //
];

这样,我们的设计最终变成了这个样子:
在这里插入图片描述

理想主义的形式

我们在这个需求的第一版的迭代上,加入了这种设计,然后随着后继的迭代,我们又做了如下的更改:

  1. 对要发送的model进行处理,将db的原始数据进行裁剪,形成了统一的数据约定;
  2. 将event的处理变成了异步。

这时,我们的设计就成为了这种:

在这里插入图片描述

小结

我们将一个看似简单的需求————注册送券,无限的复杂化,去预测这个需求可能发生的迭代,可能产生的变化。
在这里,我们使用观察者模式,实现了对现有代码的扩展,同时,运用了大量的工厂模式(array).
在保证实现基础的基础上,我们做到了:

  1. 对原有代码实现了非侵入的扩展(user相关的内容没有更改)。
  2. 加入的所更改都是以增加为主;
  3. 新的业务逻辑是低耦合甚至完全解耦的。
  4. 为以后可能的变化留出了充足的时间。
发布了64 篇原创文章 · 获赞 9 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/geyunfei_hit/article/details/99297305