So really, what do you all see in consistent practice?
They both exist for a reason, they're both useful when the other isn't. You're always going to be tightly coupled to a table in SQL land. It's just your choice if you want to couple it via column index or string.
In practice, I've never designed a system to use Data Tables. I have a strongly typed Object Oriented Language so I choose to use objects over generic data containers.
There are ORM frameworks like Entity Framework (microsoft), Dapper, NHibernate and more that allow your Database Row (XML nodes, Json Object etc) to represent a .net Object (defined, anonymous and sometimes Tuples). The goal is to take a data from a storage system system and convert your request into a class. After retrieving data;
A Data Table Might look like:
[1]Id [2]FirstName [3]LastName
(int) (string) (string)
--------------------------------------------
1 Erik Philips
So computing a Full Name becomes code that looks like:
var fullName = $"{dt.Rows[i][2].ToString()} {dt.Rows[i][3].ToString()}";
or
var fullName = $"{dt.Rows[i]["FirstName"].ToString()} {dt.Rows[i]["LastName"].ToString()}";
Which does it's job but the logic in an Object Oriented Language should be encapsulated. Using an ORM your data is requested via a class that represents data access and is configured to get the data and map it (which I won't go into detail about the specifics). First you start by creating a POCO:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
and then you ask for them via the Data Access Class;
Entity Framework (DbContext)
var person = DbContext.Persons.FirstOrDefault(p => p.id == 1);
// OR
var person = DbContext.Set<Person>().FirstOrDefault(p => p.id == 1);
Dapper: (*I've not used dapper so this might be technically wrong but you get the idea)
var person = connection.Query<Person>("Select * FROM CUSTOMERS WHERE Id = 1").FirstOrDefault()
NHibernate:
var person = session.Get<Person>(Id);
In all these instances you get back a Person class/object. Now we can encapsulate business logic (we need to have a consistent way to represent "Full Name" to our data consumers).
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BornOn { get; set; }
public boolean IsMarried { get; set; }
public string FullName
{
get
{
return $"{FirstName} {LastName}";
}
}
}
but is that the main goal of it all?
In my personal opinion, the goal of it all is to aid the developer writing good code, which includes (items relevant to the question I'll comment on);
using the S.O.L.I.D. Principles:
Single responsibility principle
// What else could this class possibly be used for?
public class Person ....
Writing readable code with as little documentation as possible.
var tom = dbContext.Persons.FirstOrDefault(p => p.FirstName == "Tom");
if (tom.IsMarried) ...
if (tom.BornOn > DateTime.Now) ...