CRC Cards as training material

CRC Card: Class name, Responsibilities, Collaborators.

CRC Cards are a teaching tool on how to design software. They were proposed by Kent Beck and Ward Cunningham, and, hell yeah it's useful.

Component name


  1. First responsability
  2. second responsability

  1. First collaborator
  2. second collaborator

When I'm talking with tech leaders, my goal is to sharpen our vision about the software we are working on. When developers implemente features, I often see responsability leaks: the feature works, but the component are hard to read, hard to reuse and we pile up technical debt too quickly.

CRC cards are a great tool to focus the discussion and to aknowledge the fact that the developer shared her global vision to each individual local component who must rely only on its local scope. Let's take an example of one discussion.

The horrible UserBookmarks component 😨

We're about to look at a very ugly code that is definetely doing too many things. The purpose of the discussion I have is to show to the tech lead that we really want to prevent this to happen and it is her mission to standardize it with her team.

As the code does too many things, it can be hard to follow this post, but, I'll try my best to take you with me on this journey.

Here is the UserBookmarks component:

interface Props {
  user: User
}

export const UserBookmarks: FunctionComponent<Props> = ({ user }) => {
  const [showAddBookmarkModal, setShowAddBookmarkModal] = useState(false)
  const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    setIsLoading(true)

    try {
      const userBookmark = await fetch(`/users/${user.id}/bookmarks`, {
        method: 'GET'
      })
      setBookmarks(userBookmarks)
    } catch (error) {
      setBookmarks([])
    } finally {
      setIsLoading(false)
    }
  }, [])

  const addBookmarkToUser = async ({ bookmark }) => {
    setIsLoading(true)
    try {
      const newBookmark = await fetch(`/users/${user.id}/bookmarks`, {
        method: 'POST',
        body: JSON.stringify({ bookmark })
      })
      setBookmarks([...bookmarks, newBookmark])
    } catch (error) {
      console.warn(error);
    } finally {
      setIsLoading(false)
    }
  }

  const tilesAnimation = gsap.to({
    duration: 0.8,
    opacity: 0.35,
    yoyo: true,
    repeat: -1,
    stagger: 0.025,
  })

  return <div className="user-bookmarks">
    <Title title={i18n('bookmark.user_bookmarks')} />
    {isLoading
      ? <TilesSkeleton animation={tilesAnimation} numberOfTiles={16} />
      : <Bookmarks data={bookmarks} />}

    <PrimaryButton
      text={i18n('bookmark.add_bookmark')}
      onClick={() => setShowAddBookmarkModal(true))}
      image={<FontAwesomeIcon icon={['fas', 'plus']} color="white" />}
    />

    <AddBookmarkModal
      visible={showAddBookmarkModal}
      onClose={() => setShowAddBookmarkModal(false)}
      bookmarks={bookmarks}
      onBookmarkAdd={addBookmarkToUser}
    />
  </div>
}

It's a reeeeeeally long component that does many things. Most of the time, the tech lead freezes seeing how many things we need to talk to. Let's break it down piece by piece.

The name

The easiest part: it has a name, maybe it's not perfect but UserBookmarks make sense for me. Let's create its CRC card knowing that the only code calling it is the main page.

UserBookmarks



    1. Home.tsx

    That's a start!

    Inputs and output

    The UserBookmarks component takes a user prop and return a list of styled bookmarks with the possibility to add bookmark via a modal. I'll say something positive about this component, it's that it is pretty convenient for the parent calling it, just pass it the user, it'll give you all the user bookmarks and more. Maybe too much.

    But hey! We can already note the first responsability on our CRC card.

    UserBookmarks


    1. Display user bookmarks

    1. Home.tsx

    As we are talking about inputs, I want to know how the props are used. Are they necessary? Are they sufficient? Here we can see that user is only used for fetching data:

    // ...
    const userBookmark = await fetch(`/users/${user.id}/bookmarks`, {
    // ...
    const newBookmark = await fetch(`/users/${user.id}/bookmarks`, {
    // ...

    Only the user id is necessary, why not just give the user id instead of the whole object? That will be our first simplification .

    ℹ️ We'll simplify at the very end, once we'll be finished with our CRC card.

    ℹ️ Here is a classic responsibility leak I frequentely see. When you're developing a feature, you have the big picture but UserBookmarks and its local knowledge don't care about the user having a user.address.line1 property.

    The secret sauce

    Now comes where we'll challenge how the component does its magic.

    "Tell me about your hooks, I'll tell you who you are"

    Without being a rule of thumb, I like to count how many use there are in the component, it gives me good approximation about how many responsibilities lies.

    1. const [showAddBookmarkModal, setShowAddBookmarkModal] = useState(false)
    2. const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
    3. const [isLoading, setIsLoading] = useState(false)
    4. useEffect(() => {

    4 hooks, is it too many? too few? I guess it depends but here we can see there are 3 hooks managing fetching the data and one handling the modal for adding a bookmark. We can update our CRC Card.

    UserBookmarks


    1. Display user bookmarks
    2. fetching bookmarks
    3. handling async processes (loading and error state)
    4. display 'add bookmark' button
    5. handling 'add bookmark' modal visibility

    1. Home.tsx

    The ugly responsability

    The main problem I see in the UserBookmarks component is not about the component itself, it's more on one of its children: TilesSkeleton. It's doing a good job for the user experience but it's asking too much for the parent to be able to use it: UserBookmarks doesn't care about animating the skeleton.

    const tilesAnimation = gsap.to({
      duration: 0.8,
      opacity: 0.35,
      yoyo: true,
      repeat: -1,
      stagger: 0.025,
    })
    // ...
    <TilesSkeleton animation={tilesAnimation} numberOfTiles={16} />

    UserBookmarks


    1. Display user bookmarks
    2. fetching bookmarks
    3. handling async processes (loading, error)
    4. display 'add bookmark' button
    5. handling 'add bookmark' modal visibility
    6. Defining TilesSkeleton animation

    1. Home.tsx

    What do we keep?

    Now that we listed the responsabilities, it is time to know what do we want to keep by defining the main responsability of the UserBookmarks in one and sentence.

    UserBookmarks retrieve and handle the user bookmarks with an additional button to add it.

    That's it. Having a simple sentence defining what's the component does is the purpose of this exercice. Definetely difficult when we first read it, it is now a pretty concise and acceptable responsability we want UserBookmarks to have.

    Let's display the responsabilities that doesn't quite respect the definition.

    UserBookmarks


    1. Display user bookmarks
    2. ~ fetching bookmarks
    3. handling async processes (loading, error)
    4. display 'add bookmark' button
    5. ⚠️ handling 'add bookmark' modal visibility
    6. ⚠️ Defining TilesSkeleton animation

    1. Home.tsx

    Simplification

    Once we said all that, let's relook our component: less responsability and less hooks.

    Has only necessary prop

    interface Props {
      userId: number
    }
    
    export const UserBookmarks: FunctionComponent<Props> = ({ userId }) => {
    // ...
          const userBookmark = await fetch(`/users/${userId}/bookmarks`, {
            method: 'GET'
          })
    // ...
          const newBookmark = await fetch(`/users/${userId}/bookmarks`, {
            method: 'POST',
            body: JSON.stringify({ bookmark })
          })
    // ...

    Is not responsible of child's animation

    // removing the magic number at the same time
    <TilesSkeleton numberOfTiles={MAXIMUM_BOOKMARKS} />

    Does not fetch itself user bookmarks

    // ...
    const { bookmarks, refetch, isLoading, error } = useUserBookmarksQuery({
      userId,
    })
    // ...

    Does not handle AddBookmark modal

    Here we only remove code

    The final version

    interface Props {
      user: User
    }
    
    export const UserBookmarks: FunctionComponent<Props> = ({ user }) => {
      const { bookmarks, refetch, isLoading, error } = useUserBookmarksQuery({
        userId,
      })
    
      return <div className="user-bookmarks">
        <Title title={i18n('bookmark.user_bookmarks')} />
        {isError
          ? <ErrorMessage>An error occured</ErrorMessage>
          : isLoading
          ? <TilesSkeleton numberOfTiles={16} />
          : <Bookmarks bookmarks={bookmarks} />}
    
        <AddBookmarkButton onNewBookmark={() => refetch())} />
      </div>
    }

    And here its final CRC Card:

    UserBookmarks


    1. Retrieve bookmarks
    2. handling async processes (loading, error)
    3. display user bookmarks
    4. display 'add bookmark' button

    1. Home.tsx
    2. Title
    3. ErrorMessage
    4. useUserBookmarksQuery
    5. Bookmarks
    6. AddBookmarkButton

    Final thought

    What a cleanup! Looks like UserBookmarks does not do anything at all and delegate everything as there are more collaborators now!

    That's the goal, delegation is for me underrated, we tend to extend too easily existing files instead of creating new files with clear responsability. Now I'm pretty sure what I'll encounter on the useUserBookmarksQuery, TilesSkeleton, Bookmarks and AddBookmarkButton and if I need to specify a new prop coming from the user to display bookmarks, for instance has the user's authorization to add bookmark? I know where I'll start: UserBookmarks.

    Playground

    Do you want to create your own CRC cards?

    Take a look at the CRC card playground!