Introduction to Spring Converters and Formatters

SHARE & COMMENT :

Introduction

In this article, we will provide introductory details on Spring Converters and Formatters. Converter components are used for converting one type to another type and also to provide a cleaner separation by forcing to place all such conversion related code in one single place. Spring already supports built-in converters for the commonly used types and the framework is extensible enough for writing custom converters as well. Spring Formatters come into picture to format the data according to the display where it is rendered.
Examples may include formatting date/timestamp values according to locales etc. The first section of this article deals with Converters whereas the rest deals with Formatters and plenty of code samples are given at appropriate places for better illustration. This article assumes that readers has the sufficient knowledge on Spring Framework and its workflow. If you are beginner looking for basic concepts on Spring Framework, please read Introduction to Spring Framework and Introduction to Spring Web Flow (SWF). The following section provides the list of populate articles in the Spring Framework.
also read:

Built-in Converters

In this section, we will look into the series of Built-in Converters in Spring. It’s always worthwhile to see the exhaustive list of pre-built converters before even thinking of writing a custom converter that suits for a particular business need. Converters in Spring are available as services and typically client will make use of converter services while working with the conversion process.

package net.javabeat.articles.spring.converter.builtin;

import java.util.List;

import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;

public class BuiltInForStringTypeTest {

	public static void main(String[] args) {

		GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService();

		testToArray(conversionService);
		testToCollection(conversionService);
		testToBoolean(conversionService);
		testToCharacter(conversionService);
		testToNumber(conversionService);
		testToEnum(conversionService);
	}

	private static void testToArray(GenericConversionService conversionService){

		String[] stringArray = conversionService.convert("One,Two,Three", String[].class);
		for (String element : stringArray){
			System.out.println("Element is " + element);
		}
	}

	private static void testToCollection(GenericConversionService conversionService){

		@SuppressWarnings("unchecked")
		List listOfStrings = conversionService.convert("One,Two,Three", List.class);
		for (String element : listOfStrings){
			System.out.println("Element is " + element);
		}
	}

	private static void testToBoolean(GenericConversionService conversionService){

		Boolean data = null;

		data = conversionService.convert("true", Boolean.class);
		System.out.println("Boolean value is " + data);

		data = conversionService.convert("no", Boolean.class);
		System.out.println("Boolean value is " + data);
	}

	private static void testToCharacter(GenericConversionService conversionService){

		Character data = null;

		data = conversionService.convert("A", Character.class);
		System.out.println("Character value is " + data);

		data = conversionService.convert("Exception", Character.class);
		System.out.println("Character value is " + data);
	}

	private static void testToNumber(GenericConversionService conversionService){

		Integer intData = conversionService.convert("124", Integer.class);
		System.out.println("Integer value is " + intData);

		Float floatData = conversionService.convert("215f", Float.class);
		System.out.println("Float value is " + floatData);
	}

	private static void testToEnum(GenericConversionService conversionService){

		TaskStatus taskStatus = conversionService.convert("PENDING", TaskStatus.class);
		System.out.println("Task Status is " + taskStatus);
	}

}

Go through the above code listing which illustrates the concept of converters. In the above sample code, an attempt is made to convert a string object to various different types such as Array, Collection, Boolean, Character, Number and Enumeration. The example is pretty straightforward, though it is worthwhile to provide description on what each test method is doing. It is essential to the client to have an instance of ConversionService before working out with converts and the same is obtained through ConversionServiceFactory.
For converting a string to an array (a string array), the string has to be comma-delimited, though there is currently no support for the client to pass the delimiter. The same thing holds good for converting a string to a Collection type. String objects can even be converted to Boolean objects. The valid string values for a corresponding ‘true’ Boolean object are ‘true’, ‘on’, ‘yes’ and ’1′, whereas for a ‘falsify’ a Boolean object, the allowed values are ‘false’, ‘off’, ‘no’ and ’0′. For converting a string to a character, the string holding the character must be of length one, else an ‘IllegalArgumentException’ will be thrown at the run-time.

package net.javabeat.articles.spring.converter.builtin;

public enum TaskStatus {

	STARTED,

	COMPLETED,

	PENDING
}

Similarly, while converting a string to an enumeration, the appropriate valueOf() method will be called on the Enum object. In the above example, an attempt is made to convert the task status to a TaskStatus enumeration object.

Converting Array types to Collection and String

In the following example, we will see how to use Converters for converting an Array type to other different types such as Collection, String and to a Generic object.

package net.javabeat.articles.spring.converter.builtin;

import java.util.List;

import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.StringToArticleConverter;

import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;

public class BuiltInForArrayTypeTest {

	public static void main(String[] args) {

		GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService();

		testToCollection(conversionService);
		testToString(conversionService);
		testToObject(conversionService);
	}

	private static void testToCollection (GenericConversionService conversionService){

		@SuppressWarnings("unchecked")
		List listOfColors = conversionService.convert(
			new String[]{"Red","Blue","Green"}, List.class);
		for (String color : listOfColors){
			System.out.println("Color is " + color);
		}
	}

	private static void testToString(GenericConversionService conversionService){

		String colors = conversionService.convert(
			new String[]{"Red","Blue","Green"}, String.class);
		System.out.println("Colors is " + colors);
	}

	private static void testToObject(GenericConversionService conversionService){

		conversionService.addConverter(new StringToArticleConverter());

		Article article = conversionService.convert(
			new String[]{"Introduction to Google Guice,Google Guice"}, Article.class);
		System.out.println("Article name is " + article.getName());
		System.out.println("Article category is " + article.getCategory());
	}

}

In the above example, we have converted the array of string (representing colors) to a list of string objects by calling the convert() defined on the ConversionService object. The example also illustrates how a conversion can happen from a string array type to a string type. Note that the resultant string will have the comma delimiter strings from the string array. The method ‘testToObject()’ introduces a custom Domain object called ‘Article’ which will be explained later.

package net.javabeat.articles.spring.converter.builtin;

import java.util.Arrays;
import java.util.List;

import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.ArticleToStringConverter;

import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;

public class BuiltInForCollectionTypeTest {

	public static void main(String[] args) {

		GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService();

		testToArray(conversionService);
		testToString(conversionService);
		testToObject(conversionService);
	}

	private static void testToArray(GenericConversionService conversionService){

		List languages = Arrays.asList("C", "C++", "Java");
		String[] stringArray = conversionService.convert(languages, String[].class);
		for (String language : stringArray){
			System.out.println("Language is " + language);
		}
	}

	private static void testToString(GenericConversionService conversionService){

		List languages = Arrays.asList("C", "C++", "Java");
		String languagesAsString = conversionService.convert(languages, String.class);
		System.out.println("All languages -->" + languagesAsString);
	}

	private static void testToObject(GenericConversionService conversionService){

		conversionService.addConverter(new ArticleToStringConverter());

		Article articleObject = new Article("Introduction to Google Guice", "Google Guice");
		String articleAsString = conversionService.convert(
			new Article[]{articleObject}, String.class);
		System.out.println("Article -->" + articleAsString);
	}
}

The above example shows how to convert Collection types to Array, string and Object type. Note that the test methods follow the very similar pattern as shown in the previous example.

Custom Converters using Spring Converters and Formatters

So far we have seen how to make use of Spring Converters for performing conversions between the basic and the very often used data types in the Java programming language. However that may not often suffice and there will be always a necessity to do conversions on user-defined objects. As always Spring’s framework is extensible and in this section, we will illustrate the usage of Custom converters for converting user-defined or custom objects.

package net.javabeat.articles.spring.converter.custom;

public class Article {

	private String name;
	private String category;

	public Article(String name, String category){
		this.name = name;
		this.category = category;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getCategory() {
		return category;
	}

	public void setCategory(String category) {
		this.category = category;
	}
}

The user-defined class we will be using for illustrating the concept as well as throughout this article is ‘Article’. As you can see in the above definition, the structure of ‘Article’ class is very simple; it has two properties, the ‘name’ and the ‘category’.

package net.javabeat.articles.spring.converter.custom;

import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;

public class ArticleToStringConverter implements Converter<Article, String>{

	@Override
	public String convert(Article article) {

		if (article == null){
			throw new ConversionFailedException(TypeDescriptor.valueOf(Article.class),
				TypeDescriptor.valueOf(String.class), article, null);
		}

		StringBuilder builder = new StringBuilder();
		builder.append(article.getName());
		builder.append("-");
		builder.append(article.getCategory());
		return builder.toString();
	}

}

In this section, we will see how to convert a given ‘Article’ object to a ‘String’ object by writing a custom converter. As you can see in the above listing, for writing any custom converter, the interface Converter needs to be implemented and the method convert() has to be overridden. This interface uses Java Generics to achieve maximum compile-time safety and that is obvious in the declaration of the interface itself. The interface accepts the source and the target types as type parameters and the method convert() accepts the same source parameter and returns the target parameter. In our example case, the source parameter will be of type ‘Article’ and the target parameter will be of ‘String’. In the implementation of the convert() method, after performing suitable null-conditional checks, we return an instance of string object after concatenating the various properties of the article.
Before seeing the usage of this Custom converter, we will also write the reverse converter class which tries to convert a string object to an Article object. Note the following class definition below. Here the source type is ‘String’ and the target type is ‘Article’.

package net.javabeat.articles.spring.converter.custom;

import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;

public class StringToArticleConverter implements Converter<String, Article>{

	@Override
	public Article convert(String articleAsString) {

		if (articleAsString == null){
			throw new ConversionFailedException(TypeDescriptor.valueOf(String.class),
				TypeDescriptor.valueOf(String.class), articleAsString, null);
		}

		String[] tempArray = articleAsString.split(",");
		Article article = new Article(tempArray[0], tempArray[1]);
		return article;
	}

}

In the above sample, it is expected that the name and the category properties of the article object are concatenated as a string with comma as a delimiter. Hence appropriate parsing is done within the implementation of convert() method and an appropriate instance of Article object is constructed from the string.

package net.javabeat.articles.spring.converter.custom;

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;

public class CustomConverterTest {

	public static void main(String[] args) {

//		test1();
		test2();
		test3();
	}

	private static void test1(){

		GenericConversionService conversionService = new GenericConversionService();

		Converter<Article, String> customConverter = new ArticleToStringConverter();
		conversionService.addConverter(customConverter);

		String result = customConverter.convert(null);
		System.out.println("Result is " + result);
	}

	private static void test2(){

		GenericConversionService conversionService = new GenericConversionService();

		Converter<Article, String> customConverter = new ArticleToStringConverter();
		conversionService.addConverter(customConverter);

		Article article = new Article("Introduction to Spring Converters", "Core Spring");
		String result = conversionService.convert(article, String.class);
		System.out.println("Result is '" + result + "'");
	}

	private static void test3(){

		GenericConversionService conversionService = new GenericConversionService();

		Converter<String, Article> customConverter = new StringToArticleConverter();
		conversionService.addConverter(customConverter);

		String articleAsString = new String(
			"Introduction to Spring Converters,Core Spring");
		Article result = conversionService.convert(articleAsString, Article.class);
		System.out.println("Article name is " + result.getName());
		System.out.println("Article category is " + result.getCategory());
	}
}

It is necessary to make any Custom Converters visible to the Spring’s Conversion framework by appropriately registering them. Registration of a custom converter to the Converter registry can be achieved by calling the method addConverter() defined on the ConversionService object. In the ‘test2()’ method, we have registered the ‘ArticleToStringConverter’ and have called the method ‘convert()’ defined on ConversionService by passing in an article object. Similarly in the method test3(), the converter ‘StringToArticleConverter’ is registered and a similar attempt is made to convert the String object to an Article object.

Converter Factory

In this section, we will see the usage of Converter Factories which follow the Factory Design pattern for creating Converter objects. Converter Factory provides a centralized place for creating converter objects. This prevents the client from directly getting exposed to a series of custom converter classes in an application.

package net.javabeat.articles.spring.converter.factory;

import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.StringToArticleConverter;

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

public class StringToArticleConverterFactory implements ConverterFactory<String, Article>{

	@SuppressWarnings("unchecked")
	@Override
	public  Converter<String, T> getConverter(Class arg0) {
		return (Converter<String, T>) new StringToArticleConverter();
	}
}

Have a look at the above sample where we create a factory for ‘StringToArticle’ converter. Note that any converter factory class must implement ‘ConvertFactory’ interface and the method getConverter() has to overridden that will create and return a suitable converter object. Similarly, in the below listing, a factory is created for wrapping the implementation for ‘StringToArticleConverter’.

package net.javabeat.articles.spring.converter.factory;

import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.ArticleToStringConverter;

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

public class ArticleToStringConverterFactory implements ConverterFactory<Article, String>{

	@SuppressWarnings("unchecked")
	@Override
	public  Converter<Article, T> getConverter(Class arg0) {
		return (Converter<Article, T>)new ArticleToStringConverter();
	}

}

When it comes to the client usage, we register the custom converter factory implementations by calling the method ‘addCustomFactory()’ defined on the ConversionService object. Have a glance over the following piece of client code.

package net.javabeat.articles.spring.converter.factory;

import net.javabeat.articles.spring.converter.custom.Article;

import org.springframework.core.convert.support.GenericConversionService;

public class FactoryTest {

	public static void main(String[] args) {

		GenericConversionService conversionService = new GenericConversionService();

		conversionService.addConverterFactory(new ArticleToStringConverterFactory());
		conversionService.addConverterFactory(new StringToArticleConverterFactory());

		String articleAsString = "Java Programming,Java";
		Article article = conversionService.convert(articleAsString, Article.class);
		System.out.println("Article name is " + article.getName());
		System.out.println("Article category is " + article.getCategory());

		articleAsString = conversionService.convert(article, String.class);
		System.out.println("Article as string is '" + articleAsString + "'");
	}
}

The custom converter factory implementations ‘ArticleToStringConversionFactory’ and ‘StringToArticleConversionFactory’ are appropriately registered by calling the method addConversionFactory(). Note that when this method is called, the converter instances returned from the converter factory will be registered and maintained in the Converter registry.

Built-in Formatters in Spring Converters and Formatters

Similar to built-in components available for Converter components, there are number of built-in Formatter components for formatting date, timestamp and numeric data.

package net.javabeat.articles.spring.formatter.builtin;

import java.util.Date;
import java.util.Locale;

import org.springframework.format.Formatter;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.format.number.NumberFormatter;

public class FormatterTest {

	public static void main(String[] args) throws Exception{

		testDateFormatter();
		testNumberFormatter();
	}

	private static void testDateFormatter(){

		Formatter dateFormatter = new DateFormatter();
		String dateAsString = dateFormatter.print(new Date(), Locale.CHINA);
		System.out.println("Date as string in chinese locale is " + dateAsString);
	}

	private static void testNumberFormatter() throws Exception{

		NumberFormatter doubleFormatter = new NumberFormatter();
		doubleFormatter.setPattern("#####.###");
		String number = doubleFormatter.print(new Double(12325.1144d), Locale.ITALIAN);
		System.out.println("Number is " + number);
	}
}

Have a look at the above sample code. Formatters in Spring are modeled through the Formatter interface which in turn extends the Printer and the Parser interface. The interface Printer encapsulates the behavior of displaying the data on a particular locale whereas the interface Parser is responsible for parsing the data for the given locale. Implementations are free to ignore the locale if they want to. The above example illustrates the usage of Date and Number Formatters.

Custom Formatters

It is always possible to write Custom formatters especially for parsing and formatting user-defined objects. For illustration purposes, we will consider the user-defined Domain object CreditCardNumber in this example.

package net.javabeat.articles.spring.formatter.custom;

public class CreditCardNumber {

	private int firstFourDigits;
	private int secondFourDigits;
	private int thirdFourDigits;
	private int fourthFourDigits;

	public CreditCardNumber(){}

	public CreditCardNumber(int firstFourDigits, int secondFourDigits,
		int thirdFourDigits, int fourthFourDigits){
		this.firstFourDigits = firstFourDigits;
		this.secondFourDigits = secondFourDigits;
		this.thirdFourDigits = thirdFourDigits;
		this.firstFourDigits = firstFourDigits;
	}

	public int getFirstFourDigits() {
		return firstFourDigits;
	}
	public void setFirstFourDigits(int firstFourDigits) {
		this.firstFourDigits = firstFourDigits;
	}
	public int getSecondFourDigits() {
		return secondFourDigits;
	}
	public void setSecondFourDigits(int secondFourDigits) {
		this.secondFourDigits = secondFourDigits;
	}
	public int getThirdFourDigits() {
		return thirdFourDigits;
	}
	public void setThirdFourDigits(int thirdFourDigits) {
		this.thirdFourDigits = thirdFourDigits;
	}
	public int getFourthFourDigits() {
		return fourthFourDigits;
	}
	public void setFourthFourDigits(int fourthFourDigits) {
		this.fourthFourDigits = fourthFourDigits;
	}

}

The listing for the model class CreditCardNumber is shown below. As one can see, the structure is pretty simple as it holds four integer properties representing the four ‘four digits number that appear in a credit card.
For writing a custom formatter, we need to define the behavior of parsing and printing (displaying) the domain objects. Parsing of an object is encapsulated in the Parser interface and one needs to implement this interface to write a custom parser. Again, the Parser interface supports compile-time safety by defining the generic type T.

package net.javabeat.articles.spring.formatter.custom;

import java.text.ParseException;
import java.util.Locale;

import org.springframework.format.Parser;

public class CreditCardNumberParser implements Parser{

	@Override
	public CreditCardNumber parse(String ccNumber, Locale locale) throws ParseException {

		String digitsArray[] = ccNumber.split("-");
		if (digitsArray == null || digitsArray.length != 4){
			throw new org.springframework.expression.ParseException(-1, "Invalid format");
		}

		CreditCardNumber ccNumberObject = new CreditCardNumber();
		ccNumberObject.setFirstFourDigits(Integer.parseInt(digitsArray[0]));
		ccNumberObject.setSecondFourDigits(Integer.parseInt(digitsArray[1]));
		ccNumberObject.setThirdFourDigits(Integer.parseInt(digitsArray[2]));
		ccNumberObject.setFourthFourDigits(Integer.parseInt(digitsArray[3]));

		return ccNumberObject;
	}

}

Note that the method parse() has to be overridden for parsing the credit card number which will be passed as a string as designated by the first parameter. Note that the parse() method also accepts a Locale object as the second parameter. It is up to the implementation whether to consider or to ignore the ‘Locale’ parameter. For simplicity, in our example, we have ignored this parameter.
It is expected that the credit card number is passed as a string with the delimiter being ‘-’ between every set of four digits. The implementation checks if the format of the credit card number is correct, if not, it throws a ParseException thereby aborting the parse operation. Once the incoming string proves to be of correct format, an instance of Credit Card Number class is created and the delimited values obtained from the string is appropriately set to the object and returned.

package net.javabeat.articles.spring.formatter.custom;

import java.util.Locale;

import org.springframework.format.Printer;

public class CreditCardNumberPrinter implements Printer{

	@Override
	public String print(CreditCardNumber ccNumberObject, Locale locale) {

		return
				ccNumberObject.getFirstFourDigits() + "-"
			+	ccNumberObject.getSecondFourDigits() + "-"
			+ 	ccNumberObject.getThirdFourDigits() + "-"
			+ 	ccNumberObject.getFirstFourDigits();
	}

}

Displaying the data is encapsulated through the interface Printer and a custom displayable class for displaying an object is expected to implement this interface and thereby providing the functionality in the print() method. Note that the first parameter passed to print() is the object itself and the second parameter is the Locale object. Again, it is up to the implementation to consider or to ignore the Locale parameter. Within the implementation of the print() method we have constructed a suitable string from the CreditCardNumber object.

package net.javabeat.articles.spring.formatter.custom;

import java.text.ParseException;
import java.util.Locale;

import org.springframework.format.Formatter;
import org.springframework.format.Parser;
import org.springframework.format.Printer;

public class CreditCardNumberFormatter implements Formatter{

	private Parser parser;
	private Printer printer;

	public CreditCardNumberFormatter(Parser parser,
		Printer printer){
		this.parser = parser;
		this.printer = printer;
	}

	@Override
	public String print(CreditCardNumber ccNumberObject, Locale locale) {
		return printer.print(ccNumberObject, locale);
	}

	@Override
	public CreditCardNumber parse(String ccNumber, Locale locale) throws ParseException {
		return parser.parse(ccNumber, locale);
	}
}

Let us have a look at the implementation of Formatter class for formatting the credit card number. Formatter interface defines the abstract methods parse() and print() whose return types are Parser and Printer respectively. Because we have already provided the implementation of Parser and Printer, the methods parse() and print() redirects the control to the parse() and print() methods present in CreditCardNumberParser and CreditCardNumberPrinter respectively.

package net.javabeat.articles.spring.formatter.custom;

import org.springframework.format.support.FormattingConversionService;

public class Client {

	public static void main(String[] args) {

		FormattingConversionService service = new FormattingConversionService();

		CreditCardNumberParser parser = new CreditCardNumberParser();
		CreditCardNumberPrinter printer = new CreditCardNumberPrinter();
		service.addFormatterForFieldType(CreditCardNumber.class, printer, parser);

//		CreditCardNumberFormatter formatter = new CreditCardNumberFormatter(
//			parser, printer);
//		service.addFormatterForFieldType(CreditCardNumber.class, formatter);

		test1(service);
		test2(service);
	}

	private static void test1(FormattingConversionService service){

		String ccNumber = "1111-2222-3333-4444";
		CreditCardNumber ccNumberObject = service.convert(ccNumber, CreditCardNumber.class);

		System.out.println(ccNumberObject.getFirstFourDigits());
		System.out.println(ccNumberObject.getSecondFourDigits());
		System.out.println(ccNumberObject.getThirdFourDigits());
		System.out.println(ccNumberObject.getFourthFourDigits());
	}

	private static void test2(FormattingConversionService service){

		CreditCardNumber ccNumberObject = new CreditCardNumber(
			1111, 2222, 3333, 4444);

		String ccNumber = service.convert(ccNumberObject, String.class);
		System.out.println("CC Number is " + ccNumber);
	}

}

In the client program, we have to register the customized formatter implementation of Credit Card Number object by method addFormatterForFieldType() defined on the FormattingConversionService object. Note that the service ‘FormattingConversionService’ can be used to parse and print data as well as can be used for registering customized formatters. The method addFormatterForFieldType() accepts the class type for which the formatting has to be applied, printer object and the parser object as its arguments. An overloaded version of the same method is available which takes the class type and the formatter object directly.

Conclusion

This article started with explaining the needs to have converter components and went on to explaining the various built-in converters available in Spring framework. Code samples were provided to illustrate about writing custom converters also. As a closure to converters, the need for Converter Factories was also discussed in detail. The final section of the article explained the needs for formatters and discussed the various built-in formatters available in Spring. It also provided assistance in writing custom formatters with examples.
also read:

 

Comments

comments

About Krishna Srinivasan

He is Founder and Chief Editor of JavaBeat. He has more than 8+ years of experience on developing Web applications. He writes about Spring, DOJO, JSF, Hibernate and many other emerging technologies in this blog.

Comments

  1. Great tutorial

Trackbacks

  1. [...] Introduction to Spring Converters and Formatters [...]

Speak Your Mind

*

Close
Please support the site
By clicking any of these buttons you help our site to get better