提示与技巧

避免代码重复

尽可能避免复制粘贴同一段代码到多个地方是很好的习惯。尽管有时需要一些思考才能明白如何避免复制粘贴代码,但是将代码只重复一次通常会在你需要更改设计或者修复错误时为你节省很多时间和精力。

下面是一些实现代码复用的技巧。

不要将你的应用复制多份

你应当尽可能避免复制你的应用文件夹来创建一个有细微差别的版本,因为此时重复代码很难维护。

If you need multiple rounds, set num_rounds. If you need slightly different versions (e.g. different treatments), then you should use the techniques described in 实验组(Treatments), such as making 2 session configs that have a different 'treatment' parameter, and then checking for session.config['treatment'] in your app’s code.

如何创建多个字段

假定你的应用中有很多几乎完全一样的字段,例如:

class Player(BasePlayer):

    f1 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f2 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f3 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f4 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f5 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f6 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f7 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f8 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f9 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f10 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )

    # etc...

这太复杂了,你应当找到一个能将其简化的方法。

Are the fields all displayed on separate pages? If so, consider converting this to a 10-round game with just one field.

If that’s not possible, then you can reduce the amount of repeated code by defining a function that returns a field:

def make_field(label):
    return models.IntegerField(
        choices=[1,2,3,4,5],
        label=label,
        widget=widgets.RadioSelect,
    )

class Player(BasePlayer):

    q1 = make_field('I am quick to understand things.')
    q2 = make_field('I use difficult words.')
    q3 = make_field('I am full of ideas.')
    q4 = make_field('I have excellent ideas.')

通过使用多轮游戏避免代码重复

如果你有很多几乎完全一样的页面,考虑仅保留一个页面并将游戏重复多轮。你的代码可被简化的一个标志是它看上去像下面这样:

# [pages 1 through 7....]

class Decision8(Page):
    form_model = 'player'
    form_fields = ['decision8']

class Decision9(Page):
    form_model = 'player'
    form_fields = ['decision9']

# etc...

避免重复的验证方法

If you have many repetitive FIELD_error_message methods, you can replace them with a single error_message function. For example:

def quiz1_error_message(player, value):
    if value != 42:
        return 'Wrong answer'

def quiz2_error_message(player, value):
    if value != 'Ottawa':
        return 'Wrong answer'

def quiz3_error_message(player, value):
    if value != 3.14:
        return 'Wrong answer'

def quiz4_error_message(player, value):
    if value != 'George Washington':
        return 'Wrong answer'

You can instead define this function on your page:

@staticmethod
def error_message(player, values):
    solutions = dict(
        quiz1=42,
        quiz2='Ottawa',
        quiz3='3.14',
        quiz4='George Washington'
    )

    error_messages = dict()

    for field_name in solutions:
        if values[field_name] != solutions[field_name]:
            error_messages[field_name] = 'Wrong answer'

    return error_messages

(通常 error_message 被用来返回某一错误信息字符串,但你也可以返回一个字典。)

Avoid duplicated page functions

Any page function can be moved out of the page class, and into a top-level function. This is a handy way to share the same function across multiple pages. For example, let’s say many pages need to have these 2 functions:

class Page1(Page):
    @staticmethod
    def is_displayed(player: Player):
        participant = player.participant

        return participant.expiry

    @staticmethod
    def get_timeout_seconds(player):
        participant = player.participant
        import time
        return participant.expiry - time.time()

You can move those functions before all the pages (remove the @staticmethod), and then reference them wherever they need to be used:

def is_displayed1(player: Player):
    participant = player.participant

    return participant.expiry


def get_timeout_seconds1(player: Player):
    participant = player.participant
    import time

    return participant.expiry - time.time()


class Page1(Page):
    is_displayed = is_displayed1
    get_timeout_seconds = get_timeout_seconds1


class Page2(Page):
    is_displayed = is_displayed1
    get_timeout_seconds = get_timeout_seconds1

(In the sample games, after_all_players_arrive and live_method are frequently defined in this manner.)

提高代码性能

你应当避免不必要的 get_players(), get_player_by_id(), in_*_rounds(), get_others_in_group() 或者任何返回一个玩家或者玩家列表的函数的调用。这些方法都需要数据库查询,而数据库查询是很慢的。

例如,下面的代码有冗余的查询,对同一玩家进行了5次数据库查询:

@staticmethod
def vars_for_template(player):
    return dict(
        a=player.in_round(1).a,
        b=player.in_round(1).b,
        c=player.in_round(1).c,
        d=player.in_round(1).d,
        e=player.in_round(1).e
    )

这应当被简化为:

@staticmethod
def vars_for_template(player):
    round_1_player = player.in_round(1)
    return dict(
        a=round_1_player.a,
        b=round_1_player.b,
        c=round_1_player.c,
        d=round_1_player.d,
        e=round_1_player.e
    )

作为额外的好处,这通常使得代码更具有可读性。

使用BooleanField而不是StringField,如果可能的话

许多 StringFields 应当被转化为 BooleanFields,尤其是其仅有2个不同的值时。

假定你有一个字段叫做 treatment:

treatment = models.StringField()

假定 treatment 有4个不同的可能值:

  • high_income_high_tax
  • high_income_low_tax
  • low_income_high_tax
  • low_income_low_tax

在你的页面中,你可能会这样使用它:

class HighIncome(Page):
    @staticmethod
    def is_displayed(player):
        return player.treatment == 'high_income_high_tax' or player.treatment == 'high_income_low_tax'

class HighTax(Page):
    @staticmethod
    def is_displayed(player):
        return player.treatment == 'high_income_high_tax' or player.treatment == 'low_income_high_tax'

将其化为两个单独的BooleanFields是很好的改进:

high_income = models.BooleanField()
high_tax = models.BooleanField()

那么你的页面代码可被简化为:

class HighIncome(Page):
    @staticmethod
    def is_displayed(player):
        return player.high_income

class HighTax(Page):
    @staticmethod
    def is_displayed(player):
        return player.high_tax