What are the best practices for designing code when working with the continuous integration paradigm of small and often commits? For example, given a big feature, how to break it to smaller parts? How to develop an improved functionality while old is still alive. Etc.
As an example, o开发者_运维百科ne can use 'feature toggle' so a new feature lives dormant in a code until ready.
The domain languages I'm interested in are Java/C# and similar.
UPDATE: Note, I'm not asking about how to enable CI in general. Such answers go in the way of the need to set up a build server, having automated tests, etc. The question is how to design code so that frequent, small, commits work. E.g., all tests pass. Meaning both when introducing a new feature as well as modifying a feature.
First you would want to try to adhere to the Open/Closed Principle
From wikipedia:
"The idea was that once completed, the implementation of a class could only be modified to correct errors; new or changed features would require that a different class be created. That class could reuse coding from the original class through inheritance. The derived subclass might or might not have the same interface as the original class."
Applying this technique you would end up with both versions of the feature existing in the codebase at the same time. When you come to implement your toggle it is a matter of switching between the two versions of code at runtime, maybe using some sort of configuration value with dependency injection.
For code changes that require modification of the underlying data schema, you can setup a kind of "continuous conversion", that can convert data on the fly between the two implementations at runtime. Randy Shoup from ebay does a much better job of explaining this than I do in his Evolvable Systems talk on InfoQ.
These techniques should allow you to make smaller changes to the codebase and integrate more frequently.
I think that all the points Josh raised also help reach this goal. Whatever techniques lessen the risk of changing the system will let you work at a faster pace.
This is going to be open to lots of debate, but continuous integration and TDD go hand in hand. You need more than just a "does it compile?" level of tests to ensure that continually changing code is robust.
If you are following a high degree of testing coverage then any feature you push will be fully tested within the build before it can be accepted. This means you have to code in a way that will allow you to get entire slices of functionality in quickly, and then build out instead of up. (vertical slicing).
What does this mean in terms of code?
You need to rely heavily on the Dependency Inversion Principle
Instead of this:
public class PersonService
{
private SqlDataAccess DataAccess = new SqlDataAccess();
public void SavePerson(Person person)
{
DataAccess.SavePerson(person);
}
}
You should do this:
public class PersonService
{
private IDataAccess DataAccess {get; set;}
public PersonService(IDataAccess dataAccess)
{
if(dataAccess == null)
throw new ArgumentNullException("dataAccess");
DataAccess = dataAccess;
}
public void SavePerson(Person person)
{
DataAccess.SavePerson(person);
}
}
This is a simple example, but offers you the ability to make a decision like implementing an in-memory data access layer while fleshing out the UI and major functionality. Tests can still be written because you can mock the service, and you can push functionality more quickly since not every piece has to be 100% there. As the requirements start to solidify you can make a more informed decision about data access, and simply replace the temporary component with the more permanent one without worrying about breaking existing code.
And of course you would have automated integration tests running to ensure you don't break the rest of your code base right ;)
精彩评论