One place for hosting & domains

      Design

      12 Navigation Menu Design Tips for a Better User Experience


      While creating attractive and valuable web pages is important, your efforts can be wasted if they are unorganized. This could make it difficult for users to view and interact with your content, leading to bounces (page exits) and potentially lower search engine rankings.

      Fortunately, you can design the perfect navigation menu to help users quickly find the pages they’re looking for. With many styles and formats to choose from, you’re able to create menus that impress visitors and deliver an excellent User Experience (UX).

      In this post, we’ll introduce you to navigation menus. Then, we’ll explore twelve useful tips for designing your menus as well as share some examples to inspire you.

      Ready? Let’s get started!

      An Introduction to Navigation Menus

      Navigation menus display an organized list of all your web pages from one dedicated area. Typically, they appear across headers or sidebars, so that they’re clearly visible and accessible for your website visitors.

      Menus enable users to navigate around your site more easily, but they also help them to make sense of your content. For instance, by viewing your menu, users can better understand the relationships between your web pages:

      mega menu dropdown example

      When setting up your navigation menu, you might consider featuring submenus or local navigation menus within your overarching main menu. Then, you can add lower levels of categories to your navigation if you have lots of content on your site.

      12 Tips for Designing the Perfect Navigation Menu

      Now that you know how helpful navigation menus can be, let’s take a look at twelve useful tips for designing one.

      1. Prioritize Accessibility

      A well–designed website is one where users don’t have to work hard to find what they’re looking for. Meaning, when a visitor lands on your page, they should be able to quickly locate your menu and understand how to use it:

      stylized dropdown navigation menu

      Although you can be creative, it’s important to prioritize designing an accessible website. Therefore, try to avoid vague or complex labels that might confuse readers. Instead, opt for clear fonts, high-contrast colors, and direct language.

       

      2. Optimize the User Experience (UX) 

      Providing a quality UX can boost your conversions and reduce bounce rate. To optimize your UX, aim to keep your menu simple so users don’t have to get to grips with complex systems. There’s a lot to be said for neat, clean designs that allow visitors to breeze through your website.

      It’s a general rule of thumb that in three clicks or less, people should be able to land where they want to be on your site. That’s why websites with lots of content areas often choose mega menus:

      mega menu navigation menu design

      These mega menus are frequently used by large e-commerce stores since they make all pages accessible from one space.

      Another factor that can impact your UX is your hosting provider. DreamHost provides quality shared hosting that can set you up with customizable themes and must-have plugins for all types of websites. We also offer user-friendly interfaces plus regular updates and around-the-clock support.

      3. Stick with Straightforward Designs

      You might be tempted to fill your menus with lots of effects to impress your visitors. However, consider saving the flashy features for your overall web design. Still, you might like to include images if it assists with your navigation goals:

      navigation menu active and hover state design example

      Another option is to utilize relevant, helpful icons such as directional arrows to guide users through your sections.

      4. Appeal to Your Audience     

      You can’t design the perfect navigation menu without considering your unique target audience. With this in mind, you can choose color schemes, typefaces, and call-to-actions (CTAs) that are more likely to appeal to your market. This can make your links appear more clickable.

      For example, a hard news website is unlikely to use the same font and messaging as a quirky baking blog:

      split navigation menu design with logo at center

      When choosing headings or CTAs to feature in your menu, you’ll want to inspire users to act. Essentially, visitors need to be incentivized to read further or discover more of your content.

      5. Be Consistent

      It’s important that the format and design of your menu meets your visitors’ expectations. So, consider using the same styling options to highlight menu items. This way, users know when a link will take them to a new page or expand into a dropdown menu.

      For example, Benefit’s website uses directional arrows beside links that expand into dropdown menus:

      simple mega menu with call-to-action

      Additionally, it can be helpful to distinguish between primary and secondary headings. You might want to do this by making top-level menu items slightly larger, or applying a bold style to indicate more significance.

      6. Organize Appropriately 

      A navigation menu is an ideal way to organize your web pages. Plus, it enables users to view your content in a way that makes sense. For instance, blogs can organize posts by topics while an e-commerce website might group products by categories:

      multi-level dropdown menu design example

      Once you’ve identified the main categories of your content, you can build your navigation menu around this. It’s also useful to choose relevant headings that properly describe the page.

      7. Establish a Clear Hierarchy

      Implementing a hierarchy within your menu enables you to break content up into smaller chunks. This makes it more digestible for users. As such, try to group relevant information together.

      For some websites, it can be useful to organize information according to what is most popular or important to visitors. Then, you can make these headings stand out within your menu. Strive to achieve a balance between showing users pages of interest while also leading them towards pages that best serve your business goals.

      Get Content Delivered Straight to Your Inbox

      Subscribe to our blog and receive great content just like this delivered straight to your inbox.

      8. Consider the Mobile Experience

      A responsive menu will display attractively across different size screens such as smartphones and tablets. This is important since nearly 60% of total global traffic comes from mobile phones.

      Most websites tend to opt for hamburger menus for mobile devices:

      mobile nav menu design

      Failing to build a responsive website is arguably one of the biggest mistakes you can make when it comes to web design. Therefore, as you are creating your menu, consider which links are most important to include in your primary menu as this is what will be seen on smaller screens.

      9. Use Familiar Web Conventions

      Designing your menu with unfamiliar conventions might require users to learn new practices, which can be inconvenient and annoying, so you’ll want to avoid this. For instance, most users are accustomed to clicking on the website logo to return to the homepage.

      If your logo leads to a signup or product page, this may confuse your visitors. Another common convention is ‘visited’ links changing color. Including these well-known practices on your website enables users to intuitively navigate your pages.

      10. Optimize for Search Engines

      In order to drive more organic traffic to your website, you can optimize your navigation headings with popular keywords. Google Analytics and Google Keyword Planner are excellent tools that enable you to identify which words and phrases users are searching:

      getting keyword ideas in Google Keyword Planner

      Then, you can include these key terms within your menu. As a result, your website may just rank higher in search engines.

      11. Choose the Right Type of Menu

      There are many types of navigation menus to consider. Dropdown menus often display when you hover or click primary categories. Then, you’re presented with a list of secondary items.

      These menus look stylish and modern. Plus, they’re a great way to conserve space:

      simple dropdown navigation menu design

      You can go a step further and design a complete mega menu. These are best used for content-rich sites, since they can present all your pages without appearing too clunky:

      menu menu navigation design example

      Horizontal menus, which list major pages in a row format, are also quite common. Alternatively, a vertical menu, listed in a column at the side of the page, assists readers with scanning, since eyes naturally move down (not across):

      stylized sidebar navigation menu design

      Vertical menus tend to be a good match for websites with longer menu labels since they offer more space. However, they can also be eye-catching, which makes them a good choice for creative service sites.

      12. Add Breadcrumbs

      Breadcrumbs enable users to see where they are within your site’s structure. Plus, they make it easy for visitors to return to high-tier pages that led them to their current location:

      sidebar navigation menu design example

      Adding breadcrumbs to your menu avoids users needing to navigate all the way back to the beginning. Instead, they can easily jump back a step or two to find what they need.

      Excellent Examples of Navigation Menus 

      Now that you know how to design the perfect menu for your site, let’s take a look at some examples.

      Mostly Serious

      Mostly Serious features a clear hamburger icon to make room for a fun animation:

      off canvas menu design default state

      When you click on the icon, it opens a vertical sidebar menu with only the primary headings displayed:

      off canvas menu design expanded active state

      Once you start scrolling past the animation, you’ll see a sticky horizontal menu that’s neat and accessible, without distracting from the experience of reading the page:

      fixed nav bar example

      In this example, each type of menu is used appropriately. On top of that, when you hover over menu items, all navigational links are highlighted in bright blue and underlined for consistency.

      Bobbi Brown

      The Bobbi Brown website features a primary horizontal menu nestled beneath the heading. This makes it one of the first things you see when you land on the page.

      Each of the main menu items features its own dropdown menu that includes text links among high-quality images, which make the menu more engaging:

      simple mega menu with images

      Additionally, the menu is organized effectively, with the most important categories appearing first such as New and Bestsellers. Even within the dropdown menus, image links prioritize the most useful customer pages, while other areas of the site are stacked vertically at the side.

      This is Amber

      This is Amber features a quirky off-canvas flyout menu in the form of tabs that expand when clicked. Then, the selected page slides across, replacing the existing page you’re viewing:

      horizontal navigation menu design

      It’s an incredibly unique way of displaying menu items. Plus, it does a great job of building a brand identity. Visitors can also access the primary links through a horizontal header menu at the top of the page.

      Blackbird Cigar

      Blackbird Cigar uses a hamburger menu, which opens a vertical menu when clicked. This is styled like a dropdown menu although links open across instead of down:

      nested sidebar navigation menu design

      Moreover, the menu features a stylish design that conveys a clear hierarchy, enabling visitors to understand the relationship between pages. For example, when visitors hover over primary links, they turn transparent, while secondary links are distinguished from top-tier pages using contrasting colors.

      French but Nice

      French but Nice is a portfolio website that uses a captivating vertical sidebar menu that organizes projects chronologically:

      brutalist sidebar navigation design

      When a user hovers over one of the links, a preview of the page appears in a lightbox. This is particularly useful for a website of this kind, since visitors can view multiple projects without leaving the menu.

      Create the Perfect Navigation Menu

      A navigation menu is a necessary part of any website, so it’s important to make sure that yours is user-friendly and effective. Otherwise, your content can be difficult to find and hard to make sense of.

      However, when you follow a few (or all) of our top tips, you’ll be able to more easily design the perfect navigation menu. For instance, you might choose a hamburger menu so that your pages can be viewed on mobile devices. Or, you could utilize strong colors, fonts, and images to make your links more clickable.

      At DreamHost, we understand the importance of getting your content online quickly. That’s why we offer Shared Hosting with SSL certificates, a domain, and privacy protection to get you set up in no time. Choose a plan today to get started!

      Great Design, Powered by DreamHost

      We make sure your website is fast, secure and always up so your visitors trust you. Plans start at $1.99/mo.

      shared hosting



      Source link

      How To Design a Document Schema in MongoDB


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      If you have a lot of experience working with relational databases, it can be difficult to move past the principles of the relational model, such as thinking in terms of tables and relationships. Document-oriented databases like MongoDB make it possible to break free from rigidity and limitations of the relational model. However, the flexibility and freedom that comes with being able to store self-descriptive documents in the database can lead to other pitfalls and difficulties.

      This conceptual article outlines five common guidelines related to schema design in a document-oriented database and highlights various considerations one should make when modeling relationships between data. It will also walk through several strategies one can employ to model such relationships, including embedding documents within arrays and using child and parent references, as well as when these strategies would be most appropriate to use.

      Guideline 1 — Storing Together What Needs to be Accessed Together

      In a typical relational database, data is kept in tables, and each table is constructed with a fixed list of columns representing various attributes that make up an entity, object, or event. For example, in a table representing students at a a university, you might find columns holding each student’s first name, last name, date of birth, and a unique identification number.

      Typically, each table represents a single subject. If you wanted to store information about a student’s current studies, scholarships, or prior education, it could make sense to keep that data in a separate table from the one holding their personal information. You could then connect these tables to signify that there is a relationship between the data in each one, indicating that the information they contain has a meaningful connection.

      For instance, a table describing each student’s scholarship status could refer to students by their student ID number, but it would not store the student’s name or address directly, avoiding data duplication. In such a case, to retrieve information about any student with all information on the student’s social media accounts, prior education, and scholarships, a query would need to access more than one table at a time and then compile the results from different tables into one.

      This method of describing relationships through references is known as a normalized data model. Storing data this way — using multiple separate, concise objects related to each other — is also possible in document-oriented databases. However, the flexibility of the document model and the freedom it gives to store embedded documents and arrays within a single document means that you can model data differently than you might in a relational database.

      The underlying concept for modeling data in a document-oriented database is to “store together what will be accessed together.”“ Digging further into the student example, say that most students at this school have more than one email address. Because of this, the university wants the ability to store multiple email addresses with each student’s contact information.

      In a case like this, an example document could have a structure like the following:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "emails": [
              {
                  "email": "sammy@digitalocean.com",
                  "type": "work"
              },
              {
                  "email": "sammy@example.com",
                  "type": "home"
              }
          ]
      }
      

      Notice that this example document contains an embedded list of email addresses.

      Representing more than a single subject inside a single document characterizes a denormalized data model. It allows applications to retrieve and manipulate all the relevant data for a given object (here, a student) in one go without a need to access multiple separate objects and collections. Doing so also guarantees the atomicity of operations on such a document without having to use multi-document transactions to guarantee integrity.

      Storing together what needs to be accessed together using embedded documents is often the optimal way to represent data in a document-oriented database. In the following guidelines, you’ll learn how different relationships between objects, such as one-to-one or one-to-many relationships, can be best modeled in a document-oriented database.

      Guideline 2 — Modeling One-to-One Relationships with Embedded Documents

      A one-to-one relationship represents an association between two distinct objects where one object is connected with exactly one of another kind.

      Continuing with the student example from the previous section, each student has only one valid student ID card at any given point in time. One card never belongs to multiple students, and no student can have multiple identification cards. If you were to store all this data in a relational database, it would likely make sense to model the relationship between students and their ID cards by storing the student records and the ID card records in separate tables that are tied together through references.

      One common method for representing such relationships in a document database is by using embedded documents. As an example, the following document describes a student named Sammy and their student ID card:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "id_card": {
              "number": "123-1234-123",
              "issued_on": ISODate("2020-01-23"),
              "expires_on": ISODate("2020-01-23")
          }
      }
      

      Notice that instead of a single value, this example document’s id_card field holds an embedded document representing the student’s identification card, described by an ID number, the card’s date of issue, and the card’s expiration date. The identity card essentially becomes a part of the document describing the student Sammy, even though it’s a separate object in real life. Usually, structuring the document schema like this so that you can retrieve all related information through a single query is a sound choice.

      Things become less straightforward if you encounter relationships connecting one object of a kind with many objects of another type, such as a student’s email addresses, the courses they attend, or the messages they post on the student council’s message board. In the next few guidelines, you’ll use these data examples to learn different approaches for working with one-to-many and many-to-many relationships.

      Guideline 3 — Modeling One-to-Few Relationships with Embedded Documents

      When an object of one type is related to multiple objects of another type, it can be described as a one-to-many relationship. A student can have multiple email addresses, a car can have numerous parts, or a shopping order can consist of multiple items. Each of these examples represents a one-to-many relationship.

      While the most common way to represent a one-to-one relationship in a document database is through an embedded document, there are several ways to model one-to-many relationships in a document schema. When considering your options for how to best model these, though, there are three properties of the given relationship you should consider:

      • Cardinality: Cardinality is the measure of the number of individual elements in a given set. For example, if a class has 30 students, you could say that class has a cardinality of 30. In a one-to-many relationship, the cardinality can be different in each case. A student could have one email address or multiple. They could be registered for just a few classes or they could have a completely full schedule. In a one-to-many relationship, the size of “many” will affect how you might model the data.
      • Independent access: Some related data will rarely, if ever, be accessed separately from the main object. For example, it might be uncommon to retrieve a single student’s email address without other student details. On the other hand, a university’s courses might need to be accessed and updated individually, regardless of the student or students that are registered to attend them. Whether or not you will ever access a related document alone will also affect how you might model the data.
      • Whether the relationship between data is strictly a one-to-many relationship: Consider the courses an example student attends at a university. From the student’s perspective, they can participate in multiple courses. On the surface, this may seem like a one-to-many relationship. However, university courses are rarely attended by a single student; more often, multiple students will attend the same class. In cases like this, the relationship in question is not really a one-to-many relationship, but a many-to-many relationship, and thus you’d take a different approach to model this relationship than you would a one-to-many relationship.

      Imagine you’re deciding how to store student email addresses. Each student can have multiple email addresses, such as one for work, one for personal use, and one provided by the university. A document representing a single email address might take a form like this:

      {
          "email": "sammy@digitalocean.com",
          "type": "work"
      }
      

      In terms of cardinality, there will be only a few email addresses for each student, since it’s unlikely that a student will have dozens — let alone hundreds — of email addresses. Thus, this relationship can be characterized as a one-to-few relationship, which is a compelling reason to embed email addresses directly into the student document and store them together. You don’t run any risk that the list of email addresses will grow indefinitely, which would make the document big and inefficient to use.

      Note: Be aware that there are certain pitfalls associated with storing data in arrays. For instance, a single MongoDB document cannot exceed 16MB in size. While it is possible and common to embed multiple documents using array fields, if the list of objects grows uncontrollably the document could quickly reach this size limit. Additionally, storing a large amount of data inside embedded arrays have a big impact on query performance.

      Embedding multiple documents in an array field will likely be suitable in many situations, but know that it also may not always be the best solution.

      Regarding independent access, email addresses will likely not be accessed separately from the student. As such, there is no clear incentive to store them as separate documents in a separate collection. This is another compelling reason to embed them inside the student’s document.

      The last thing to consider is whether this relationship is really a one-to-many relationship instead of a many-to-many relationship. Because an email address belongs to a single person, it’s reasonable to describe this relationship as a one-to-many relationship (or, perhaps more accurately, a one-to-few relationship) instead of a many-to-many relationship.

      These three assumptions suggest that embedding students’ various email addresses within the same documents that describe students themselves would be a good choice for storing this kind of data. A sample student’s document with email addresses embedded might take this shape:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "emails": [
              {
                  "email": "sammy@digitalocean.com",
                  "type": "work"
              },
              {
                  "email": "sammy@example.com",
                  "type": "home"
              }
          ]
      }
      

      Using this structure, every time you retrieve a student’s document you will also retrieve the embedded email addresses in the same read operation.

      If you model a relationship of the one-to-few variety, where the related documents do not need to be accessed independently, embedding documents directly like this is usually desirable, as this can reduce the complexity of the schema.

      As mentioned previously, though, embedding documents like this isn’t always the optimal solution. The next section provides more details on why this might be the case in some scenarios, and outlines how to use child references as an alternative way to represent relationships in a document database.

      Guideline 4 — Modeling One-to-Many and Many-to-Many Relationships with Child References

      The nature of the relationship between students and their email addresses informed how that relationship could best be modeled in a document database. There are some differences between this and the relationship between students and the courses they attend, so the way you model the relationships between students and their courses will be different as well.

      A document describing a single course that a student attends could follow a structure like this:

      {
          "name": "Physics 101",
          "department": "Department of Physics",
          "points": 7
      }
      

      Say that you decided from the outset to use embedded documents to store information about each students’ courses, as in this example:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "emails": [
              {
                  "email": "sammy@digitalocean.com",
                  "type": "work"
              },
              {
                  "email": "sammy@example.com",
                  "type": "home"
              }
          ],
          "courses": [
              {
                  "name": "Physics 101",
                  "department": "Department of Physics",
                  "points": 7
              },
              {
                  "name": "Introduction to Cloud Computing",
                  "department": "Department of Computer Science",
                  "points": 4
              }
          ]
      }
      

      This would be a perfectly valid MongoDB document and could well serve the purpose, but consider the three relationship properties you learned about in the previous guideline.

      The first one is cardinality. A student will likely only maintain a few email addresses, but they can attend multiple courses during their study. After several years of attendance, there could be dozens of courses the student took part in. Plus, they’d attend these courses along with many other students who are likewise attending their own set of courses over their years of attendance.

      If you decided to embed each course like the previous example, the student’s document would quickly get unwieldy. With a higher cardinality, the embedded document approach becomes less compelling.

      The second consideration is independent access. Unlike email addresses, it’s sound to assume there would be cases in which information about university courses would need to be retrieved on their own. For instance, say someone needs information about available courses to prepare a marketing brochure. Additionally, courses will likely need to be updated over time: the professor teaching the course might change, its schedule may fluctuate, or its prerequisites might need to be updated.

      If you were to store the courses as documents embedded within student documents, retrieving the list of all the courses offered by the university would become troublesome. Also, each time a course needs an update, you would need to go through all student records and update the course information everywhere. Both are good reasons to store courses separately and not embed them fully.

      The third thing to consider is whether the relationship between student and a university course is actually one-to-many or instead many-to-many. In this case, it’s the latter, as more than one student can attend each course. This relationship’s cardinality and independent access aspects suggest against embedding each course document, primarily for practical reasons like ease of access and update. Considering the many-to-many nature of the relationship between courses and students, it might make sense to store course documents in a separate collection with unique identifiers of their own.

      The documents representing classes in this separate collection might have a structure like these examples:

      {
          "_id": ObjectId("61741c9cbc9ec583c836170a"),
          "name": "Physics 101",
          "department": "Department of Physics",
          "points": 7
      },
      {
          "_id": ObjectId("61741c9cbc9ec583c836170b"),
          "name": "Introduction to Cloud Computing",
          "department": "Department of Computer Science",
          "points": 4
      }
      

      If you decide to store course information like this, you’ll need to find a way to connect students with these courses so that you will know which students attend which courses. In cases like this where the number of related objects isn’t excessively large, especially with many-to-many relationships, one common way of doing this is to use child references.

      With child references, a student’s document will reference the object identifiers of the courses that the student attends in an embedded array, as in this example:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "emails": [
              {
                  "email": "sammy@digitalocean.com",
                  "type": "work"
              },
              {
                  "email": "sammy@example.com",
                  "type": "home"
              }
          ],
          "courses": [
              ObjectId("61741c9cbc9ec583c836170a"),
              ObjectId("61741c9cbc9ec583c836170b")
          ]
      }
      

      Notice that this example document still has a courses field which also is an array, but instead of embedding full course documents like in the earlier example, only the identifiers referencing the course documents in the separate collection are embedded. Now, when retrieving a student document, courses will not be immediately available and will need to be queried separately. On the other hand, it’s immediately known which courses to retrieve. Also, in case any course’s details need to be updated, only the course document itself needs to be altered. All references between students and their courses will remain valid.

      Note: There is no firm rule for when the cardinality of a relation is too great to embed child references in this manner. You might choose a different approach at either a lower or higher cardinality if it’s what best suits the application in question. After all, you will always want to structure your data to suit the manner in which your application queries and updates it.

      If you model a one-to-many relationship where the amount of related documents is within reasonable bounds and related documents need to be accessed independently, favor storing the related documents separately and embedding child references to connect to them.

      Now that you’ve learned how to use child references to signify relationships between different types of data, this guide will outline an inverse concept: parent references.

      Guideline 5 — Modeling Unbounded One-to-Many Relationships with Parent References

      Using child references works well when there are too many related objects to embed them directly inside the parent document, but the amount is still within known bounds. However, there are cases when the number of associated documents might be unbounded and will continue to grow with time.

      As an example, imagine that the university’s student council has a message board where any student can post whatever messages they want, including questions about courses, travel stories, job postings, study materials, or just a free chat. A sample message in this example consists of a subject and a message body:

      {
          "_id": ObjectId("61741c9cbc9ec583c836174c"),
          "subject": "Books on kinematics and dynamics",
          "message": "Hello! Could you recommend good introductory books covering the topics of kinematics and dynamics? Thanks!",
          "posted_on": ISODate("2021-07-23T16:03:21Z")
      }
      

      You could use either of the two approaches discussed previously — embedding and child references — to model this relationship. If you were to decide on embedding, the student’s document might take a shape like this:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "emails": [
              {
                  "email": "sammy@digitalocean.com",
                  "type": "work"
              },
              {
                  "email": "sammy@example.com",
                  "type": "home"
              }
          ],
          "courses": [
              ObjectId("61741c9cbc9ec583c836170a"),
              ObjectId("61741c9cbc9ec583c836170b")
          ],
          "message_board_messages": [
              {
                  "subject": "Books on kinematics and dynamics",
                  "message": "Hello! Could you recommend good introductory books covering the topics of kinematics and dynamics? Thanks!",
                  "posted_on": ISODate("2021-07-23T16:03:21Z")
              },
              . . .
          ]
      }
      

      However, if a student is prolific with writing messages their document will quickly become incredibly long and could easily exceed the 16MB size limit, so the cardinality of this relation suggests against embedding. Additionally, the messages might need to be accessed separately from the student, as could be the case if the message board page is designed to show the latest messages posted by students. This also suggests that embedding is not the best choice for this scenario.

      Note: You should also consider whether the message board messages are frequently accessed when retrieving the student’s document. If not, having them all embedded inside that document would incur a performance penalty when retrieving and manipulating this document, even when the list of messages would not be used often. Infrequent access of related data is often another clue that you shouldn’t embed documents.

      Now consider using child references instead of embedding full documents as in the previous example. The individual messages would be stored in a separate collection, and the student’s document could then have the following structure:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "emails": [
              {
                  "email": "sammy@digitalocean.com",
                  "type": "work"
              },
              {
                  "email": "sammy@example.com",
                  "type": "home"
              }
          ],
          "courses": [
              ObjectId("61741c9cbc9ec583c836170a"),
              ObjectId("61741c9cbc9ec583c836170b")
          ],
          "message_board_messages": [
              ObjectId("61741c9cbc9ec583c836174c"),
              . . .
          ]
      }
      

      In this example, the message_board_messages field now stores the child references to all messages written by Sammy. However, changing the approach solves only one of the issues mentioned before in that it would now be possible to access the messages independently. But although the student’s document size would grow more slowly using the child references approach, the collection of object identifiers could also become unwieldy given the unbounded cardinality of this relation. A student could easily write thousands of messages during their four years of study, after all.

      In such scenarios, a common way to connect one object to another is through parent references. Unlike the child references described previously, it’s now not the student document referring to individual messages, but rather a reference in the message’s document pointing towards the student that wrote it.

      To use parent references, you would need to modify the message document schema to contain a reference to the student who authored the message:

      {
          "_id": ObjectId("61741c9cbc9ec583c836174c"),
          "subject": "Books on kinematics and dynamics",
          "message": "Hello! Could you recommend a good introductory books covering the topics of kinematics and dynamics? Thanks!",
          "posted_on": ISODate("2021-07-23T16:03:21Z"),
          "posted_by": ObjectId("612d1e835ebee16872a109a4")
      }
      

      Notice the new posted_by field contains the object identifier of the student’s document. Now, the student’s document won’t contain any information about the messages they’ve posted:

      {
          "_id": ObjectId("612d1e835ebee16872a109a4"),
          "first_name": "Sammy",
          "last_name": "Shark",
          "emails": [
              {
                  "email": "sammy@digitalocean.com",
                  "type": "work"
              },
              {
                  "email": "sammy@example.com",
                  "type": "home"
              }
          ],
          "courses": [
              ObjectId("61741c9cbc9ec583c836170a"),
              ObjectId("61741c9cbc9ec583c836170b")
          ]
      }
      

      To retrieve the list of messages written by a student, you would use a query on the messages collection and filter against the posted_by field. Having them in a separate collection makes it safe to let the list of messages grow without affecting any of the student’s documents.

      Note: When using parent references, creating an index on the field referencing the parent document can significantly increase the query performance each time you filter against the parent document identifier.

      If you model a one-to-many relationship where the amount of related documents is unbounded, regardless of whether the documents need to be accessed independently, it’s generally advised that you store related documents separately and use parent references to connect them to the parent document.

      Conclusion

      Thanks to the flexibility of document-oriented databases, determining the best way to model relationships in a document databases is less of a strict science than it is in a relational database. By reading this article, you’ve acquainted yourself with embedding documents and using child and parent references to store related data. You’ve learned about considering the relationship cardinality and avoiding unbounded arrays, as well as taking into account whether the document will be accessed separately or frequently.

      These are just a few guidelines that can help you model typical relationships in MongoDB, but modeling database schema is not a one size fits all. Always take into account your application and how it uses and updates the data when designing the schema.

      To learn more about schema design and common patterns for storing different kinds of data in MongoDB, we encourage you to check the official MongoDB documentation on that topic.



      Source link

      SOLID: os primeiros 5 princípios do design orientado a objeto


      Introdução

      SOLID é uma sigla para os primeiros cinco princípios do design orientado a objeto (OOD) criada por Robert C. Martin (também conhecido como Uncle Bob).

      Nota: embora esses princípios sejam aplicáveis a várias linguagens de programação, o código de amostra contido neste artigo usará o PHP.

      Esses princípios estabelecem práticas que contribuem para o desenvolvimento de software com considerações de manutenção e extensão à medida que o projeto cresce. A adoção dessas práticas também pode contribuir para evitar problemas de código, refatoração de código e o desenvolvimento ágil e adaptativo de software.

      SOLID significa:

      Neste artigo, cada princípio será apresentado individualmente para que você compreenda como o SOLID pode ajudá-lo(a) a melhorar como desenvolvedor(a).

      Princípio da responsabilidade única

      O Princípio da responsabilidade única (SRP) declara:

      Uma classe deve ter um e apenas um motivo para mudar, o que significa que uma classe deve ter apenas uma função.

      Por exemplo, considere um aplicativo que recebe uma coleção de formas — círculos e quadrados — e calcula a soma da área de todas as formas na coleção.

      Primeiramente, crie as classes de formas e faça com que os construtores configurem os parâmetros necessários.

      Para quadrados, será necessário saber o length (comprimento) de um lado:

      class Square
      {
          public $length;
      
          public function construct($length)
          {
              $this->length = $length;
          }
      }
      

      Para os círculos, será necessário saber o radius (raio):

      class Circle
      {
          public $radius;
      
          public function construct($radius)
          {
              $this->radius = $radius;
          }
      }
      

      Em seguida, crie a classe AreaCalculator e então escreva a lógica para somar as áreas de todas as formas fornecidas. A área de um quadrado é calculada pelo quadrado do comprimento. A área de um círculo é calculada por pi multiplicado pelo quadrado do raio.

      class AreaCalculator
      {
          protected $shapes;
      
          public function __construct($shapes = [])
          {
              $this->shapes = $shapes;
          }
      
          public function sum()
          {
              foreach ($this->shapes as $shape) {
                  if (is_a($shape, 'Square')) {
                      $area[] = pow($shape->length, 2);
                  } elseif (is_a($shape, 'Circle')) {
                      $area[] = pi() * pow($shape->radius, 2);
                  }
              }
      
              return array_sum($area);
          }
      
          public function output()
          {
              return implode('', [
                '',
                    'Sum of the areas of provided shapes: ',
                    $this->sum(),
                '',
            ]);
          }
      }
      

      Para usar a classe AreaCalculator, será necessário criar uma instância da classe, passar uma matriz de formas e exibir o resultado no final da página.

      Aqui está um exemplo com uma coleção de três formas:

      • um círculo com um raio de 2
      • um quadrado com um comprimento de 5
      • um segundo quadrado com um comprimento de 6
      $shapes = [
        new Circle(2),
        new Square(5),
        new Square(6),
      ];
      
      $areas = new AreaCalculator($shapes);
      
      echo $areas->output();
      

      O problema com o método de saída é que o AreaCalculator manuseia a lógica para gerar os dados.

      Considere um cenário onde o resultado deve ser convertido em outro formato, como o JSON.

      Toda a lógica seria manuseada pela classe AreaCalculator. Isso violaria o princípio da responsabilidade única. A classe AreaCalculator deve estar preocupada somente com a soma das áreas das formas fornecidas. Ela não deve se importar se o usuário quer JSON ou HTML.

      Para resolver isso, crie uma classe separada chamada SumCalculatorOutputter e use essa nova classe para lidar com a lógica necessária para gerar os dados para o usuário:

      class SumCalculatorOutputter
      {
          protected $calculator;
      
          public function __constructor(AreaCalculator $calculator)
          {
              $this->calculator = $calculator;
          }
      
          public function JSON()
          {
              $data = [
                'sum' => $this->calculator->sum(),
            ];
      
              return json_encode($data);
          }
      
          public function HTML()
          {
              return implode('', [
                '',
                    'Sum of the areas of provided shapes: ',
                    $this->calculator->sum(),
                '',
            ]);
          }
      }
      

      A classe SumCalculatorOutputter funcionaria da seguinte forma:

      $shapes = [
        new Circle(2),
        new Square(5),
        new Square(6),
      ];
      
      $areas = new AreaCalculator($shapes);
      $output = new SumCalculatorOutputter($areas);
      
      echo $output->JSON();
      echo $output->HTML();
      

      Agora, a lógica necessária para gerar os dados para o usuário é manuseada pela classe SumCalculatorOutputter.

      Isso satisfaz o princípio da responsabilidade única.

      Princípio do aberto-fechado

      O Princípio do aberto-fechado (S.R.P.) declara:

      Os objetos ou entidades devem estar abertos para extensão, mas fechados para modificação.

      Isso significa que uma classe deve ser extensível sem que seja modificada.

      Vamos revisitar a classe AreaCalculator e focar no método sum(soma):

      class AreaCalculator
      {
          protected $shapes;
      
          public function __construct($shapes = [])
          {
              $this->shapes = $shapes;
          }
      
          public function sum()
          {
              foreach ($this->shapes as $shape) {
                  if (is_a($shape, 'Square')) {
                      $area[] = pow($shape->length, 2);
                  } elseif (is_a($shape, 'Circle')) {
                      $area[] = pi() * pow($shape->radius, 2);
                  }
              }
      
              return array_sum($area);
          }
      }
      

      Considere um cenário onde o usuário deseja a sum de formas adicionais, como triângulos, pentágonos, hexágonos, etc. Seria necessário editar constantemente este arquivo e adicionar mais blocos de if/else. Isso violaria o princípio do aberto-fechado.

      Uma maneira de tornar esse método sum melhor é remover a lógica para calcular a área de cada forma do método da classe AreaCalculator e anexá-la à classe de cada forma.

      Aqui está o método area definido em Square:

      class Square
      {
          public $length;
      
          public function __construct($length)
          {
              $this->length = $length;
          }
      
          public function area()
          {
              return pow($this->length, 2);
          }
      }
      

      E aqui está o método area definido em Circle:

      class Circle
      {
          public $radius;
      
          public function construct($radius)
          {
              $this->radius = $radius;
          }
      
          public function area()
          {
              return pi() * pow($shape->radius, 2);
          }
      }
      

      O método sum para AreaCalculator pode então ser reescrito como:

      class AreaCalculator
      {
          // ...
      
          public function sum()
          {
              foreach ($this->shapes as $shape) {
                  $area[] = $shape->area();
              }
      
              return array_sum($area);
          }
      }
      

      Agora, é possível criar outra classe de formas e a passar ao calcular a soma sem quebrar o código.

      No entanto, outro problema surge. Como saber que o objeto passado para o AreaCalculator é na verdade uma forma ou se a forma possui um método chamado area?

      Programar em uma interface é uma parte integral do SOLID.

      Crie uma ShapeInterface que suporte area:

      interface ShapeInterface
      {
          public function area();
      }
      

      Modifique suas classes de formas para implement (implementar) a ShapeInterface.

      Aqui está a atualização para Square:

      class Square implements ShapeInterface
      {
          // ...
      }
      

      E aqui está a atualização para Circle:

      class Circle implements ShapeInterface
      {
          // ...
      }
      

      No método sum para AreaCalculator, verifique se as formas fornecidas são na verdade instâncias de ShapeInterface; caso contrário, lance uma exceção:

       class AreaCalculator
      {
          // ...
      
          public function sum()
          {
              foreach ($this->shapes as $shape) {
                  if (is_a($shape, 'ShapeInterface')) {
                      $area[] = $shape->area();
                      continue;
                  }
      
                  throw new AreaCalculatorInvalidShapeException();
              }
      
              return array_sum($area);
          }
      }
      

      Isso satisfaz o princípio do aberto-fechado.

      Princípio da substituição de Liskov

      O Princípio da substituição de Liskov declara:

      Seja q(x) uma propriedade demonstrável sobre objetos de x do tipo T. Então q(y) deve ser demonstrável para objetos y do tipo S onde S é um subtipo de T.

      Isso significa que cada subclasse ou classe derivada deve ser substituível pela classe sua classe base ou pai.

      Analisando novamente a classe de exemplo AreaCalculator, considere uma nova classe VolumeCalculator que estende a classe AreaCalculator:

      class VolumeCalculator extends AreaCalculator
      {
          public function construct($shapes = [])
          {
              parent::construct($shapes);
          }
      
          public function sum()
          {
              // logic to calculate the volumes and then return an array of output
              return [$summedData];
          }
      }
      

      Lembre-se que a classe SumCalculatorOutputter se assemelha a isto:

      class SumCalculatorOutputter {
          protected $calculator;
      
          public function __constructor(AreaCalculator $calculator) {
              $this->calculator = $calculator;
          }
      
          public function JSON() {
              $data = array(
                  'sum' => $this->calculator->sum();
              );
      
              return json_encode($data);
          }
      
          public function HTML() {
              return implode('', array(
                  '',
                      'Sum of the areas of provided shapes: ',
                      $this->calculator->sum(),
                  ''
              ));
          }
      }
      

      Se você tentar executar um exemplo como este:

      $areas = new AreaCalculator($shapes);
      $volumes = new VolumeCalculator($solidShapes);
      
      $output = new SumCalculatorOutputter($areas);
      $output2 = new SumCalculatorOutputter($volumes);
      

      Quando chamar o método HTML no objeto $output2, você irá obter um erro E_NOTICE informando uma conversão de matriz em string.

      Para corrigir isso, em vez de retornar uma matriz do método de soma de classe VolumeCalculator, retorne $summedData:

      class VolumeCalculator extends AreaCalculator
      {
          public function construct($shapes = [])
          {
              parent::construct($shapes);
          }
      
          public function sum()
          {
              // logic to calculate the volumes and then return a value of output
              return $summedData;
          }
      }
      

      O $summedData pode ser um float, duplo ou inteiro.

      Isso satisfaz o princípio da substituição de Liskov.

      Princípio da segregação de interfaces

      O Princípio da segregação de interfaces declara:

      Um cliente nunca deve ser forçado a implementar uma interface que ele não usa, ou os clientes não devem ser forçados a depender de métodos que não usam.

      Ainda utilizando o exemplo anterior do ShapeInterface, você precisará suportar as novas formas tridimensionais Cuboid e Spheroid, e essas formas também precisarão ter o volume calculado.

      Vamos considerar o que aconteceria se você modificasse a ShapeInterface para adicionar outro contrato:

      interface ShapeInterface
      {
          public function area();
      
          public function volume();
      }
      

      Agora, qualquer forma criada deve implementar o método volume, mas você sabe que os quadrados são formas planas que não têm volume, de modo que essa interface forçaria a classe Square a implementar um método sem utilidade para ela.

      Isso violaria o princípio da segregação de interfaces. Ao invés disso, você poderia criar outra interface chamada ThreeDimensionalShapeInterface que possui o contrato volume e as formas tridimensionais poderiam implementar essa interface:

      interface ShapeInterface
      {
          public function area();
      }
      
      interface ThreeDimensionalShapeInterface
      {
          public function volume();
      }
      
      class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
      {
          public function area()
          {
              // calculate the surface area of the cuboid
          }
      
          public function volume()
          {
              // calculate the volume of the cuboid
          }
      }
      

      Essa é uma abordagem muito mais vantajosa, mas uma armadilha a ser observada é quando sugerir o tipo dessas interfaces. Ao invés de usar uma ShapeInterface ou uma ThreeDimensionalShapeInterface, você pode criar outra interface, talvez ManageShapeInterface, e implementá-la tanto nas formas planas quanto tridimensionais.

      Dessa forma, é possível ter uma única API para gerenciar todas as formas:

      interface ManageShapeInterface
      {
          public function calculate();
      }
      
      class Square implements ShapeInterface, ManageShapeInterface
      {
          public function area()
          {
              // calculate the area of the square
          }
      
          public function calculate()
          {
              return $this->area();
          }
      }
      
      class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
      {
          public function area()
          {
              // calculate the surface area of the cuboid
          }
      
          public function volume()
          {
              // calculate the volume of the cuboid
          }
      
          public function calculate()
          {
              return $this->area();
          }
      }
      

      Agora, na classe AreaCalculator, substitua a chamada do método area por calculate e verifique se o objeto é uma instância da ManageShapeInterface e não da ShapeInterface.

      Isso satisfaz o princípio da segregação de interfaces.

      Princípio da inversão de dependência

      O princípio da inversão de dependência declara:

      As entidades devem depender de abstrações, não de implementações. Ele declara que o módulo de alto nível não deve depender do módulo de baixo nível, mas devem depender de abstrações.

      Esse princípio permite a desestruturação.

      Aqui está um exemplo de um PasswordReminder que se conecta a um banco de dados MySQL:

      class MySQLConnection
      {
          public function connect()
          {
              // handle the database connection
              return 'Database connection';
          }
      }
      
      class PasswordReminder
      {
          private $dbConnection;
      
          public function __construct(MySQLConnection $dbConnection)
          {
              $this->dbConnection = $dbConnection;
          }
      }
      

      Primeiramente, o MySQLConnection é o módulo de baixo nível, enquanto o PasswordReminder é de alto nível. No entanto, de acordo com a definição de D em SOLID, que declara Dependa de abstrações e não de implementações, Esse trecho de código acima viola esse princípio, uma vez que a classe PasswordReminder está sendo forçada a depender da classe MySQLConnection.

      Mais tarde, se você alterasse o mecanismo do banco de dados, também teria que editar a classe PasswordReminder e isso violaria o princípio do aberto-fechado.

      A classe PasswordReminder não deve se importar com qual banco de dados seu aplicativo usa. Para resolver esses problemas, programe em uma interface, uma vez que os módulos de alto e baixo nível devem depender de abstrações:

      interface DBConnectionInterface
      {
          public function connect();
      }
      

      A interface possui um método de conexão e a classe MySQLConnection implementa essa interface. Além disso, em vez de sugerir o tipo diretamente da classe MySQLConnection no construtor do PasswordReminder, você sugere o tipo de DBConnectionInterface. Sendo assim, independentemente do tipo de banco de dados que seu aplicativo usa, a classe PasswordReminder poderá se conectar ao banco de dados sem problemas e o princípio do aberto-fechado não será violado.

      class MySQLConnection implements DBConnectionInterface
      {
          public function connect()
          {
              // handle the database connection
              return 'Database connection';
          }
      }
      
      class PasswordReminder
      {
          private $dbConnection;
      
          public function __construct(DBConnectionInterface $dbConnection)
          {
              $this->dbConnection = $dbConnection;
          }
      }
      

      Esse código estabelece que tanto os módulos de alto quanto de baixo nível dependem de abstrações.

      Conclusão

      Neste artigo, os cinco princípios do Código SOLID foram-lhe apresentados. Projetos que aderem aos princípios SOLID podem ser compartilhados com colaboradores, estendidos, modificados, testados e refatorados com menos complicações.

      Continue seu aprendizado lendo sobre outras práticas para o desenvolvimento de software ágil e adaptativo.



      Source link