1 引言

DDD(领域驱动设计)早在2004年由Eric Evans提出,随着软件系统日趋复杂,近两年在后端、移动端架构中得到广泛的运用。DDD在不同的领域有不同的架构形态,接口测试面试题例如后端侧重分布式服务,在微服务架构中DDD会围绕核心服务而建设,移动后端工程师端更侧重UI交互,于是产生The Clean Architecture、VIPER这样的主流架构。移动端DDD架构虽然已有多年历史,但主要关注页面维度,尽可能让页面渲染、交互、数据请求和存储等逻辑各司其职架构图怎么制作,很少关注到页面内后端开发部的UI元素维度,而列表就是移动端最常见的UI元素,几乎每个页面都有列表,大部分页面剥离列表后只剩下一个空壳页面,页面渲染实际上是列表的渲染。对于Android而言,MVVM最多只描述源码1688了让Activity/Fragment后端语言级界面订阅U后端是做什么的I状态的更新,但不会约定RecyclerView内每个item如何更新。典型的VIPER架构:

DDD在网易LOFTER大型列表治理中的运用

那么,何谓大型列表?Lofter作为年轻人的兴趣社区,比较注重内容能丰富而接口测试高效地展现给用户,很多内容聚合页的RecyclerView包含文字、图片、音乐、视频、长文章、直播、小编推荐等十余种类型,每种类型的视图内部又有很多嵌套深、交互多的元素,不同类型之间还能包含公共的元素,一些元源码资本素在不同源码编程器页面场景甚至有不同的展示样式,这些元素之间的展示状态和排列关系可能还是动态的。如后端开发以下仅是关注流中的某个图片类型的Item:

DDD在网易LOFTER大型列表治理中的运用

该图片类型的item,在个人主页场景下,头部用户信息区又变为日期+合集:

DDD在网易LOFTER大型列表治理中的运用

像喜欢、点赞、收藏、评论发表、右上角菜单操作、用户信息,评论展示、审核状态等元素,对于其它类型的item(如文源码字、音乐、视频等)都是一样的。

承载源码网站这样巨型列表的页面架构设计,列表渲染的复杂性已远远大于采用MVC/MVP/MVVM/MVI设计的页面维后端语言度的复杂性,下面通过回顾Lofter大型列表架构的演进之路,来看源码之家如何用DDD治理大型列表架构。

2 单体阶段

这个阶段列表承载的样式都比较简单,虽然有多个类型,但无论是自己基于RecyclerView.Adapter扩展或者源码编辑器使用BRVAH开源库,渲架构师和程序员的区别染代码往往写在一个Adapter里,并不会难以维护。此时的架构示意图:

DDD在网易LOFTER大型列表治理中的运用

相当于在BRVAH的convert中写switch-case语句:

public class LofterAdapter<T extends MultiItemEntity, K extends BaseViewHolder>
        extends BaseMultiItemQuickAdapter<T, K> {
    @Override
    public void convert(helper: K, item: T) {
        switch (item.type) {
            TEXT: 
                ...
                break;        
            PHOTO:
                ...
                break;
            VIDEO:
                ...
                break;
            MUSIC:
                ...
                break;
        }
    }
}

但是,架构的劣化往往都是从单体开始,随着业务日趋复杂,Lofter首页关注流列表Dashb源码网站oardAdapter代码已达8000多行,并且由于关注流样式在较多页面复用,还派生了很多子类。一旦版本迭代包含关注流的需求,研发排期都会成架构师和程序员的区别倍增架构加,于是趁着某个版本产品对关注流重新设计,开始着手对adapter进行拆分。

3 分治阶段

我们当时基于BRVAH开源库开发了一套拆分框架,叫ComMultiItemAdapter。但当时BRVAH开源库还没后端是做什么的有支持BaseProvid架构师和程序员的区别erMultiAdapter这样的拆分方案,当然,为了不影响今后版本升级,我们并没有对BRVAH源码进行修改,所有的改动都是基于扩展实现。列表的构造阶段不再仅架构是什么意思关联item类型和item布局,大致写法如下:

addItemHolderController(TEXT, R.layout.text, new TextItemRender(adapterController));
addItemHolderController(PHOTO, R.layout.photo, new PhotoItemRender(adapterController));
addItemHolderController(MUSIC, R.layout.audio, new MusicItemRender(adapterController));
addItemHolderController(VIDEO, R.layout.video, new VideoItemRender(adapterController));

我们看到,列表的每种视图类型还关联了独立的渲染控制器,渲染控制器可以获取到加载的布局和条目数据(构造函数传入接口类型的adapterController的作架构师和程序员的区别用后面再说),此时的架构的示意图:

DDD在网易LOFTER大型列表治理中的运用

以TextItem架构师和程序员的区别Render为例,布局创建事件分发到doOnCreate方法,数据绑定事件分发到doOnBind方法:

public class TextItemRender extends BaseRender {
    @Override
    public void doOnCreate(TextItemHolder holder) {
        ...    
    }
    @Override
    public void doOnBind(TextItemHolder holder) {
        ...    
    }
}
public class TextItemHolder extends BaseItemHolder {
    public TextView title;
    public TextView content;
    ...
}

但不同类型的item之间并接口测试面试题非完全相互独立的,例如文字、图接口是什么片、视频、音乐卡片都有喜欢、推荐、收藏、评论、菜单项等互动操作的元素,随着版本迭代进行,这种不同类型卡接口类型片的公后端需要学什么后端语言元素越来越多,导致BaseRender越来越庞大,BaseRender类代码也高达2300多行,BaseRender对应的基类ViewHolder也有150个属性,用来存放所有各类型卡片都要展示的公共view引用。

我们知道,BRVAH允许item定义自己的ViewHolder类型,这样可以在item布局创建阶段,也就是doOnCreate方法接口crc错误计数中可以通过findViewById预先将View元素的引用保源码1688存在ViewHolder的属性中,然后在数据绑定阶段可以快速地更新Vie源码时代w元素的领域驱动设计状态,如上面的TextItemHolder。这种模式早已耳熟能源码资本详,看似非常好用,但当item布局复杂,所有的View元素都会排列在ViewHolder中,导致ViewHolder会有非常多的属性,且不同类型之间的公共元素会排列在BaseItemHolder接口英文中,最终我们很难找到关心架构图模板的View元素在哪里。

落地到具体后端需要学什么的页面开发中,我们会发现渲架构染控制器仅仅有以上两个方法,相对其需要满足的业务场景架构图怎么制作来说还显得过于单薄,例如缺少页面生命周期、滚动事件监听、图片加载、视频播放等基础能力,这就是渲染控制器构造函数中传接口测试入adapterController参数的作用。RecyclerView Adapter Cont后端需要学什么roller的职责主要架构师和程序员的区别扮演了Activity和RecyclerView架构师和程序员的区别之间的中介者的角色,在某些源码编辑器特殊场景下,可能还会与Presenter/ViewModel发生联系,RecyclerView Adapter Controller最终也将变为一领域驱动设计个“垃圾堆”。其典型源码编程器的示意图如下:

DDD在网易LOFTER大型列表治理中的运用

该阶段下,看似大型列表得到了治理,但弊端也架构是什么意思同样明显。

注:BRVAH最新版本,已支持架构师工资注册各类型的ItemProvider,在ItemProvider里完成布局的加载和数据绑定,具体使用这里不作表述,可参见官方介绍,本质上与ComMultiI源码之家temAdapter的功能相似。

4 DDD后端是做什么的阶段

在分治阶段,Lofter关注流卡片样式还没有引言的复杂,在后端接口数据模型重构和关注流按照最新样式改版的背景下,分治阶段的ComMul源码tiItemAdapter框架已无法适应团队业务发展的节奏,此时我们非常迫切后端需要一个能满足多人协作开发和维护需要的大型列表治理框架。幸运的是,我们还有DDD,DDD从诞生之日起就是为了应对软件系统的复杂性,虽然DDD在移动端源码编辑器手机版下载已产生像架构师证书VIPER、The Clean Archi接口类型tecture的标准化架构,但在这种大型列表治理的更细分的领域下却鲜有优秀案例。那么,是否能直接用The Clean Architecture来治理大型列表呢?当你深入思考后会发现各种不兼容后端是做什么的,这是因为软件架构的本质是为业务场景服务,同一种架构思想在不同的业务场景下必然会有差异化甚至特殊化的形态,这都意味着任何已有D架构师和程序员的区别DD架构都无法生搬硬套到大型列表治理中。

4.1 DDD规划

限界上下文的划分是DDD源码编辑器下载建模阶段最关键的一步,我们回过头来看RecyclerView图片类型的卡片,我们将卡片切分为很多bar,有的bar是多个类型卡片公共的,bar和bar之间在UI及交互上天然分割,虽然bar和bar之间会有访问,例如点操作栏接口类型的收藏按钮会在内容底部源码交易平台出现已收藏的提示条,但bar的业务边界已非常明确,故bar作为DDD接口测试的限界上下文再合适不过。当定义了bar后后端开发工程师,我们不再惧怕不同类型卡片之间有太多公共元素而导致BaseRender变得臃肿不堪,我后端框架们只需为各类型卡片编排所需的bar即可。以下是bar的部分划分方式:

DDD在网易LOFTER大型列表治理中的运用

架构师工资于文章卡片领域,我们枚举所有视图类型的bar,用DD后端是做什么的D划分为各个限界上下文,注意bar和限界上下文之间是充分非必要关系。图片加载器和视频播放器(包含列表自动播放)作为某些bar需要使用的通用能力,同时解耦领域逻辑对图片加源码编辑器下载载和视频播放底层库的依赖,并后端对底层基础能力进行封装,可成为单独的源码交易平台限界上下文,这也符合关注点分离原则后端。注意,bar的划分是非常灵活的,并非要界面上连续的矩形方块,亦或要满足一定条件才可见,甚至并不一定要满足UI可后端开发需要学什么见,例如以下事件后端是什么工作消费bar,仅用来拦接口截touch事件,处理整个卡片区域的双击喜欢动效、长按弹出菜单、点击空白背景跳转文章详情等手势行为。

DDD在网易LOFTER大型列表治理中的运用

部分bar之间存在一定程度的合作,源码1688例如评论输入源码资本bar发布评论后需要在评论展示bar展示刚发布的评论,操后端开发工程师作栏bar点收藏操作后会展示收藏成功反馈bar,仅收藏操作成功才展示。bar可作为每个子域内的聚合根,由于bar内的逻辑比较相似,各个子域可以基于聚合根协议ItemPartView扩展自己的子域逻辑。我们以最简单的调试信息展示Debug架构师和程序员的区别InfoBar(方便查看item在当前列表中的位置)为例,省略掉属性注入方法后,主要方法如下:

class DebugInfoBar : FrameLayout, ItemPartView {
    // bar的充血模型,后面叙述
    private var mItemPartLayoutMan: ItemPartLayoutMan<*, *, *>? = null
    // 工具类
    private var adapterContext: UnityAdapter.IAdapterContext? = null
    // ViewHolder
    private var mItemViewHolder: ItemViewHolder? = null
    // item对应的列表中的数据
    private var itemModel: PostCardModel? = null
    // findViewById保存在bar内,而不是ViewHolder中
    private var hintTv: TextView? = null
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    init {
        View.inflate(context, R.layout.item_part_debug_info_bar, this)
    }
    // 对应RecyclerView onCreateViewHolder
    override fun onCreate() {
    }
    // 对应RecyclerView onBindViewHolder
    override fun onUpdate(itemModel: Any?) {
        itemModel ?: return
        visibility = View.GONE
        val position = mItemViewHolder?.itemPosition ?: -1
        hintTv?.text = "我在第${position}位置 ${TrackerHelper.getPostCard2StatisType(mItemViewHolder?.itemViewType!!)}"
        if (position == -1) {
            setBackgroundColor(Color.BLUE)
        } else {
            setBackgroundColor(if (position % 2 == 0) {
                Color.GREEN
            } else {
                Color.RED
            })
        }
    }
    // 对应RecyclerView onBindViewHolder局部更新
    override fun onUpdate(itemModel: Any?, payloads: List<Any>?) {
    }
    // 图片加载失败时恢复加载,为扩展方法,由DDD框架自动调用
    override fun reloadImage() {
    }
    override fun onFinishInflate() {
        super.onFinishInflate()
        hintTv = findViewById(R.id.hint_tv)
    }
}

乍一看onCreate、onUpdate方法跟分治阶段的实现类似,不过就是接收了Recyc源码编程器lerView的onCreateViewHolder和onBindV后端是什么工作iewHolder通知,但思维方式已发生了微妙的转变,即我们将通知的接收放到了RecyclerView Item的每个bar中,这样传统View对象不再是分治阶段中的贫血模型的对象,仅仅是一个裸布局,必须得在View的外部完成id查找、点击事件监听、数据绑定、渲染等基本操作,这些切源码交易平台片View可自己完成这一系列过程。

这对工程维护来说会有怎样的变化呢?如果将ItemPartView的各种操作逻辑放源码网站到外部,最终会变得散乱不堪,查找和迭代都会极其不便,我们以点击事件监听为例,在分治阶段,使用BRVAH框架一般会这样写:

// 创建Adapter时(adapter为BaseQuickAdapter类型)
adapter.setOnItemChildClickListener(object : OnItemChildClickListener {
    override fun onItemChildClick(adapter: BaseQuickAdapter, view: View, position: Int) {
        when (view.id) {
            R.id.item_view_1 -> {
                // firstly, get item data from view or view holder                            
            }
            R.id.item_view_2 -> {
               // firstly, get item data from view or view holder             
            }     
        }    
    }
})
// 创建item view时(viewHolder为BaseViewHolder类型)
viewHolder.addOnClickListener(R.id.item_view_1, R.id.item_view_2);

但创建Adapter时一般是在Activit源码编辑器手机版下载y/Fragment中,创建item view时是在Item Render中,将点击事件的注册和回调拆成了两个步骤源码编辑器,大型列表还有很多类型的item,item内布局又极其复杂,view id往往有数十后端框架上百个,它们的注册和回调没有统一的地方管理,对维护造成不变。此外,点击事件的回调逻辑on后端工程师ItemChildClick并不在开发者所关注的item view的代码内,当工程庞大,容易导致开发者产生遗漏,我们线上有发生过修改item内vie架构图模板w曝光埋点参数但忘记修改点击事件埋点的案例。

4接口卡.2 DDD治理

在分治阶段,往往会为各个类型定义特定的ViewHolder,其中的属性接口测试专门用来架构图保存findViewById的引用,当不同类型卡片的公共元素越来越多,会导致基类ViewHolder的属性个数越来越多,如果没有详尽的注释,开发者会分不清哪个属性对应哪个公共元素,甚至还有ViewHolder中定义属性用来保存数据的情况,这也是我们在第二阶段遇到的痛点,如下图所示(虽然也可以使用源码1688kotlinx的synthetic工具、BRVAH的BaseViewHolder.getView方法来避免ViewHolder问题,但写法上会繁琐一些,更重要的源码编辑器是当某个后端是做什么的view后端是什么工作使用场景比较多、view后端开发是干什么的 id复用其它命名、view id命名不直观、IDE架构师和程序员的区别布局工具自动接口测试生成的view id,也无法利用ViewHolder属性更好命名所操作的源码1688元素,代码可读架构师证书性会大大降低)。

DDD在网易LOFTER大型列表治理中的运用

而在bar中自行设置点击事件,可以将点击事件的注册和回调合为一步,逻辑后端开发需要学什么统一内聚在开发者所关注的bar中,可以直接在bar中开辟属架构图模板性保存fi接口ndViewById的结果,不再需要定义任何特定类型的ViewHolder架构图模板,彻底消灭了在ViewHolder中添加属性的方式,对多人维护的模块更接口测试用例设计源码编程器好。

class PostOperationBar : RelativeLayout, ItemPartView, View.OnClickListener, View.OnLongClickListener {
    private var mItemPartLayoutMan: ItemPartLayoutMan<*, *, *>? = null
    private var adapterContext: UnityAdapter.IAdapterContext? = null
    private var mItemViewHolder: ItemViewHolder? = null
    private var mItemModel: PostCardModel? = null
    private var likeBtn: View? = null //喜欢按钮
    private var recommendBtn: View? = null //推荐按钮
    private var subscribeBtn: View? = null //收藏按钮
    private var commentBtn: Viw? = null //评论按钮
    init {
        View.inflate(context, R.layout.item_part_post_operation_bar, this)    
    }    
    override fun onCreate() {
        // 优化view元素命名,onUpdate等其它方法中使用这些view元素可读性更强
        likeBtn = findViewById(R.id.btn_1);
        recommendBtn = findViewById(R.id.btn_2);
        subscribeBtn = findViewById(R.id.btn_3);
        commentBtn = findViewById(R.id.btn_4);
        subscribeBtn = findViewById(R.i.btn_5);
        // bar内设置点击
        likeBtn.setOnClickListener(this)
        recommendBtn.setOnClickListener(this)
        subscribeBtn.setOnClickListener(this)
        commentBtn.setOnClickListener(this)
        // bar内设置长按
        subscribeBtn.setOnLongClickListener(this)
    }
    override fun onUpdate(itemModel: Any?) {
        // 使用可读性更强的命名
        likeBtn?.setSelected(mItemModel?.liked)
        recommendBtn?.setSelected(mItemModel?.recommended)
        subscribeBtn?.setSelected(mItemModel?.subscribed)
        ...
    }
    override fun onUpdate(itemModel: Any?, payloads: List<Any>?) {
        ...
    }
    override fun reloadImage() {
        ...
    }
    // bar内处理点击事件
    override fun onClick(view: View) {
        // 这里用when,bar内的可点击元素一般不会很多
        when (view.id) {
            R.id.btn_1 -> {
                ...            
            }
            R.id.btn_2 -> {
                ...            
            }
            R.id.btn_3 -> {
                ...            
            }
            R.id.btn_4 -> {
                ...            
            }
            R.id.btn_5 -> {
                ...            
            }
        }
    }
    // bar内处理长按
    override fun onLongClick(view: View): Boolean {
        when (view.id) {
            R.id.btn_5 -> {
                ...            
            }                    
        }
    }  
}

通过限界上下文的划分,我们虽然迈开了DDD治理的第一步,我们让bar能完成id查找、点击源码资本事件监听、数据绑定、渲染这一系列过程源码之家,ItemPartView成为了充血模型的对源码时代象,但是在大型列表的复杂应用场景下,仅仅成为这样的充血模型对象还是远远不够的,想象列表内视频滚出屏幕停止播放的场景、滚动停止自动选择屏幕内露出的视频播放的场景、页面进源码编辑器下载入后台停止播放视频的场景、监听用户在其它源码资本页面触发这篇文章的喜欢并同步更新当前列表中的喜欢状态接口测试用例设计的场景、后端开发工程师页面销毁时释放bar的额外资源,这些常见的应用场景却超出了ItemPartView现有能力,打破DDD治理的既定规约,为了不让工程走向后端劣化,这就需要引入bar的另一个聚合根ItemPartLayoutMan,用来监听页面生命周期、架构onActivityResult、滚动状态等通知接口卡,为ItemPartView赋能。

我们以用户信息bar为例,需要处理列表内该bar上的用户关注状态、合集信息的动态更源码编辑器新,可以在ItemPartLayoutMan的生命周期注册和销毁广播,例如在接收到关注用户广播时,需获取列表中包含该用户头像的item,最终调用RecyclerView Adapter的notifyI后端需要学什么temChanged向bar接口发送局架构师证书部更新:

DDD在网易LOFTER大型列表治理中的运用

DDD在网易LOFTER大型列表治理中的运用

class PostOwnerItemPartLayoutMan() : ItemPartLayoutMan<PostOwnerBar, PostCardModel, PostCardModel>() {
    // 关注用户通知
    private val followUserReceiver = object : BroadcastReceiver() {
        val actionUserId: Long = ...
        val isFollowAction: Boolean = ...
        adapterContext?.adapter?.data?.forEachIndexed { position, item ->
            item?.model?.let { model ->
                if (model.userId == actionUserId) {
                    adapterContext.adapter.notifyItemChanged(position, if (isFollowAction) {
                        PayLoadType.PAYLOAD_FOLLOW_ACTION
                    } else {
                        PayLoadType.PAYLOAD_CANCEL_FOLLOW_ACTION
                    })                   
                }                            
            }
        }
    }
    // 更新文章被加入合集通知
    private val updateCollectionInfoReceiver = object : BroadcastReceiver() {
        ...
    }
    override fun onContextAttached() {
        adapterContext.registerLocalBroadcastReceiver(BroadCastHelper.FOLLOW_FILTER, followUserReceiver)
        adapterContext.registerLocalBroadcastReceiver(CollectionConstant.ActionKey.ACTION_COLLECT_FILTER, updateCollectionInfoReceiver)
    }
    override fun getItemPartModel(itemModel: PostCardModel?): PostCardModel? {
        return itemModel
    }
    override fun onDestroy(owner: LifecycleOwner) {
        adapterContext.unregisterLocalBroadcastReceiver(followUserReceiver)
        adapterContext.unregisterLocalBroadcastReceiver(updateCollectionInfoReceiver)
    } 
    //通知该区域更新UI的行为,更新时会先检查model数据,model数据由更新发起者设置
    enum class PayLoadType {
        //关注行为
        PAYLOAD_FOLLOW_ACTION,
        //取消关注行为
        PAYLOAD_CANCEL_FOLLOW_ACTION,
        // 更新合集信息
        PAYLOAD_UPDATE_COLLECTION_INFO_ACTION
    }       
}

在用户信息bar在onUp架构师和程序员的区别date方法收到局部更新通知,并更新界面:

class PostOwnerBar : FrameLayout, ItemPartView, View.OnClickListener {
    ...
    override fun onUpdate(itemModel: Any?, payloads: List<Any>?) {
        payloads?.let { list ->
            list.forEach { one ->
                when (one) {
                    PostOwnerItemPartLayoutMan.PayLoadType.PAYLOAD_FOLLOW_ACTION -> {
                        // 按钮设为已关注                                                
                    }
                    PostOwnerItemPartLayoutMan.PayLoadType.PAYLOAD_CANCEL_FOLLOW_ACTION -> {
                        // 按钮恢复未关注                                                
                    }
                }
            }
    }    
}

再以列表内视频播放场景为例,PostVideoPlayBar是视频后端框架播放bar,我们可以在它对应的ItemPartLayoutM接口类型an中处理滚动播放、停止播放等交互逻辑:

class PostVideoPlayItemPartLayoutMan() : ItemPartLayoutMan<PostVideoPlayBar, PostCardModel, PostCardModel>() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        // 处理滚出屏幕停止播放
    }    
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
       // 处理滚动停止自动播放
    }  
    override fun onStop(LifecycleOwner owner) {
        // 处理视频停止播放
    }
}

领域内不可避免会涉及到一些中间数据的缓存,例如解析后的富文本数据、已曝光过的广告id、默认头像drawable等。分治阶段,会将数据都缓存在RecyclerVie架构w Adapter Controller中后端是什么工作。DDD阶段,每个限界上下文都可以拥有自己的数据缓存管理类,当然实架构师证书际未必需要对每个限界上下文都定义不同的类型:

// 上下文缓存管理类
class PostCardCache : IAdapterCache {
    // 已曝光的广告id
    val exposedAdIds = HashSet<String>()
    override fun clear() {
        exposedAdIds.clear()
    }
}

每个限界上下文都能通过领域工具接口卡类adapterContext访问到所需的数据缓存:

// 列表内广告曝光方法
fun trackAdExpose() {
    val adInfo = mItemModel?.adInfo ?: return
    adapterContext?.cache?.getItemCache(PostCardCache::class.java)?.let { cache ->
        if (!cache.exposedAdIds.contains(adInfo.id)) {
            adInfo.addShow()
            cache.exposedAdIds.add(adInfo.id)
        }
    }
}

领域工具类接口测试用例设计还提供了调用其它上下文服务的能力,bar只需声明自己的服务接口,其它bar就能很方便地调用到它领域驱动设计。我们以收藏提源码之家示条bar的展接口英文示为例(点收藏按钮时内容底部出现收藏提示条bar),收藏提示条bar定义展示自己的服务接口:

interface ISubscribeToastItemService : ItemService {
    fun showWithAlphaAnim(folderName: String?)
}

收藏提示条bar实现服务接口:

class SubscribeToastBar : FrameLayout, ItemPartView, ISubscribeToastItemService  {
    override fun showWithAlphaAnim(folderName: String?) {
        // 根据不同item类型找到内容view锚点的位置,并调整自身位置
    }
}

我们只需在操作栏bar的收藏按钮点击事件中,调用adap后端需要学什么terContext.itemServiceProvider.g源码编辑器手机版下载etItemService方法,传入ISubscribeToast领域驱动设计ItemService服务接口类,就能很方便地调用到收藏提示条bar的服务:

class PostOperationBar : RelativeLayout, ItemPartView, View.OnClickListener, View.OnLongClickListener {
    ...
    // bar内处理点击事件
    override fun onClick(view: View) {
        // 这里用when,bar内的可点击元素一般不会很多
        when (view.id) {
            ...
            R.id.btn_5 -> { // 收藏按钮
               doRequest { success ->
                   if (success) {
                      adapterContext?.itemServiceProvider!!.getItemService(ISubscribeToastItemService::class.java, itemViewHolder)?
                       .showWithAlphaAnim(folderName)                      
                   }               
               }            
            }
        }
    } 
}

4.源码3 DDD架后端工程师

经过DDD治理后,列表架构也变得更清晰,我们从页面的视角来看,完整的DDD架构如下:

DDD在网易LOFTER大型列表治理中的运用

由于团队成员对BRVAH较熟悉,为了更快落地,我们在BRVAH之上,设计了一套DDD Addapter Framework,用于支撑最基本的领域逻辑,每个item类型真正要展示bar,交给ItemLayo后端是做什么的utMan来编排,每个ItemLayoutMan对应一个XML文件,可以采用XML里编排bar的方式完成item布局的定义,bar本架构是什么意思身是一个ViewGroup,同时也是领域服务。页面初始化时,我们只需注册页面关心的item类型和ItemLayoutMan集合即可,页面关心的类型一般跟接口回吐的数据有关。我们以代表视频类型卡片的Po接口文档stVideoItemLay源码网站outMan为例,看下它是如何编排bar的。注接口测试册所需编排的bar:

class PostVideoItemLayoutMan() : ItemLayoutMan<ItemViewHolder, PostCardModel>() {
    override fun onContextAttached() {
        registerSimpleItemPartView(TopStubBar::class.java, BottomStubBar::class.java, DebugInfoBar::class.java, RecommendWordsBar::class.java, 
                                   PostAuditBar::class.java, PostReadNumBar::class.java, PostTextBodyBar::class.java, PostTagsBar::class.java,
                                   DoubleClickBar::class.java, SubscribeToastBar::class.java, PostTopHintBar::class.java)
        registerItemPartLayoutMan(PostOwnerItemPartLayoutMan::class.java, PostVideoPlayItemPartLayoutMan::class.java, 
                                  PostOperationItemPartLayoutMan::class.java, PostCommentListItemPartLayoutMan::class.java,
                                  PostCommentInputItemPartLayoutMan::class.java)
    }
}

然后在XML里编排和布局。在DDD治理之前,列表的it后端开发em布局非常庞大,相当于每个bar的布局都在item根布局下面平铺,嵌套层级非常深,虽然有的布局有include抽取,但只是规范并非强制接口文档,导致i架构图模板nclude和平铺混用,而经过DDD治理后端需要学什么之后,每个item的XML文件很容易维护,层级只有一级:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/lofter_background_primary">
    <lofter.component.middle.business.postCard2.common.viewstub.TopStubBar
        .../>
    <lofter.component.middle.business.postCard2.common.debug.DebugInfoBar
        .../>
    <lofter.component.middle.business.postCard2.common.audit.PostAuditBar
        .../>
    <lofter.component.middle.business.postCard2.common.owner.user.PostOwnerBar
        .../>
    <lofter.component.middle.business.postCard2.text.PostTextBodyBar
        .../>
    <lofter.component.middle.business.postCard2.video.PostVideoPlayBar
        .../>
    <lofter.component.middle.business.postCard2.common.side.PostReadNumBar
        .../>
    <lofter.component.middle.business.postCard2.common.operation.PostOperationBar
        .../>
    <lofter.component.middle.business.postCard2.common.viewstub.BottomStubBar
        .../>
    <lofter.component.middle.business.postCard2.common.toast.subscribe.SubscribeToastBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginLeft="@dimen/post_card_content_left_margin"
        android:layout_marginRight="@dimen/post_card_content_right_margin"
        android:visibility="gone"/>
    ...
    <lofter.component.middle.business.postCard2.common.doubleClickLike.DoubleClickBar
        .../>
</androidx.constraintlayout.widget.ConstraintLayout>

页面初始化时,Activity/Fragment后端语言里构造UnityAdapter,注册所关心的ItemLayoutMan即可:

adapter = UnityAdapter(this, ArrayList<UnityItemEntity>()).apply {
    registerItemLayoutMan(UnityViewType.POST_CARD_TEXT, R.layout.post_card_item_text, PostTextItemLayoutMan::class.java) //文字类型item
    registerItemLayoutMan(UnityViewType.POST_CARD_PHOTO, R.layout.post_card_item_photo, PostPhotoItemLayoutMan::class.java) //图片类型item
    registerItemLayoutMan(UnityViewType.POST_CARD_MUSIC, R.layout.post_card_item_music, PostMusicItemLayoutMan::class.java) //音乐类型item
    registerItemLayoutMan(UnityViewType.POST_CARD_VIDEO, R.layout.post_card_item_video, PostVideoItemLayoutMan::class.java) //视频类型item
    ...
}

我们再看下DDD治架构是什么意思理后的工程结源码构是怎样的,由后端需要学什么于bar的数量非后端框架常多,我们将限界上下文进行聚类,首先将各item类型特有的bar存放到对应类型目录:

DDD在网易LOFTER大型列表治理中的运用

而像操作栏、用户信息等公共元素bar存放common目录。例如common目录里存放事件消费bar、操作栏bar:

DDD在网易LOFTER大型列表治理中的运用

bar目录里接口类型面,只约定聚合根、上下文服务放必须放在一级目录下,其它可自行存放。例如操作栏bar内点击事件逻辑比较复杂,涉及的类比较多,可在operation目录内创建一个click目录存放。

4.4 防腐

DDD中,为了避免上下文被其它上下文侵蚀,会在上下文之间引入防腐层。而在客户端大型列表领域,列领域驱动设计表渲染数据是从后端数据转换过来的,列表样式往往会在多个页面复用,每个页面又请求不同的后端服务,另外,接口文档不同上下文只需要整个item数据的其中一部分,或经中间处理转为bar视图渲染的数据,于是我们需要给领域引入防腐层:

DDD在网易LOFTER大型列表治理中的运用

4.5 DDD框架

大型列后端工程师表治理是一个艰后端开发工程师辛的过程,我们在文章卡片改版的版本中完成了文章卡片列表治理,为了确保治理经验可复制,我们接口卡设计了与业务无关的DDD Adapter Framework,新的大型列表均可直接口测试面试题接基于DDD框架开发。除了上面介绍的要点外,DDD框架支持更多场景的使用,例如将单体Adapter作为一个特殊的item类型,这样老的单体Adapter的卡片样式也能迅速迁移到DDD Adapter中分发。除了治理大型列表,DDD框架的目标是统一整个应用所有列表,避免某些item类型不能在其它列表展示的情况,统一所有item类型卡片池领域驱动设计,当任何页面需要列表渲染,像叮当猫一样提供随取随用的服务。例如除了文章卡接口英文片,我们已在后端是做什么的视频大卡片、视频剧集弹窗的复杂列表领域都使用了DDD框架:

DDD在网易LOFTER大型列表治理中的运用

DDD在网易LOFTER大型列表治理中的运用

5 结束

由于大型列表往往不是一两个人维架构师证书护,DDD模式要在团队中真正推广,需要一架构师个接纳的过程,推广者除了在组内分源码编辑器享,还要充分吸纳其它成员提出的建议。例如有人提出,要为每个bar都定义一个ItemPartLayoutMan有点繁琐,有的bar比较简单,不需要监听页面生命周期和滚动事件,也不需要监听其它页面的通知进行动态更新,那么后端工程师就需要改进这些问题,针接口英文对这个提议,提供了registerSimpleItemPartView方法,这样可直接添加源码时代bar,不再需要ItemPartLayoutMan,这些渐进的改进也是对框架的打架构师磨。治理后,文章卡片已有较多人维护且能协同开发,版本迭代中有交互和U架构图怎么制作I调整时开发效率更高,开发修改的测试影响范围更小,再通过code review保障,暂未发生过线上bug。

作者:LOFTER技术组 范晨灿

本文发布架构设计自网易元气事业部前端团队,文章未经授权禁止任何形式的转载。欢迎与我们交流前端相关的技术问题和经验,同时,团队以及部门正在招聘前端、服务端以及客户端各岗位的开发人员,以上都可以联系LofterFron接口tendTeam@corp.netease.com进行交流。