设计模式之简单工厂模式

概述

简单工厂模式 属于创建型模式,在简单工厂模式中用于被创建实例的方法通常为静态方法,又叫静态工厂方法

结构与业务场景

工厂(Factory)

核心部分,负责实现创建所有产品的内部逻辑,工厂类可以被外界直接调用,创建所需对象。

抽象产品类(Product)

工厂类所创建的所有对象的父类,封装了产品对象的公共方法,所有的具体产品为其子类对象

具体产品类(ConcreteProduct)

简单工厂模式的创建目标,所有被创建的对象都是某个具体类的实例。它要实现抽象产品类中声明的方法。

实际运用

这里我以的画板元素 的实现作为案例进行讲述,如何实现简单工厂模式。我们的画板需要添加 文本条码二维码表格边框图形流水号时间日期 等等,那么针对这些元素他们都有绘制移动缩放旋转辅助线 等等功能,所以这些元素就是具体的实例,而这些动作就是抽象的方法实现。

  • 首先,我们不考虑工厂类,工厂类我们放到最后去实现,我们先实现 抽象产品类 在我的项目里我叫它 JCElementBaseView,这是一个抽象产品类,用户不用初始化它,即使你初始化它也没有意义,因为它并没有什么实质的东西展示,它能做的就是对子类提供重写的方法和实现。
    @class JCElementBaseView;
    @protocol JCElementProtocol <NSObject>
    @optional
    - (void)elementViewTouchBegin:(JCElementBaseView *)element;
    - (void)elementViewDragBegin:(JCElementBaseView *)element;
    - (void)elementViewDoubleClick:(JCElementBaseView *)element;
    - (void)elementViewFrameShouldChange:(JCElementBaseView *)element;
    - (void)elementViewFrameDidChange:(JCElementBaseView *)element;
    - (void)elementViewDidClick:(JCElementBaseView *)element;
    @end

    @interface JCElementBaseView : UIView
    /** 旋转角度 */
    @property (nonatomicassignCGFloat rotate;
    /** 是否选中 */
    @property (nonatomicassignBOOL selected;
    /** 锁定 */
    @property (nonatomicassignBOOL isLock;
    /** 元素的id:时间戳,精确到毫秒 */
    @property (nonatomiccopyNSString *elementId;
    /** 代理 */
    @property (nonatomicweakid<JCElementProtocol> delegate;

    /** for override */
    - (void)zoomDragMoving: (UIControl *) c withEvent:touches;
    - (void)zoomDragOutside: (UIControl *) c withEvent:touches;
    - (void)zoomDragEnded: (UIControl *) c withEvent:touches;

    /** 元素最小宽度---子类实现 毫米 */
    - (CGFloat)minWidth_mm;
    /** 元素最小高度---子类实现 毫米 */
    - (CGFloat)minHeight_mm;
    /** 元素最大宽度---子类实现 毫米 */
    - (CGFloat)maxWidth_mm;
    /** 元素最大高度---子类实现 毫米 */
    - (CGFloat)maxHeight_mm;
    /** 获取旋转角度为0时的frame */
    - (CGRect)getOrignalFrame;
    /** 刷新图像 */
    - (void)reloadSDKImage;

    @end
  • 接下来就是针对我们特定的元素的一些特定的实现一一去构建子类(具体产品类),这里我们以 条码 为例作为大概展示其子类是如何实现的,基本可以看出,都是以实现抽象父类 的方法为主。
    @implementation JCElementBarCode

    - (instancetype)init {
        if (self = [super init]) {
            self.backgroundColor = [UIColor clearColor];
            [self initUI];
        }
        return self;
    }

    - (void)initUI {
        [self insertSubview:self.barCodeImageView atIndex:0];
        [self.barCodeImageView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.size.equalTo(self);
            make.top.left.equalTo(self);
        }];
        [self.barCodeImageView addSubview:self.errorButton];
        [self.errorButton mas_makeConstraints:^(MASConstraintMaker *make) {
            make.size.mas_equalTo((CGSize){116,24});
            make.center.equalTo(self.barCodeImageView);
        }];
    }

    - (void)reloadSDKImage {
        NSString *value = [DrawBoardInfo currentValueFor:self.excelValue selectRowIndex:self.excelIndex elementId:self.elementId];
        if (IsExcelString(self.excelValue)) {
            _text = value;
        }
        // resize
        CGFloat minWidth = [self minWidth_mm]*DrawBoardInfo.mm2pxScale;
        rotate(
               if (self.width < minWidth) {
                   self.width = minWidth;
               }
               )
        [self drawBarCodeImage];
    }

    #pragma mark -- set
    - (void)setText:(NSString *)text {
        _text = text;
        /** 根据文本画一维码 */
        [self reloadSDKImage];
    }

    - (void)setConfigure:(JCElementBarCodeConfigure *)configure {
        _configure = configure;
        /** 根据条码类型重新布局 */
        [self drawBarCodeImage];
    }

    - (void)setExcelIndex:(NSInteger)excelIndex {
        [super setExcelIndex:excelIndex];
        [self reloadSDKImage];
    }

    #pragma mark -- getImage
    - (void)drawBarCodeImage {
        if (!self.text || !self.configure) return;
        [JCDrawCodeManager drawBarCodeWithElement:self result:^(BOOL success, UIImage * _Nonnull image, NSString * _Nonnull errorMsg) {
            self.barCodeImageView.image  = image;
            [self showErrorMsg:errorMsg];
        }];
    }

    #pragma mark -  DragMove
    - (void)zoomDragMoving: (UIControl *) c withEvent:touches {
        [super zoomDragMoving:c withEvent:touches];
        [self drawBarCodeImage];
    }

    - (void)zoomDragOutside: (UIControl *) c withEvent:touches {
        [super zoomDragOutside:c withEvent:touches];
        [self drawBarCodeImage];
    }

    - (void)zoomDragEnded: (UIControl *) c withEvent:touches {
        [super zoomDragEnded:c withEvent:touches];
        [self drawBarCodeImage];
    }

    #pragma mark -- lazy
    - (UIImageView *)barCodeImageView {
        if (!_barCodeImageView) {
            _barCodeImageView = [[UIImageView alloc] init];
            _barCodeImageView.contentMode = UIViewContentModeScaleToFill;
            _barCodeImageView.backgroundColor = [UIColor clearColor];
            // 最近邻居算法,图像放大不模糊
            _barCodeImageView.layer.magnificationFilter = kCAFilterNearest;
        }
        return _barCodeImageView;
    }

    #pragma mark -- FrameProtocol
    - (CGFloat)minWidth_mm {
        if (self.configure.codeType == JCCodeType_EAN13) {
            return 13*2;
        } else if (self.configure.codeType == JCCodeType_EAN8) {
            return 7*2;
        } else if (self.configure.codeType == JCCodeType_UPCA) {
            return 11*2;
        } else if (self.configure.codeType == JCCodeType_UPCE) {
            return 7*2;
        } else if (self.configure.codeType == JCCodeType_ITF25 || self.configure.codeType == JCCodeType_CODEBAR) {
            return 2*2;
        } else {
            return MAX(self.text.length*2,2);
        }
    }

    - (CGFloat)minHeight_mm {
        return self.configure.textHeight + 3;
    }

    - (CGFloat)maxWidth_mm {
        return 200;
    }

    - (CGFloat)maxHeight_mm {
        return 100;
    }
  • 既然具体产品类都有了,剩下就是构建 工厂类 ,这里我是用的一个单独的Manager 的类来实现元素的构建的 JCDefaultElementManager
    @implementation JCDefaultElementManager

    + (JCElementBaseView *)defaultElementWithClass:(Class)eClass {
        CGFloat mm2pxScale = DrawBoardInfo.mm2pxScale;
        JCElementBaseView *element;
        if ([eClass isEqual:[JCElementText class]]) {
            JCElementText *temp = [JCElementText new];
            temp.placeHolder = XY_LANGUAGE_TITLE_NAMED(@"",element_text_placeholder);
            temp.text = @"";
            temp.configure = [JCElementTextConfigure defaultConfigure];
            temp.frame = (CGRect){10,10,4*DrawBoardInfo.mm2pxScale*text_default_font_mm_size,24};
            element = temp;
        } else if ([eClass isEqual:[JCElementTextImage class]]) {
            JCElementTextImage *temp = [JCElementTextImage new];
            temp.frame = (CGRect){10,10,4.2*DrawBoardInfo.mm2pxScale*text_default_font_mm_size,24};
            temp.placeHolder = XY_LANGUAGE_TITLE_NAMED(@"",element_text_placeholder);
            temp.text = XY_LANGUAGE_TITLE_NAMED(@"",element_text_placeholder);
            temp.configure = [JCElementTextConfigure defaultConfigure];
            element = temp;
        } else if ([eClass isEqual:[JCElementBarCode class]]) {
            JCElementBarCode *temp = [JCElementBarCode new];
            temp.frame = (CGRect){10,10,20*DrawBoardInfo.mm2pxScale,10*DrawBoardInfo.mm2pxScale};
            temp.configure = [JCElementBarCodeConfigure defaultConfigure];
            temp.text = @"123456";
            element = temp;
        } else if ([eClass isEqual:[JCElementQRCode class]]) {
            JCElementQRCode *temp = [JCElementQRCode new];
            temp.frame = (CGRect){10,10,10*mm2pxScale,10*mm2pxScale};
            temp.codeType = JCCodeType_QRCODE;
            temp.qrCodeString = @"123456";
            element = temp;
        } else if ([eClass isEqual:[JCElementGraph class]]) {
            JCElementGraph *temp = [JCElementGraph new];
            temp.frame = (CGRect){10,10,10*DrawBoardInfo.mm2pxScale,10*DrawBoardInfo.mm2pxScale};
            temp.configure = [JCEleBoxConfigure defaultConfigure];
            element = temp;
        } else if ([eClass isEqual:[JCElementSerialNumber class]]) {
            JCElementSerialNumber *temp = [JCElementSerialNumber new];
            temp.frame = (CGRect){10,10,145,58};
            temp.styleConfigure = [JCSerialStyleConfigure defaultConfigure];
            JCElementTextConfigure *configure = [JCElementTextConfigure defaultConfigure];
            configure.textAlignment = NSTextAlignmentCenter;
            temp.configure = configure;
            element = temp;
        } else if ([eClass isEqual:[JCElementTime class]]) {
            JCElementTime *temp = [JCElementTime new];
            temp.frame = (CGRect){10,10,200,29};
            temp.timeConfigure = [JCEleTimeConfigure defaultConfigure];
            temp.configure = [JCElementTextConfigure defaultConfigure];
            element = temp;
        } else if ([eClass isEqual:[JCElementSheet class]]) {
            JCElementSheet *temp = [JCElementSheet new];
            temp.configure = [JCSheetConfigure defaultConfigure];
            temp.left = 10;
            temp.top = 10;
            element = temp;
        } else if ([eClass isEqual:[JCElementLogoView class]]) {
            JCElementLogoView *temp = [JCElementLogoView new];
            element = temp;
        } else if ([eClass isEqual:[JCElementLine class]]) {
            JCElementLine *temp = [[JCElementLine alloc] init];
            temp.frame = (CGRect){10,10,10*DrawBoardInfo.mm2pxScale,0.4*DrawBoardInfo.mm2pxScale};
            temp.configure = [JCEleLineConfigure defaultConfigure];
            element = temp;
        } 

        NSInteger count = DrawBoardInfo.addElementIndex;
        if (count > 8) count = 1;
        if(count == 0){
            element.left = 10;
            element.top = 10;
        }else{
            element.left = 10 + count*16;
            element.top = 10 + count*16;
        }
        /** 设置时间戳 */
        element.elementId = [XYTool randomElementId];
        DrawBoardInfo.addElementIndex++;
        return element;
    }

工厂类就很简单了,用户只用关心需要创建什么类型的元素,直接传入相应元素的 类名 即可,也很显然这个是静态方法,所以也叫 静态工厂类,接下来我们来看看业务场景中用户究竟是如何创建和使用元素的:

-(void)add_wenben{
    [self addElement:Element(JCElementTextImage)];
}

-(void)add_yiweima{
    [self addElement:Element(JCElementBarCode)];
}

-(void)add_erweima{
    [self addElement:Element(JCElementQRCode)];
}

-(void)add_liushuihao{
    [self addElement:Element(JCElementSerialNumber)];
}

-(void)add_shijian{
    [self addElement:Element(JCElementTime)];
}

-(void)add_xingzhuang{
    [self addElement:Element(JCElementGraph)];
}

-(void)add_xiantiao{
    [self addElement:Element(JCElementLine)];
}

是不是及其简单和清爽,需要往画板上添加一个元素,直接简单的调用一行代码指定一个元素类名即可实现,不用关心内部如何实现的,即使对缩放等操作过程需要了解,我们也提供了相应的 代理,这些都是基于自己的业务需求进行扩展,任何一个设计模式 都不是 为了设计而设计,我们要为了更简单更快速的处理我们的业务需求,这才是根本。

简单工厂优缺点和适用环境

优点

  • 用户只需要知道具体工厂的名称即可得到所要的产品,无需知道具体产品的创建过程。
  • 工厂类包含必要的判断逻辑,可以决定在什么时候创建哪一个产品的实例,客户端可以免除直接创建产品对象的职责。(可以结合反射等思想,配置文件等等更加优化实现)
  • 灵活性强,对于新产品的创建,只需多写一个相应的工厂类
  • 典型的解耦框架,高层模块只需要知道产品的抽象类,无需关心其他实现类

缺点

  • 类的个数容易过多,增加复杂度
  • 增加了系统的抽象性和理解难度
  • 抽象产品只能生产一种产品,此弊端可使用 抽象工厂模式 来解决

适用环境

  • 工厂类负责创建对象比较少,因为这样不会造成工厂方法中的业务逻辑过于负责,比如上述的画板案例中,元素的种类就是那么多,不会无限制的进行扩张,在可预见范围内都是可以接受的。
  • 客户端只知道传入工厂类的参数,对于如何创建对象并不关心,俗称只管拿去用。

总结

我们使用设计模式的目的无非就是三个

  • 缩短开发时间
  • 降低维护成本
  • 在应用程序之间和内部轻松集成,减少耦合

具体什么时候使用何种设计模式因项目和业务需求而不同,这是个人针对设计模式 的第一篇文章,后续还会针对性的出一些个人对其他设计模式的理解和应用,之所以写这系列的文章也是希望能对自己的架构之路有所提升,同时如果能帮助到其他人就更好了。

------------- 本文结束  感谢您的阅读 -------------