单一职责原则的理解与实现

何谓单一职责原则

按字面理解,单一职责原则就是自己只负责自己的事,不需要理会别人的事。如果了解面对对象编程,那么应该会很容易了解这个单一职责原则。

在面对对象编程中,每个对象只负责自己的任务,比如该提供数据的就只是提供数据,该负责提供服务的就只提供服务,或者只是维护对象之间的关系,这样的开发方式代码耦合度较低,较灵活,易扩展。当然也可以一个对象负责多个任务,但是任务多了修改起来就比较容易影响到其他的任务。

Object Design: Roles, Responsibilies, and Collaborations这本书中提出可以从以下几方面判断出一个对象的多个行为构造出的是多职责还是单职责。
1、Information holder - 该对象设计为存储对象并提供对象信息给其他对象
2、Structurer - 该对象设计为维护对象与信息间的关系
3、Service provider - 该对象设计为处理任务与提供服务给其他对象
4、Controller - 该对象设计为负责控制一系列职责的任务处理
5、Coordinator - 该对象设计为把任务绑定/委托到其他对象上
6、Interfacer - 该对象设计为在各个对象间负责转化信息或者请求

一旦你知道了这些概念,那就狠容易知道你的代码到底是多职责还是单一职责了。

实现物品添加购物车

有这样的需求,有一些产品,需要以产品列表形式展示,并且提供双击产品添加到购物车。有一个购物车,接收产品添加到购物车的操作,添加之后并显示出来

这种简单的需求如果使用面对过程的方式来实现时很容易的,代码量也很少,但是不益于以后扩展。比如资源的来源、种类变了,或者添加方式变了,改一个东西都容易影响到其他逻辑,使用面对对象的方式来实现就会比较灵活,可以把这个需求抽象,然后再慢慢实现。

Paste_Image.png

简单抽象,就可以抽象出产品、购物车两个对象,购物车与产品需要通信所以需要一个通信的对象。再细化一下,把产品与购物车细分为信息提供与负责交互的两个对象并把这些对象转化为编程对象。

Paste_Image.png

由上可以抽象出6个对象,产品对象、事件对象、购物车对象均有一个负责对外的对象,类似于门面模式。Product、Event、Cart这三个对象比较灵活,可以复用或者拓展,当然实现起来会相对复杂一点,代码量也会多一点。但是这只是相对于简单不需要拓展的需求上,如果是比较庞大的需求或者是比较灵活的架构,使用面对对象编程的方式不仅可以节省代码,而且扩展维护更方便,使用哪种方式只是根据需求来选择了。

再对这些对象细化成基本工程,以下是基本交互思路图。

Paste_Image.png

以下是具体代码实现

Product.js 负责提供产品数据

function Product(id, description) {
    /**
     * 获取商品ID
     * 
     * @return {int   }  商品id
     */
    this.getId = function() {
        return id;
    };

    /**
     * 获取商品描述
     * 
     * @return {string} 商品描述
     */
    this.getDescription = function() {
        return description;
    }
}

module.exports = Product;

产品控制器ProductController.js, 负责对外交互

var ProductRepository = require('./ProductRepository.js');

function ProductController(productRepository, eventAggregator) {
    // 获取所有的物品
    var products = productRepository.getProducts();
    this.onProductSelect = function(id) {
        var product;
        products.forEach(function(pro) {
            if(pro.getId() == id){
                product = pro;
            }
            console.log('product is ' + pro.getId());
        });
        // 触发双击添加购物车事件
        eventAggregator.publish('productSelected', {
            product: product
        });
    }
    // 列出所有物品
    products.forEach(function(product) {
        var id = product.getId(),
            description = product.getDescription();

        console.log('product ' + id + ' , and description is ' + description);
        // 触发双击添加购物车
        if(id == 1){
            this.onProductSelect(id);
        }
    }.bind(this));
}

module.exports = ProductController;

购物车基本功能

function Cart(eventAggregator) {
    this.items = []; // 购物车列表
    /**
     * 添加物品到购物车
     * 
     * @param {object} item 商品对象
     */
    this.addItem = function(item) {
        this.items.push(item);
        console.log('add Item ' + item);
        // 触发添加购物车事件
        eventAggregator.publish('itemAdded', item);
    };
}

module.exports = Cart;

购物车控制器CartController.js ,负责购物车事件处理

function CartController(eventAggregator, cart) {
    // 订阅物品添加事件
    eventAggregator.subscribe('itemAdded', function(eventArgs) {
        var id = eventArgs.getId(),
            description = eventArgs.getDescription();

        console.log('the ' + id + 'has been add to cart &' + description);
    });

    // 订阅物品加入购物车事件
    eventAggregator.subscribe('productSelected', function(eventArgs) {
        console.log('recieved productSelected event ' + eventArgs);
        cart.addItem(eventArgs.product);
    });
}

module.exports = CartController;

事件对象Event.js,提供基本事件处理

function Event(name) {
    this.handlers = []; // 事件回调数组

    /**
     * 获取事件名称
     *              
     * @return {objecg} 事件对象
     */
    this.getName = function() {
        return name;
    };

    /**
     * 给事件添加处理函数
     *
     */
    this.addHandler = function(handler) {
        this.handlers.push(handler);
    };

    /**
     * 删除已经添加的事件处理函数
     * 
     * @param  {function} handler 需要删除的事件处理函数
     * @return {void}
     */
    this.removeHandler = function(handler) {
        var i = 0,
            len = this.handlers.length;
        for (; i < len; i++) {
            if(this.handlers[i] == handler){
                this.handlers.splice(i, 1);
                break;
            }
        }
    };

    /**
     * 执行事件处理函数
     * 
     * @param  {*} eventArgs 处理函数调用参数
     * @return {void}
     */
    this.fire = function(eventArgs) {
        this.handlers.forEach(function(h) {
            h(eventArgs);
        });
    }
}

module.exports = Event;

事件聚合器EventAggregator.js,负责提供发布、订阅方法给其他对象进行通信

var Event = require('./Event.js');

function EventAggregator() {
    this.events = []; // 事件对象集合

    /**
     * 根据事件名称获取事件对象
     * 
     * @param  {string} eventName 事件名称
     * @return {object}           事件对象
     */
    function getEvent(eventName) {
        var event;
        this.events.forEach(function(ev) {
            if (ev.getName() == eventName) {
                event = ev;
            }
        });
        //console.log('get event after ' + event);
        return event;
    }

    /**
     * 事件发布
     * 
     * @param  {string} eventName 事件名称
     * @param  {*     } eventArgs 调用事件处理函数时传的参数
     * @return {void}           
     */
    this.publish = function(eventName, eventArgs) {
        var event = getEvent.call(this, eventName);
        if (!event) {
            event = new Event(eventName);
            this.events.push(event);
        }
        event.fire(eventArgs);
    };

    /**
     * 订阅事件
     * 
     * @param  {string} eventName 事件名称
     * @param  {function} handler   事件触发时处理函数
     * @return {void}           
     */
    this.subscribe = function(eventName, handler) {
        var event = getEvent.call(this, eventName);
        if (!event) {
            event = new Event(eventName);
            this.events.push(event);
            console.log('add event ' + event.getName());
        }
        event.addHandler(handler);
    }
}

module.exports = EventAggregator;

以上代码就是购物车需求的实现,代码相对于面向过程变成的确挺多,但是职责清晰,易于理解。以上代码实现均参考汤姆大叔博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 1.埋点是做什么的 2.如何进行埋点 3.埋点方案的设计 近期常被问到这个问题,我担心我的答案会将一些天真烂漫的孩...
    lxg阅读 1,997评论 0 1
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,473评论 2 59
  • 如果你问我对一个人最高的评价是什么? “有趣。” 有人说一个人有趣是因为有爱和被爱;有人说有趣是因为经历得多、见得...
    captain_嫣阅读 4,570评论 54 140
  • 昨天早上爸爸跑到格格床上来看她,丫头睁开眼对着爸爸说:“你终于回来了!”因为爸爸昨晚很晚才回来,她都已经睡着了。孩...
    格格麻麻阅读 250评论 0 0