python状态模式的一次实践

前言

要想写成可维护性高的代码,必须要懂的设计模式。在平时工作中我会经常使用策略模式去让代码更加简洁,对于状态模式一直只是知道,但从来没实践过,前段时间帮朋友做一个抢苗的爬虫程序,发现在这种需要下单的业务场景下特别适合状态模式发挥其作用。

什么是状态模式

状态模式(State)定义:
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。

类型:形为型模式
在这里插入图片描述

业务背景

我的业务流程是这样的
在这里插入图片描述

代码实现

先上一张类图
在这里插入图片描述
可以看到和前面的那张状态模式类图有两点不一样:1、context直接作为属性保持在状态类中,而不是通过run参数传入;2、多了一个prevStep属性,这个属性是为了持有上一个状态的实体,以便回滚状态。

下面是源代码:

class SubscribeVaccine():
    # 300: 该身份证或微信号已有预约信息 203: 校验信息错误
    raiseErrCode = [300, 203]

    def __init__(self, subInfo) -> None:
        self.currStep = QueryAvaliableDateStep(self)
        self.subInfo = subInfo
        self.reqSeed = 0

        self.vacId = subInfo['vacId']
        self.vacName = subInfo['vacName']
        self.hosId = subInfo['hosId']
        self.sessionId = subInfo['sessionId']
    
    def setSubInfo(self, key, value):
        self.subInfo[key] = value

    def getSubInfo(self, key):
        return self.subInfo[key]

    def setStep(self, step):
        self.currStep = step

    def runStep(self):
        return self.currStep.run()
    
    def request(self, url, successCode=200):
        log.info(f'zstl加密种子: {self.reqSeed}')
        data, reqSeed = requestZMYY(url, self.sessionId, 0, successCode)
        self.reqSeed = reqSeed

        if 'GetCaptcha' not in url and 'UserSubcribeList' not in url:
            log.info(f'接口返回结果: {data}')
        if data['status'] == 408:
            raise BusinessException(code=0, msg='接口出现408错误,请检查sessionId是否过期和程序请求逻辑是否有问题!')
        if data['status'] in self.raiseErrCode:
            raise BusinessException(code=0, msg=data['msg'])
        if data['status'] != successCode:
            log.error(f'知苗易约接口{url}请求错误, data: {data}, 指定成功码为: {successCode}')
            return None
        
        return data

class BaseStep():
    def __init__(self, context: SubscribeVaccine, prevStep = None) -> None:
        self.context = context
        self.prevStep: BaseStep = prevStep

    def run(self):
        pass

    def rollbackPrevStep(self):
        if not self.prevStep:
            return
        self.context.setStep(self.prevStep)
        return self.context.runStep()

class QueryAvaliableDateStep(BaseStep):
    def __init__(self, context: SubscribeVaccine, prevStep=None) -> None:
        super().__init__(context, prevStep=prevStep)

    def run(self):
        vacId = self.context.vacId
        vacName = self.context.vacName
        hosId = self.context.hosId

        log.info(f'获取疫苗<<{vacId}-{vacName}>>可预定的日期')
        month = getCurYearMon()
        url = f'{commonUrl}?act=GetCustSubscribeDateAll&pid={vacId}&id={hosId}&month={month}'
        res = self.context.request(url)
        
        dateList = res['list']
        dateAvaliableList = []
        for dateItem in dateList:
            if dateItem['enable']:
                log.info(f'{dateItem["date"]}可预约')
                dateAvaliableList.append(dateItem['date'])
        
        # 打乱顺序
        random.shuffle(dateAvaliableList)
        self.context.setStep(QueryAvaliableTimeStep(self.context, self, dateAvaliableList))
        time.sleep(0.5)
        return self.context.runStep()    

class QueryAvaliableTimeStep(BaseStep):
    def __init__(self, context: SubscribeVaccine, prevStep, dateAvaliableList: list) -> None:
        super().__init__(context, prevStep=prevStep)

        self.iter = iter(dateAvaliableList)

    def run(self):
        vacId = self.context.vacId
        hosId = self.context.hosId
        try:
            date = next(self.iter)
            self.context.setSubInfo('date', date)

            log.info(f'获取{date}可预定的时间')
            url = f'{commonUrl}?act=GetCustSubscribeDateDetail&pid={vacId}&id={hosId}&scdate={date}'
            res = self.context.request(url)

            timeList = res['list']
            mxidList = []
            for timeItem in timeList:
                if timeItem['qty'] > 0:
                    log.info(f'获取到mxid: {timeItem["mxid"]}')
                    mxidList.append(timeItem['mxid'])
            
            # 打乱顺序
            random.shuffle(mxidList)
            self.context.setStep(SubmitOrderStep(self.context, self, mxidList))
            time.sleep(0.5)
            return self.context.runStep()
        except StopIteration:
            name = self.context.getSubInfo('cname')
            raise BusinessException(code=0, msg=f'{name}预定疫苗失败!!!!!!!!!!!')

class SubmitOrderStep(BaseStep):
    def __init__(self, context: SubscribeVaccine, prevStep, mxidList) -> None:
        super().__init__(context, prevStep=prevStep)

        self.iter = iter(mxidList)

    def run(self):
        loop = True
        subSuccess = False
        while loop:
            try:
                mxid = next(self.iter)
                for i in range(5):
                    guid = self.captchaVerify()
                    if guid:
                        break
            
                if not guid:
                    raise BusinessException('滑块验证码校验失败,请检查程序是否正常!')
                
                time.sleep(random.uniform(0.1, 0.7))
                for i in range(5):
                    res = self.saveOrder(mxid, guid)
                    if res:
                        break

                    time.sleep(random.uniform(0.4, 0.7))
                if not res:
                    raise BusinessException('订单提交失败!')

                time.sleep(random.uniform(0.4, 0.7))
                success = self.queryOrderStatus()
                if success:
                    cname = self.context.getSubInfo('cname')
                    log.info(f'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
                    log.info(f'!!!!!!!!!!!!!{cname}预定成功!!!!!!!!!!!!!!!')
                    log.info(f'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
                    subSuccess = True
                    loop = False
            except StopIteration:
                log.info(f'提交失败,尝试其他日期')
                loop = False
                return self.rollbackPrevStep()

        return subSuccess

    def captchaVerify(self):
        '''
        识别滑块验证码
        '''
        log.info('获取滑块验证码')
        for i in range(5):
            res = self.context.request(f'{commonUrl}?act=GetCaptcha', 0)
            dragon = f'{res["dragon"]}'
            tiger = res.get('tiger', None)
            save_base64_to_img(dragon, './dragon.png')
            if tiger:
                save_base64_to_img(tiger, './tiger.png')
            
            if res['msg'] == 'ROTATE': # 判断是否是旋转验证码
                rotated_image = rotateCaptcha.rotateCaptcha.getImgFromDisk('./dragon.png')
                predicted_angle = rotateCaptcha.predictAngle(rotated_image)  # 预测还原角度
                x = 360 - predicted_angle
                time.sleep(random.uniform(0.5, 1))
            else:
                verify = SlideCaptcha(tiger, dragon, './res.png')
                x = verify.discern()
                time.sleep(random.uniform(0.5, 1))
            log.info(f'滑块验证码识别结果: {x}')
            
            token = res.get('payload', {}).get('SecretKey', None)
            verRes = self.context.request(f'{commonUrl}?act=CaptchaVerify&token={token}&x={x}&y=5')
            if verRes['status'] == 201:
                if i == 5:
                    raise BusinessException(code=0, msg=f'出现201错误重试5次! 请稍后再试!')
                else:
                    continue
            if not verRes:
                log.info(f'滑块验证码验证失败!')
                return None
            if verRes['status'] == 200:
                return verRes['guid']
            log.info(f'出现201错误码,重试第{i + 1}次!')

    def saveOrder(self, mxid, guid):
        '''
        下单
        '''
        userInfo = {
            'birthday': self.context.getSubInfo('birthday'),
            'tel': self.context.getSubInfo('tel'),
            'sex': self.context.getSubInfo('sex'),
            'cname': self.context.getSubInfo('cname'),
            'doctype': self.context.getSubInfo('doctype'),
            'idcard': self.context.getSubInfo('idcard'),
            'mxid': mxid,
            'date': self.context.getSubInfo('date'),
            'pid': self.context.vacId,
            'Ftime': self.context.getSubInfo('ftime'),
            'guid': guid
        }
        log.info(f'开始下单, 下单参数: {userInfo}')
        orderParams = dict2query(userInfo)
        res = self.context.request(f'{commonUrl}?act=Save20&{orderParams}')
        if not res:
            log.info(f'订单提交失败!')
            return False

        log.info(f'订单提交成功!')
        return True
    
    def queryOrderStatus(self):
        url = f'{commonUrl}?act=GetOrderStatus'
        res = self.context.request(url)
        
        return bool(res)

总结

使用状态模式实现的代码可读性和扩展性很强,如果要加一个查询可预约医院的步骤,只需要添加对应的状态类,然后改动少许代码就能够完成了。想想如果用for循环和if判断来完成这段逻辑,代码复杂度是不是会很复杂。
最后贴上源代码https://github.com/yuemiao-web/yuemiao_backend/blob/main/yuemiao/sub_vac/sub_vac.py,如果觉得作者写的还不错,请给个小星星😁
水平有限,如有错误,还请指正,欢迎大家一起交流学习。

参考文章:https://blog.csdn.net/weixin_34292287/article/details/92172254

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值