神刀安全网

S is for Single Responsibility Principle

This is part one in a five-part series of posts covering the SOLID Principles .

SOLID is a mnemonic acronym that helps define the five basic object-oriented design principles:

  • Single Responsibility Principle (this post)
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Over the next few weeks, I’ll dive into each principle, explain what it means, and how it relates to Android development. By the end of the series, you’ll have a firm grip on what these core principles mean, why they matter to you as an Android developer, and how you can apply them in your day-to-day development endeavors.

Background on SOLID

SOLID was introduced by Robert Martin (AKA: Uncle Bob) in the early 2000’s with the term being coined by Michael Feathers. When these five basic object-oriented design principles are applied together, they help developers facilitate the development of maintainable and extensible systems.

If you’re not familiar with Uncle Bob or Michael Feathers, I highly recommend picking up some of their books. Uncle Bob’s books “ Agile Software Development, Principles, Patterns and Practices ” and “ Clean Code ” are staples in the software community. Michael Feathers’ book “ Working Effectively with Legacy Code ” is the one book I require all developers to read when I’m leading a team. It helps reshape how you think about working with old code and making it maintainable again. More importantly, it helps you reformulate what “legacy” actually means. Hint – does your code have tests? No!?! Well, your code might already be … you guessed it … legacy.

Reading these books were defining moments in my career, and I highly recommend that every developer put them on their reading list, and then leave them on their bookshelf for occasional revisiting.

I personally remember implementing the SOLID principles as far back as 2003 in various .NET projects. At the time, the SOLID principles simply blew my mind because my .NET code was becoming a jumbled mess without much structure and guidance. This isn’t just a symptom of .NET, it happens all the time when new technologies emerge (in this case, mobile – Android, etc). The new technologies eventually reach a maturity level that opens the door to the SOLID conversation, and how and why it’s important.

Recently, Uncle Bob’s Clean Architecture talk has had a resurgence among the Android community, and I feel it’s now time to help explain some of the fundamental principles that Uncle Bob outlined in his books. These series of posts will talk about the SOLID principles and how they relate to Android development.

Part 1: The Single Responsibility Principle

The Single Responsibility Principle (SRP) is quite easy to understand. It states the following:

A class should have only one reason to change.

Let’s take the case of a RecyclerView and its adatper . As you probably already know, a RecyclerView is a flexible view which is capable of displaying a data set to the screen. In order for this data to get to the screen, we need a RecyclerView adapter.

An adapter takes the data from the data set and adapts it to a view. The most exercised part of an adapter is arguably the onBindViewHolder method (and sometimes ViewHolder itself, but we’ll just stick with onBindViewHolder for brevity’s sake). The RecyclerView’s adapter has one responsibility: mapping an object to its corresponding view that will be displayed on the screen.

Assume these objects and RecyclerView.Adapter implementation:

public class LineItem {     private String description;      private int quantity;      private long price;      // ... getters/setters }  public class Order {     private int orderNumber;      private List<LineItem> lineItems = new ArrayList<LineItem>();       // ... getters/setters }  public class OrderRecyclerAdapter extends RecyclerView.Adapter<OrderRecyclerAdapter.ViewHolder> {       private List<Order> items;     private int itemLayout;       public OrderRecyclerAdapter(List<Order> items, int itemLayout) {         this.items = items;         this.itemLayout = itemLayout;     }       @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {         View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);         return new ViewHolder(v);     }       @Override public void onBindViewHolder(ViewHolder holder, int position) {         // TODO: bind the view here      }       @Override public int getItemCount() {         return items.size();     }       public static class ViewHolder extends RecyclerView.ViewHolder {         public TextView orderNumber;         public TextView orderTotal;           public ViewHolder(View itemView) {             super(itemView);             orderNumber = (TextView) itemView.findViewById(R.id.order_number);             orderTotal = (ImageView) itemView.findViewById(R.id.order_total);         }     } }

In the example above, the onBindViewHolder is empty. An implementation I’ve seen many times could look like this:

@Override  public void onBindViewHolder(ViewHolder holder, int position) {     Order order = items.get(position);     holder.orderNumber.setText(order.getOrderNumber().toString());     long total = 0;     for (LineItem item : order.getItems()) {         total += item.getPrice();     }     NumberFormat formatter = NumberFormat.getCurrencyInstance(Locale.US);     String totalValue = formatter.format(cents / 100.0); // Must divide by a double otherwise we'll lose precision     holder.orderTotal.setText(totalValue)     holder.itemView.setTag(order); }

The code above violates the Single Responsibility Principle.

Why?

The adapter’s onBindViewHolder method is not only mapping from an Order object to the view, but is also performing price calculations as well as formatting. This violates the Single Responsibility Principle. The adapter should only be responsible for adapting an order object to its view representation. The onBindViewHolder is performing two extra duties that it should not be.

Why is this a problem?

Including multiple responsibilities in a class can cause various problems. First, the calculation logic for the order is now coupled to the adapter. If you need to display the total of an order elsewhere (most likely you do) you’ll have to replicate that logic. Once that happens, your application is exposed to traditional software logic duplication issues that we’re all familiar with. You update the code in one place and forget to update it in another location, etc. You get the point.

The second issue is the same as the first – you’ve coupled the formatting logic to the adapter. What if that needs to be moved or updated? At the end of the day, we’re making this class do more than it should, and now the application is more susceptible to bugs due to too much responsibility in one location.

Thankfully, this simple example can be easily fixed by extracting the order total calculation into the Order object and them moving the currency formatting into a currency formatter class of some sort. This formatter can then be used by the Order too.

An updated onBindViewHolder method could look like this:

@Override  public void onBindViewHolder(ViewHolder holder, int position) {     Order order = items.get(position);     holder.orderNumber.setText(order.getOrderNumber().toString());     holder.orderTotal.setText(order.getOrderTotal()); // A String, the calculation and formatting moved elsewhere     holder.itemView.setTag(order); }

I’m sure you’re probably thinking “Ok, that was easy. This is very simple isn’t it?”. Is it always this easy? As with most answers in software – “Well, it depends …”.

Let’s dig a bit deeper …

What is meant by “Responsibility” though?

It’s really hard to put it better than Uncle Bob, so I’m going to quote him here:

In the context of the Single Responsibility Principle (SRP) we define a responsibility as “a reason for change”. If you can think of more than one motive for changing a class, then that class has more than one responsibility.

The thing is, this is sometimes really hard to see – especially if you’ve been in the codebase for a long time. At that point, this famous quote usually comes to mind:

You can’t see the forest for the trees.

In the context of software, this means you’re too close to the details of your code to see the bigger picture. For example – the class you’re working on may look great, but that’s because you’ve been working with it for so long its hard to see that it may have multiple responsibilities.

The challenge is knowing when to apply SRP and when not to. Taking the adapter example into account, if we look at the code again, we see various things happening that could necessitate the need for change in different areas for different reasons:

public class OrderRecyclerAdapter extends RecyclerView.Adapter<OrderRecyclerAdapter.ViewHolder> {       private List<Order> items;     private int itemLayout;       public OrderRecyclerAdapter(List<Order> items, int itemLayout) {         this.items = items;         this.itemLayout = itemLayout;     }       @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {         View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);         return new ViewHolder(v);     }       @Override public void onBindViewHolder(ViewHolder holder, int position) {         Order order = items.get(position);         holder.orderNumber.setText(order.getOrderNumber().toString());         holder.orderTotal.setText(order.getOrderTotal()); // Move the calculation and formatting elsewhere         holder.itemView.setTag(order);     }        @Override public int getItemCount() {         return items.size();     }       public static class ViewHolder extends RecyclerView.ViewHolder {         public TextView orderNumber;         public TextView orderTotal;           public ViewHolder(View itemView) {             super(itemView);             orderNumber = (TextView) itemView.findViewById(R.id.order_number);             orderTotal = (ImageView) itemView.findViewById(R.id.order_total);         }     } }

The adapter is inflating a view, it’s binding an order to a view, it’s constructing the view holder, etc. This class has multiple responsibilities.

Should these responsibilities be broken apart?

This ultimately depends on how the application is changing over time. If the application changes in ways that affect the way the view is assembled and its connecting functions (the logic of the view), then as Uncle Bob states, the design will smell of Rigidity because one change is requiring another to change. The change of the view construction is also requiring a change of the adapter itself, causing the design to become rigid. However, it can also be argued that if the application is not changing in ways that require different functions to change at different times, then there is no need to separate them. In this case, separating them would be adding unnecessary needless complexity.

So, what do we do?

An Example to Illustrate Rigidity

Let’s assume a new product requirement comes in that proclaims when an order’s total amount is zero, the view should display a bright yellow “FREE” image on the screen instead of a textual total amount. Where would this logic go? In one code path, you need a TextView, and in another, you need an ImageView. There are two places code needs to be changed:

  1. In the view
  2. In the presentation logic

As with most applications I’ve seen, this applied at the adapter level. Unfortunately, this forces the Adapter to be changed when your view is changed. If the logic for this is in the adapter, then this forces the logic in the adapter to change, as well as the code for the view. This adds yet another responsibility to the adapter.

This is exactly the point where something like the Model-View-Presenter pattern offers necessary decoupling so that the classes do not become too rigid, yet provide the flexibility for extension, composability, and testing. For example, the view would implement an interface that defines how it will be interacted with, and the presenter would perform the necessary logic. The presenter in a Model-View-Presenter pattern is responsible for only the view/display logic, nothing more.

Moving this logic from the adapter into the presenter would help make the adapter adhere more to the single responsible principle.

That’s not all though …

If you’ve ever taken a deep look at any RecyclerView adapter, you’ve probably noticed that the adapter is doing a lot of things. Things the adapter still does:

  • Inflating the View
  • Creating the View Holder
  • Recycling The View Holder
  • Providing item count
  • etc.

Since SRP is about single responsibility, you’re probably wondering whether or not some of these behaviors should be extracted to adhere to SRP. Once again, I’m going to delegate to Uncle Bob Martin as his explanation really nails it:

An axis of change is only an axis of change if the changes actually occur. It is not wise to apply the SRP, or any other principle for that matter, if there is no symptom.

While the Adapter still performs various actions, that is, in fact, what it’s designed to do. After all, a RecyclerView adapter is simply an implementation of the Adapter pattern . In this case, keeping the view inflation and view holder mechanisms in place does make sense; that’s what this class’s responsibility is, and that’s what it does best. However, introducing additional behavior (like view logic) breaks SRP and can be avoided by using the Model-View-Presenter pattern or other refactorings.

Conclusion

The Single Responsibility Principle is probably one of the easiest of the SOLID principles to grok as it simply states (once again for completeness) –

A class should have only one reason to change.

That said, it’s also one of the hardest to apply. It’s easy to over-analyze code which can lead you to think you need to adhere to SRP, only to find out that if you do, you’ve now added complexity to your app. My advice is to try to take a step away from the code and look at it objectively. Remove the emotional attachment to the code and you’ll find you’re able to look at it with a fresh set of eyes. If you do this, you’ll most likely observe different things about your code that you may or may not have known. You may realize that you need to apply the Single Responsibility Pattern, or you may realize that you’ve done a great job already. Regardless, take your time, and think it through.

Lastly, as your application changes, you’ll find that you may need to apply SRP in areas that you previously did not need to. This is completely OK, and is recommended.

Happy coding. :)

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » S is for Single Responsibility Principle

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮