官术网_书友最值得收藏!

Defining composability

The idea of composability is to write code as small and well-defined units that can be combined with other units in flexible and predictable ways. Composable code is decoupled and cohesive. That is, the code has few dependencies and is responsible for a single responsibility. Writing composable code is the key to creating maintainable systems and libraries you'll use for years.

Tip

A unit is a function, class, or method. It is generally the smallest piece of independently useful code, and should be smaller than a module or group of classes (system). There is no black and white definition, so prefer smaller and simpler to larger and more complex.

Maya's utility nodes, such as the MultiplyDivide node, are examples of composable units. They are very clear in their inputs, outputs, and descriptions of what they do. The input to a MultiplyDivide node can be a number from any source, such as a Maya transform, another utility node, or a constant. The MultiplyDivide node does not care, as long as the input is a number. The node just does what it is asked to do—multiply or divide two numbers—and returns the result. It has no undefined behavior, since it will error for any unexpected inputs, and does not rely on system state at all. It relies only on its inputs.

A utility node such as MultiplyDivide, then, can be said to have clear contracts. The idea of contracts is a key to writing composable code and is something we'll explore in greater detail later in this chapter. But first, let's look at an example of non-composable code.

Identifying anti-patterns of composability

In contrast to Maya's utility nodes, the design of MEL and maya.cmds is profoundly non-composable. Let's start by creating a couple of nodes, and viewing them with the ls function as follows:

>>> import maya.cmds as cmds
>>> j1 = cmds.joint()
>>> j2 = cmds.joint()
>>> cmds.ls(type='transform')
[...u'joint1', u'joint2'...]
>>> cmds.ls(exactType='joint')
[u'joint1', u'joint2']

The type='transform' argument returns the joints because joints are a type of transform, as we learned in the last chapter. If our scene had other transforms, they would be part of the results. In contrast, the exactType='joint' argument returns only the joints in the scene, and not other transforms.

We would expect that most listing functions in Maya would support the type and exactType flags, as ls does. And if not, at least we'd expect these flags to work consistently. I have bad news. Behold the listConnections function:

>>> cmds.listConnections(j1, type='transform')
[u'joint2']
>>> cmds.listConnections(j1, exactType='transform')
Traceback (most recent call last):
TypeError: Invalid arguments for flag 'exactType'...

Using a string for the exactType argument in listConnections results in a TypeError. The argument must be a Boolean value, as shown in the following example:

>>> cmds.listConnections(j1, type='joint', exactType=True)
[u'joint2']

We see this unintuitive pattern repeated in multiple places. In this next example, we can see that the listRelatives function doesn't even support the exactType flag!

>>> cmds.listRelatives(j1, type='joint')
[u'joint2']
>>> cmds.listRelatives(j1, exactType='transform')
Traceback (most recent call last):
TypeError: Invalid flag 'exactType'

What do these strange design choices have to do with composability?

Composable functions generally do a single thing. The problematic functions called out previously are combining two behaviors into one function. There is the selection behavior (list relatives, connections, or all objects in the scene) and the filtering behavior (of the selection, choose only objects of a certain type). We can imagine that as this filtering behavior grows more complex, it becomes impossible to keep all selection functions providing filtering in sync.

Tip

It should be clear but it bears pointing out: the term selection here does not refer to selecting an object in Maya. It refers to the computer science term of choosing something.

What would it look like if we broke the filtering out into a distinct function? The following code uses functions that we will build later in this chapter.

>>> import minspect
>>> [o for o in pmc.listConnections(j1)
...  if minspect.is_exact_type(o, 'joint')]
[nt.Joint(u'joint2')]

The listConnections function still handles the selection, but now the is_exact_type function handles the filtering. We combine the two using a list comprehension, which we'll learn about in the List Comprehensions section later in this chapter. As more filtering functionality needs to be provided, nothing except is_exact_type needs to change. Likewise, as listConnections needs to change, it would never conflict with the type filtering behavior.

Avoiding the use of Boolean flags

As a rule of thumb, avoid multiple Boolean flags as function parameters. Multiple Boolean flags indicate problematic code, or what is often called a code smell. While a single flag is usually acceptable, multiple flags become exponentially more difficult to maintain. Once you get past two flags, you should almost always split your function into two or more smaller functions, or rethink how it works.

Note

A code smell is something in the design or implementation of a program that indicates deeper problems. Generally, multiple Boolean flags in a function's signature is a code smell that indicates an overly complex function body.

Additionally, you should never have arguments that are mutually exclusive or interfere with each other, flags or not. The use_stdout argument in the following example has no effect when the stream argument is not None.

>>> import os
>>> import sys
>>> def say(s, use_stdout=True, stream=None):
...     if stream is None:
...         if use_stdout:
...             stream = sys.stdout
...         else:
...             stream = sys.stderr
...     stream.write(s + os.linesep)

The preceding say function may seem contrived but it is not. You could have a function similar to this lurking in your codebase! I would expect that the evolution of the say function went like this:

>>> # Someone needs a simple debug printer to stdout
>>> say('hi')
>>> # Someone needed to print to stderr, so adds a flag.
>>> say('hi', use_stdout=True)
>>> # Someone needed an arbitrary stream, so adds support.
>>> say('hi', use_stdout=True, stream=None)

You should err on the side of not using Boolean flags. It is better to be verbose and pass in what's needed than it is to perform calculations in controlled by flags. You can always wrap a general and verbose function with a function that has a signature better suited to the context in which it is used.

As we've already seen, the teams at Autodesk (and Alias|Wavefront before them) could not keep all of their flags in sync. This isn't because they aren't smart enough or don't work hard enough. It is simply the way software evolves. For comparison, the Unix ls command supports almost 60 options!

Flags lead to users having a hard time remembering how something works. It makes a single function difficult to maintain and extend. It also adds an insurmountable burden to systems, such as Maya's listing functions, that should have consistency.

Due to MEL's limitations as a language, flags were probably a good decision at the time. But we are using the much more powerful language of Python and should take advantage of it by writing composable code and avoiding the use of Boolean flags.

Evolving legacy code into composable code

Have you ever seen code like the following?

>>> def get_all_root_joints():
...     roots = []
...     for jnt in pmc.ls(type='joint'):
...         if jnt.getParent() is None:
...             roots.append(jnt)
...     return roots
>>> get_all_root_joints()
[nt.Joint(u'joint1')]

The get_all_root_joints function returns a list of all joints in the scene that have no parent. There are a number of issues with the preceding function, but in general we can say it lacks composability.

Perhaps this function is already somewhere in your codebase. Now you have a new use case for this root-finding behavior: a user merges a Maya scene into one with existing skeletons, and you want to get only the skeleton roots from the new scene. Unfortunately, you cannot use the same logic inside get_all_root_joints unless you copy and paste it (which you shouldn't do, and won't have to do after reading this chapter). The get_all_root_joints function has a limited design that makes re-using it for new purposes impossible.

In what ways is it limited? Just like the listRelatives and previously discussed examples, we've combined selection and filtering in the same function. For example, the ls command has both selection (select all objects) and filtering (keep only joints), and the next line also filters (keep only objects without a parent). We also have several lines of boilerplate for creating the list, appending to the list, and returning it.

Wouldn't it be great if we could write our code in such a way so that it's more easily re-usable?

Rewriting code for composability

Rewriting existing code for composability usually involves splitting up a larger function into smaller pieces, each responsible for a single task. In the case of get_all_root_joints, we will just create a function to filter root joints.

>>> def is_root_joint(obj):
...     return obj.type() == 'joint' and obj.getParent() is None
>>> all_roots = [o for o in pmc.ls() if is_root_joint(o)]
>>> new_roots = [o for o in pmc.importFile(some_file_path)
...             if is_root_joint(o)]

We've pulled all of our filtering logic into a simple predicate, the is_root_joint function. A predicate is a fancy name for a function that returns True or False. We combine that predicate with a selection (ls or importFile) and suddenly the root-finding logic is usable everywhere.

Let's take this splitting of selection and filtering further.

Getting the first item in a sequence

It's very common to find code to select the first item in a sequence, unless the sequence is empty, in which case we select None. To do this, we create the sequence, assign our result variable a default value of None, check if the sequence contains items, and if so, assign the result variable to be the first item in the sequence.

The following code illustrates this process:

>>> all_roots = [o for o in pmc.ls() if is_root_joint(o)]
>>> first_root = None
>>> if all_roots:
...     first_root = all_roots[0]
>>> first_root
nt.Joint(u'joint1')

There are several problems with this implementation. First, accessing all_roots[0] only works if all_roots is indexable with an integer. Examples of compatible types are lists and tuples. However, there are many sequences in Python that are not indexable. We won't go over them now (they include sets and generators), so you'll just have to take my word for it. This pattern will not work for those types, so the common workaround is to cast the object into a list before indexing. Yuck!

The second problem (and more important for our current purposes) is that the preceding code involves a lot of boilerplate. Boilerplate is code that must be included with little modification. We use four lines for a single expression: return a value that is the first item in a sequence or a default if the sequence is empty. Instead of being happy with boilerplate, we should write a function to remove it, as we do in the following code:

>>> def first_or_default(sequence, default=None):
...     for item in sequence:
...         return item  # Return the first item
...     return default  # Return default if no items

Now our code to select the first root joint looks like this:

>>> first_root = first_or_default(
...     o for o in pmc.ls() if is_root_joint(o))
>>> first_root
nt.Joint(u'joint1')

Can we do one better? What if we combine the is_root_joint predicate with our first_or_default logic? Let's add a predicate parameter to the first_or_default function.

>>> def first_or_default(sequence, predicate=None, default=None):
...     for item in sequence:
...         if predicate is None or predicate(item):
...             return item
...     return default

Now our code looks as follows:

>>> first_root = first_or_default(pmc.ls(), is_root_joint)
>>> first_root
nt.Joint(u'joint1')

Pretty neat! We've taken what normally shows up as several lines of boilerplate imperative code, and broken it down into two totally reusable functions.

One of the benefits of developing this way is that our code is not tied to Maya. There is nothing specific to Maya in the first_or_default function. This means that your code is totally reusable, testable, and much more easy to develop. In fact, I always develop functions like this completely outside Maya.

>>> first_or_default([1, 2, 3])
1
>>> first_or_default([], default='hi!')
'hi!'

We're well on our way to writing composable code!

Writing head and tail functions

Another example of argument-driven behavior that is better served as distinct functions is the head and tail parameters in the ls function. The head argument specifies how many items to return from the start of the list, and the tail argument specifies the opposite. We can see the two in action in the following example:

>>> pmc.ls(type='joint', head=2)
[nt.Joint(u'joint1'), nt.Joint(u'joint2')]
>>> pmc.ls(type='joint', tail=2)
[nt.Joint(u'joint2'), nt.Joint(u'joint3')]

Instead of using arguments to control this behavior, it can be rewritten as head and tail functions, as shown in the following example;

>>> def head(sequence, count):
...     result = []
...     for item in sequence:
...         if len(result) == count:
...             break
...         result.append(item)
...     return result
>>> head(pmc.ls(type='joint'), 2)
[nt.Joint(u'joint1'), nt.Joint(u'joint2')]

>>> def tail(sequence, count):
...     result = list(sequence)
...     return result[-count:]
>>> tail(pmc.ls(type='joint'), 2)
[nt.Joint(u'joint2'), nt.Joint(u'joint3')]

The new head and tail functions can be re-used for any collection, do not depend on Maya, and do not have the special behavior of the ls arguments.

主站蜘蛛池模板: 章丘市| 景宁| 柳河县| 周至县| 治多县| 马尔康县| 建始县| 延津县| 陈巴尔虎旗| 武宁县| 肃南| 浏阳市| 鄯善县| 股票| 全椒县| 镇原县| 南康市| 柳州市| 乌拉特前旗| 青岛市| 临夏市| 松滋市| 三穗县| 葵青区| 建始县| 清水河县| 道真| 霸州市| 吴旗县| 新龙县| 栾川县| 奈曼旗| 泰和县| 松桃| 茌平县| 云浮市| 邯郸市| 三江| 沁阳市| 洪洞县| 梅州市|