预言需求——用好的设计,应对可能的变化
楔子
认识了一位艺术家朋友,在他的引导下,开始看一些艺术展,看各种油画,水墨,水彩————屡屡为在那种固化颜料之下的展现的波光粼粼而赞叹不已。
在另一位朋友的安利下,我迷上音乐,沉迷古典。
在流连这些艺术中时,我总在不停的感叹:好的艺术,一定是不朽的,我们可以听20年前的流行,欣赏200年前的古典,欣赏百十年前的毕加索。
每有这些感慨,我总自惭形愧,作为软件工程师,代码总是暂时的,短暂的,甚至从写下的那一刻起,就是过时的。
我们甚至在今天通宵去写一个只在明天运行一次的脚本。
是的,悲观的角度看,我们一直在做无用功
于是,我总会想,如何写出’不朽’的代码,让自己的工作,尽可能的完美,尽可能延长他的运行周期。
对我来讲,这种方式,应该是预见需求,即预期在一个时间内,你接到的需求去如何的变化,当这种变化发生时,你的设计去如何的应对。
是的,想一个预言家一样,去给你的代码预知一段时间内的未来。
好的工程师,都应该是预言家
那么今天这篇,就从一个非常小的需求,聊聊我是怎么去做预言家,给代码卜卦。
需求
这是我在之前的东家接到的一个需求,当时我们用php的laravel框架已经实现了后台服务并预期上线。
因为我们本质上是一个理财的网站,刚上线时效果不是很好,于是PM说,我们要给新注册的用户发券,促进他们消费。
是的,我们的需求很简单,在用户注册时,给用户发券
也就是说,需求是这个样子:
第一次实现
在当时,我们已经有了auth/user/register
接口,里面调用了userService
这个类的一个newUser
的方法,这个方法里实例化了一个userData
,它生成了一个userModel
,并把它加到数据库中。
大概是这个样子:
userControler/register
|-userService/newUser
|--userData/newUser
|---userModel/create
|----mysql ...
那么直观的解决方式
我们只要有一个coupon的实现(data/model) ,然后在userService
的newUser
方法里调用newCoupon
就可以了。
于时我们代码这个样子就可以:
userControler/register
|-userService/newUser
|--userData/newUser
|---userModel/create
|----mysql ...
|--couponData/newCoupon
|---couponModel/create
|----mysql ...
可是,这样真的可以了么?
这个需求,会有什么变化?
深挖需求
之前说过,我们的需求是在用户注册时,送消费券
这个需求,有两个关键点,即什么时间————用户注册,做什么事情————送消费券
那么,从时间和行为两个维度考虑后继可能发生的变化:
- 时间问题
- 只在用户注册时发券么?
- 用户登录时会不会发券?
- 用户购买的时候会发券么?
- 用户充值的时候会发券么?
- 行为问题
- 我们只会发券么?
- 我们有没有可能发现金?(理财体验金,你懂的)
- 我们有没有可能加入邀请返现?
也就是说,我们的需求,有可能变成这个样子:
在想到这些可能到来的变化后,我不得不重新审视自己的设计……
进一步的考虑
在发现我的需求可能的变化后,我开始考虑,怎么去应对未来的变化,那么我的设计的目的变成了:
- 以一种通用的方式实现送券;
- 用合适的方式获知用户注册这个时间点;
- 尽可能简单的把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
中去将具体的事件生成;
这时,我们的需求,受设计影响成为了这样:
实现: 行为的问题
我们解决了时机,那么行为就变得容易了。
根根上文,不难看出,我们在实现行为时可以得到三个参数:
- event ---- 我们定义的事件,比如userRegister
- db_operation ---- db的操作,比如update,create
- 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
],
//
];
这样,我们的设计最终变成了这个样子:
理想主义的形式
我们在这个需求的第一版的迭代上,加入了这种设计,然后随着后继的迭代,我们又做了如下的更改:
- 对要发送的model进行处理,将db的原始数据进行裁剪,形成了统一的数据约定;
- 将event的处理变成了异步。
这时,我们的设计就成为了这种:
小结
我们将一个看似简单的需求————注册送券,无限的复杂化,去预测这个需求可能发生的迭代,可能产生的变化。
在这里,我们使用观察者模式,实现了对现有代码的扩展,同时,运用了大量的工厂模式(array).
在保证实现基础的基础上,我们做到了:
- 对原有代码实现了非侵入的扩展(user相关的内容没有更改)。
- 加入的所更改都是以增加为主;
- 新的业务逻辑是低耦合甚至完全解耦的。
- 为以后可能的变化留出了充足的时间。